fluxplugin 1.0.3 → 1.0.4
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 +15 -12
- package/dist/browser/chunks/chunk-AV4KI7LI.js +1792 -0
- package/dist/browser/chunks/{chunk-TL63576R.js → chunk-OAYWYU5O.js} +48 -11
- package/dist/browser/index.js +3 -1
- package/dist/browser/manager-ui.js +386 -185
- package/dist/browser/react.js +46 -2
- package/dist/cli.js +3622 -257
- package/dist/node/chunks/{chunk-EVX2O65A.js → chunk-SDCGUBEW.js} +49 -11
- package/dist/node/chunks/{chunk-C7VWZZOX.js → chunk-ZLA5XUXN.js} +175 -51
- package/dist/node/cli-api.js +742 -76
- package/dist/node/index.js +3 -1
- package/dist/node/main.js +8 -2
- package/dist/node/preload.js +1 -1
- package/dist/types/cli/build.d.ts +5 -0
- package/dist/types/cli/cli.d.ts +2 -0
- package/dist/types/cli/dist.d.ts +18 -0
- package/dist/types/cli/host-templates.d.ts +2 -0
- package/dist/types/cli/store.d.ts +13 -0
- package/dist/types/cli/templates.d.ts +2 -0
- package/dist/types/core/manifest.d.ts +15 -1
- package/dist/types/core/protocol.d.ts +18 -5
- package/dist/types/core/registry.d.ts +11 -0
- package/dist/types/core/routes.d.ts +3 -1
- package/dist/types/main/adapters.d.ts +3 -0
- package/dist/types/main/assets.d.ts +8 -0
- package/dist/types/main/github-registry.d.ts +2 -1
- package/dist/types/main/index.d.ts +1 -0
- package/dist/types/main/registry.d.ts +55 -18
- package/dist/types/main/stores.d.ts +7 -1
- package/dist/types/manager-ui/index.d.ts +35 -3
- package/dist/types/react/base-css.d.ts +6 -2
- package/dist/types/react/http-bridge.d.ts +5 -0
- package/dist/types/react/i18n.d.ts +33 -0
- package/dist/types/react/index.d.ts +5 -0
- package/dist/types/react/markdown.d.ts +2 -1
- package/dist/types/react/media.d.ts +33 -0
- package/dist/types/react/settings.d.ts +6 -0
- package/dist/types/react/sidebar.d.ts +42 -0
- package/dist/types/react/theme.d.ts +42 -4
- package/dist/types/react/ui.d.ts +42 -0
- package/docs/CUSTOMIZING.md +56 -0
- package/docs/STORE.md +19 -0
- package/docs/screenshots/plugin-page.png +0 -0
- package/docs/screenshots/settings-appearance.png +0 -0
- package/docs/screenshots/site-guide-dark.png +0 -0
- package/docs/screenshots/site-home.png +0 -0
- package/docs/screenshots/site-mobile.png +0 -0
- package/docs/screenshots/site-search.png +0 -0
- package/package.json +1 -1
- package/dist/browser/chunks/chunk-VXQJIDXW.js +0 -1232
package/dist/node/cli-api.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_TOPIC,
|
|
4
4
|
JsonStateFile,
|
|
5
5
|
LocalRegistry,
|
|
6
|
+
MainFramework,
|
|
6
7
|
PluginInstaller,
|
|
7
8
|
computeIntegrity,
|
|
8
9
|
copyDir,
|
|
@@ -14,10 +15,10 @@ import {
|
|
|
14
15
|
readTarGz,
|
|
15
16
|
signManifest,
|
|
16
17
|
verifyPlugin
|
|
17
|
-
} from "./chunks/chunk-
|
|
18
|
+
} from "./chunks/chunk-ZLA5XUXN.js";
|
|
18
19
|
import {
|
|
19
20
|
validateManifest
|
|
20
|
-
} from "./chunks/chunk-
|
|
21
|
+
} from "./chunks/chunk-SDCGUBEW.js";
|
|
21
22
|
import {
|
|
22
23
|
compileTailwind
|
|
23
24
|
} from "./chunks/chunk-3FH6NAJG.js";
|
|
@@ -25,11 +26,11 @@ import "./chunks/chunk-WSFCRVEQ.js";
|
|
|
25
26
|
|
|
26
27
|
// src/cli/cli.ts
|
|
27
28
|
import { parseArgs } from "node:util";
|
|
28
|
-
import { promises as
|
|
29
|
-
import
|
|
29
|
+
import { promises as fs8 } from "node:fs";
|
|
30
|
+
import path9 from "node:path";
|
|
30
31
|
|
|
31
32
|
// src/cli/build.ts
|
|
32
|
-
import { promises as fs } from "node:fs";
|
|
33
|
+
import { existsSync, promises as fs, watch as fsWatch } from "node:fs";
|
|
33
34
|
import path2 from "node:path";
|
|
34
35
|
import { build, context } from "esbuild";
|
|
35
36
|
|
|
@@ -73,7 +74,19 @@ async function buildOptions(root, dev = false) {
|
|
|
73
74
|
logLevel: "warning",
|
|
74
75
|
plugins: [sharedModulesPlugin()],
|
|
75
76
|
jsx: "automatic",
|
|
76
|
-
loader: {
|
|
77
|
+
loader: {
|
|
78
|
+
".module.css": "local-css",
|
|
79
|
+
// Images and fonts imported by the plugin (or its libraries) are inlined, so dist/ stays self-contained.
|
|
80
|
+
".png": "dataurl",
|
|
81
|
+
".jpg": "dataurl",
|
|
82
|
+
".jpeg": "dataurl",
|
|
83
|
+
".gif": "dataurl",
|
|
84
|
+
".webp": "dataurl",
|
|
85
|
+
".svg": "dataurl",
|
|
86
|
+
".woff": "dataurl",
|
|
87
|
+
".woff2": "dataurl",
|
|
88
|
+
".ttf": "dataurl"
|
|
89
|
+
}
|
|
77
90
|
};
|
|
78
91
|
if (m.main) {
|
|
79
92
|
const entry = await firstExisting(root, ["src/main.ts", "src/main.js"]);
|
|
@@ -99,18 +112,69 @@ async function buildPlugin(root, watch = false, log2 = console.log) {
|
|
|
99
112
|
};
|
|
100
113
|
if (!watch) {
|
|
101
114
|
for (const o of opts) await build(o);
|
|
115
|
+
await ensureRendererCss(root);
|
|
116
|
+
await hintMissingDeps(root, log2);
|
|
102
117
|
log2(`built ${opts.length} bundle(s) in ${root}`);
|
|
103
118
|
return async () => stopTw();
|
|
104
119
|
}
|
|
105
120
|
const ctxs = await Promise.all(opts.map((o) => context(o)));
|
|
121
|
+
await Promise.all(ctxs.map((c) => c.rebuild()));
|
|
122
|
+
await ensureRendererCss(root);
|
|
106
123
|
await Promise.all(ctxs.map((c) => c.watch()));
|
|
107
124
|
log2(`watching ${root} (${ctxs.length} bundles)`);
|
|
108
125
|
return async () => (stopTw(), void await Promise.all(ctxs.map((c) => c.dispose())));
|
|
109
126
|
}
|
|
127
|
+
async function ensureRendererCss(root) {
|
|
128
|
+
if (!await exists(path2.join(root, "dist/renderer.css"))) return;
|
|
129
|
+
const file = path2.join(root, "plugin.json");
|
|
130
|
+
const raw = JSON.parse(await fs.readFile(file, "utf8"));
|
|
131
|
+
const styles2 = raw.styles ?? [];
|
|
132
|
+
if (styles2.some((s) => s.replace(/^\.\//, "") === "dist/renderer.css")) return;
|
|
133
|
+
raw.styles = [...styles2, "./dist/renderer.css"];
|
|
134
|
+
await fs.writeFile(file, JSON.stringify(raw, null, 2));
|
|
135
|
+
}
|
|
136
|
+
async function hintMissingDeps(root, log2) {
|
|
137
|
+
try {
|
|
138
|
+
const pkg2 = JSON.parse(await fs.readFile(path2.join(root, "package.json"), "utf8"));
|
|
139
|
+
if (Object.keys(pkg2.dependencies ?? {}).length && !await exists(path2.join(root, "node_modules"))) log2(`! ${path2.basename(root)} lists dependencies but has no node_modules: run "npm install" in ${root}`);
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function linkPluginToHost(root, hostDir, log2 = console.log) {
|
|
144
|
+
const manifest = JSON.parse(await fs.readFile(path2.join(root, "plugin.json"), "utf8"));
|
|
145
|
+
const target = path2.join(path2.resolve(hostDir), "plugins", manifest.id);
|
|
146
|
+
const sync = async () => {
|
|
147
|
+
try {
|
|
148
|
+
const copied = await copyPluginRuntime(root, target);
|
|
149
|
+
const keep = new Set(copied);
|
|
150
|
+
const walk = async (rel) => {
|
|
151
|
+
for (const e of await fs.readdir(path2.join(target, rel), { withFileTypes: true })) {
|
|
152
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
153
|
+
if (e.isDirectory()) await walk(r);
|
|
154
|
+
else if (!keep.has(r)) await fs.rm(path2.join(target, r), { force: true });
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
await walk("");
|
|
158
|
+
log2(`\u21AA synced ${copied.length} files to ${path2.relative(process.cwd(), target) || target}`);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
log2(`! sync failed: ${e.message}`);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
await sync();
|
|
164
|
+
let timer;
|
|
165
|
+
const watchers = [path2.join(root, "dist"), path2.join(root, "assets"), root].filter((d) => existsSync(d)).map(
|
|
166
|
+
(d) => fsWatch(d, { recursive: d !== root }, (_e, name) => {
|
|
167
|
+
if (d === root && name !== "plugin.json" && name !== "README.md") return;
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
timer = setTimeout(() => void sync(), 250);
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
return () => watchers.forEach((w) => w.close());
|
|
173
|
+
}
|
|
110
174
|
|
|
111
175
|
// src/cli/host.ts
|
|
112
176
|
import { spawn, spawnSync } from "node:child_process";
|
|
113
|
-
import { existsSync, promises as fs3, statSync } from "node:fs";
|
|
177
|
+
import { existsSync as existsSync2, promises as fs3, statSync } from "node:fs";
|
|
114
178
|
import { createRequire } from "node:module";
|
|
115
179
|
import path4 from "node:path";
|
|
116
180
|
import { build as build2, context as context2 } from "esbuild";
|
|
@@ -139,7 +203,7 @@ async function isHostProject(cwd) {
|
|
|
139
203
|
}
|
|
140
204
|
var nodeBanner = { js: "import { createRequire as __r } from 'node:module'; const require = __r(import.meta.url);" };
|
|
141
205
|
var pick = (cwd, ...rels) => {
|
|
142
|
-
for (const r of rels) if (
|
|
206
|
+
for (const r of rels) if (existsSync2(path4.join(cwd, r))) return path4.join(cwd, r);
|
|
143
207
|
return path4.join(cwd, rels[0]);
|
|
144
208
|
};
|
|
145
209
|
function hostBuilds(cwd, outDir, dev) {
|
|
@@ -195,7 +259,7 @@ async function buildHost(cwd, opts = {}) {
|
|
|
195
259
|
function electronBinary(cwd) {
|
|
196
260
|
try {
|
|
197
261
|
const p = createRequire(path4.join(cwd, "package.json"))("electron");
|
|
198
|
-
if (typeof p === "string" &&
|
|
262
|
+
if (typeof p === "string" && existsSync2(p)) return { bin: p };
|
|
199
263
|
return { error: "the Electron binary was not found" };
|
|
200
264
|
} catch (e) {
|
|
201
265
|
return { error: e.message.split("\n")[0] };
|
|
@@ -211,7 +275,7 @@ function ensureElectron(cwd, log2 = console.log) {
|
|
|
211
275
|
return { error: "Electron is not installed in this project" };
|
|
212
276
|
}
|
|
213
277
|
const installJs = path4.join(pkgDir, "install.js");
|
|
214
|
-
if (!
|
|
278
|
+
if (!existsSync2(installJs)) return first;
|
|
215
279
|
log2("Electron's binary is missing (your package manager blocked its install script) \u2192 downloading it now\u2026");
|
|
216
280
|
const r = spawnSync(process.execPath, [installJs], { cwd: pkgDir, stdio: "inherit" });
|
|
217
281
|
if (r.status !== 0) return { error: "the Electron download failed (network or proxy?). Retry, or set ELECTRON_MIRROR" };
|
|
@@ -250,10 +314,398 @@ function startApp(cwd, opts = {}) {
|
|
|
250
314
|
|
|
251
315
|
// src/cli/host-files.generated.ts
|
|
252
316
|
var HOST_APP_FILES = {
|
|
253
|
-
"
|
|
254
|
-
"
|
|
317
|
+
"electron-builder.yml": '# Produces the installer/executable: `npm run dist` (current OS) or `npm run dist -- --win --mac --linux`.\nappId: com.example.__APP_ID__\nproductName: __APP_NAME__\ndirectories:\n output: release\nfiles:\n - dist/**/*\n - package.json\n - "!**/*.map"\n# Plugins are copied by `fluxplugin dist` into .flux-build/plugins with runtime files only (no src, no .ts, no sourcemaps).\nextraResources:\n - from: .flux-build/plugins\n to: plugins\nasar: true\nwin:\n target: [nsis]\nnsis:\n oneClick: false\n allowToChangeInstallationDirectory: true\n # Plugins the user installed live in the app\'s data folder; this removes them with the app when it is uninstalled.\n deleteAppDataOnUninstall: true\nmac:\n target: [dmg, zip]\n category: public.app-category.productivity\nlinux:\n target: [AppImage, deb]\n category: Utility\n',
|
|
318
|
+
"plugins/appearance/README.md": "# Appearance\n\nMake the app yours. This plugin adds an **Appearance** page to *Settings* and four extra themes.\n\n## What you can change\n\n- **Mode** \u2014 System, Light or Dark.\n- **Theme** \u2014 Midnight, Sunset, Forest or high-contrast Graphite.\n- **Accent color** \u2014 seven colors, or the theme's own.\n- **Text** \u2014 font family, any font installed on your computer, size and line spacing.\n- **Layout** \u2014 compact or comfortable spacing, corner roundness, translucency and reduced motion.\n\n> [!TIP]\n> Type the name of any font installed on your computer in *Your own font*. If it is missing, the next font in the list is used.\n\n## How it works\n\n| Setting | Stored as |\n| --- | --- |\n| Theme and mode | the app's theme variables |\n| Fonts, size, spacing | CSS variables on the page |\n\nYour choices are saved on this device and apply to every plugin, because plugins use the same variables.\n\n## For developers\n\nPlugins can add their own **themes** and **fonts** to the same lists \u2014 see the `theme` and `font` extension points in the documentation.\n",
|
|
319
|
+
"plugins/appearance/assets/icon.svg": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><rect width="96" height="96" rx="22" fill="hsl(280 72% 50%)"/><text x="48" y="64" font-family="system-ui,sans-serif" font-size="52" font-weight="700" text-anchor="middle" fill="#fff">A</text></svg>\n',
|
|
320
|
+
"plugins/appearance/plugin.json": '{\n "id": "com.fluxplugin.appearance",\n "name": "Appearance",\n "version": "1.0.0",\n "description": "Themes, accent colors, fonts, text size, density and more \\u2014 everything about how the app looks.",\n "author": "fluxplugin",\n "license": "MIT",\n "icon": "./assets/icon.svg",\n "readme": "./README.md",\n "categories": [\n "Personalization"\n ],\n "keywords": [\n "theme",\n "font",\n "accent",\n "dark mode"\n ],\n "links": {},\n "renderer": "./dist/renderer.js",\n "activationEvents": [\n "onStartup"\n ],\n "permissions": [\n "ui"\n ],\n "engines": {\n "fluxplugin": "^1.0.0"\n }\n}',
|
|
321
|
+
"plugins/appearance/src/renderer.tsx": `import { definePlugin } from "fluxplugin";
|
|
322
|
+
import { Button, Group, Row, Segmented, Switch, useAppearance, useT, useTheme, type ThemeMode } from "fluxplugin/react";
|
|
323
|
+
import { useState } from "react";
|
|
324
|
+
|
|
325
|
+
const ACCENTS = ["#007aff", "#34c759", "#ff9f0a", "#af52de", "#ff375f", "#5ac8fa", "#8e8e93"];
|
|
326
|
+
|
|
327
|
+
function AppearanceSection() {
|
|
328
|
+
const t = useT();
|
|
329
|
+
const { mode, setMode, themeId, setThemeId, themes } = useTheme();
|
|
330
|
+
const { appearance: a, setAppearance, resetAppearance, fonts } = useAppearance();
|
|
331
|
+
const [custom, setCustom] = useState(a.customFont);
|
|
332
|
+
return (
|
|
333
|
+
<>
|
|
334
|
+
<Group title={t("appearance.mode", "Mode")}>
|
|
335
|
+
<Row title={t("appearance.mode", "Mode")} right={
|
|
336
|
+
<Segmented<ThemeMode> value={(themeId ? "" : mode) as ThemeMode} onChange={setMode} label="Mode"
|
|
337
|
+
options={[{ value: "system", label: t("theme.system", "System") }, { value: "light", label: t("theme.light", "Light") }, { value: "dark", label: t("theme.dark", "Dark") }]} />} />
|
|
338
|
+
</Group>
|
|
339
|
+
|
|
340
|
+
<Group title={t("appearance.theme", "Theme")}>
|
|
341
|
+
<div style={{ display: "flex", gap: 12, flexWrap: "wrap", padding: 14 }}>
|
|
342
|
+
{[{ id: "", label: t("appearance.themeDefault", "Default"), tokens: { "--flux-bg": "#ffffff", "--flux-surface": "#f5f5f7", "--flux-accent": "#007aff" } }, ...themes].map((th) => (
|
|
343
|
+
<button key={th.id || "default"} type="button" className="flux-theme-card" data-on={(themeId ?? "") === th.id || undefined} data-testid={"theme-" + (th.id || "default")} onClick={() => (th.id ? setThemeId(th.id) : setMode(mode))}>
|
|
344
|
+
<span className="flux-theme-prev" style={{ display: "flex" }}>
|
|
345
|
+
<i style={{ flex: 2, background: th.tokens["--flux-bg"] }} /><i style={{ flex: 1, background: th.tokens["--flux-surface"] }} /><i style={{ flex: 1, background: th.tokens["--flux-accent"] }} />
|
|
346
|
+
</span>
|
|
347
|
+
<span className="cap">{th.label}</span>
|
|
348
|
+
</button>
|
|
349
|
+
))}
|
|
350
|
+
</div>
|
|
351
|
+
</Group>
|
|
352
|
+
|
|
353
|
+
<Group title={t("appearance.accent", "Accent color")}>
|
|
354
|
+
<div className="flux-swatches" style={{ padding: 14 }}>
|
|
355
|
+
<button type="button" className="flux-swatch" title={t("appearance.accentDefault", "Theme default")} style={{ background: "conic-gradient(#ff3b30,#ff9f0a,#34c759,#0a84ff,#af52de,#ff3b30)" }} data-on={!a.accent || undefined} onClick={() => setAppearance({ accent: undefined })} />
|
|
356
|
+
{ACCENTS.map((c) => <button key={c} type="button" aria-label={c} className="flux-swatch" style={{ background: c }} data-on={a.accent === c || undefined} data-testid={"accent-" + c.slice(1)} onClick={() => setAppearance({ accent: c })} />)}
|
|
357
|
+
</div>
|
|
358
|
+
</Group>
|
|
359
|
+
|
|
360
|
+
<Group title={t("appearance.text", "Text")}>
|
|
361
|
+
<Row title={t("appearance.font", "Font")} right={
|
|
362
|
+
<select className="flux-select" value={a.fontId} data-testid="font-select" onChange={(e) => setAppearance({ fontId: e.target.value })}>{fonts.map((f) => <option key={f.id} value={f.id}>{f.label}</option>)}</select>} />
|
|
363
|
+
<Row title={t("appearance.customFont", "Your own font")} subtitle={t("appearance.customFontHint", "Any font installed on your computer")} right={
|
|
364
|
+
<input className="flux-input" style={{ width: 170 }} placeholder="e.g. Avenir Next" value={custom} data-testid="custom-font" onChange={(e) => setCustom(e.target.value)} onBlur={() => setAppearance({ customFont: custom })} onKeyDown={(e) => e.key === "Enter" && setAppearance({ customFont: custom })} />} />
|
|
365
|
+
<Row title={t("appearance.fontSize", "Text size")} right={<><input className="flux-range" type="range" min={12} max={20} step={1} value={a.fontSize} data-testid="font-size" onChange={(e) => setAppearance({ fontSize: Number(e.target.value) })} /><span style={{ width: 34, textAlign: "right" }}>{a.fontSize}px</span></>} />
|
|
366
|
+
<Row title={t("appearance.lineHeight", "Line spacing")} right={<input className="flux-range" type="range" min={1.2} max={2} step={0.05} value={a.lineHeight} onChange={(e) => setAppearance({ lineHeight: Number(e.target.value) })} />} />
|
|
367
|
+
<div style={{ padding: "4px 14px 14px" }}><p style={{ margin: 0 }}>{t("appearance.preview", "The quick brown fox jumps over the lazy dog.")}</p></div>
|
|
368
|
+
</Group>
|
|
369
|
+
|
|
370
|
+
<Group title={t("appearance.layout", "Layout")}>
|
|
371
|
+
<Row title={t("appearance.density", "Spacing")} right={
|
|
372
|
+
<Segmented value={a.density} onChange={(density) => setAppearance({ density })} label="Spacing" options={[{ value: "comfortable", label: t("appearance.comfortable", "Comfortable") }, { value: "compact", label: t("appearance.compact", "Compact") }]} />} />
|
|
373
|
+
<Row title={t("appearance.radius", "Corners")} right={
|
|
374
|
+
<Segmented value={a.radius} onChange={(radius) => setAppearance({ radius })} label="Corners" options={[{ value: "sharp", label: t("appearance.sharp", "Sharp") }, { value: "default", label: t("appearance.default", "Default") }, { value: "round", label: t("appearance.round", "Round") }]} />} />
|
|
375
|
+
<Row title={t("appearance.translucency", "Translucency")} right={<Switch checked={a.translucency} onChange={(translucency) => setAppearance({ translucency })} label="Translucency" />} />
|
|
376
|
+
<Row title={t("appearance.motion", "Reduce motion")} right={<Switch checked={a.reduceMotion} onChange={(reduceMotion) => setAppearance({ reduceMotion })} label="Reduce motion" />} />
|
|
377
|
+
</Group>
|
|
378
|
+
<Button variant="destructive" onClick={() => (resetAppearance(), setCustom(""))}>{t("appearance.reset", "Reset appearance")}</Button>
|
|
379
|
+
</>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export default definePlugin({
|
|
384
|
+
extensions: [
|
|
385
|
+
{ point: "settings", contribution: { sections: [{ id: "appearance", title: "Appearance", icon: "palette", order: 10, component: AppearanceSection }] } },
|
|
386
|
+
{ point: "theme", contribution: { themes: [
|
|
387
|
+
{ id: "midnight", label: "Midnight", dark: true, tokens: { "--flux-bg": "#0b1020", "--flux-surface": "#111936", "--flux-surface-2": "#1a2447", "--flux-fg": "#e6ebff", "--flux-muted": "#8f9bc7", "--flux-border": "rgba(150,170,255,.18)", "--flux-accent": "#7c9cff", "--flux-material": "rgba(17,25,54,.72)" } },
|
|
388
|
+
{ id: "sunset", label: "Sunset", tokens: { "--flux-bg": "#fff8f1", "--flux-surface": "#ffefe0", "--flux-surface-2": "#ffe0c7", "--flux-fg": "#3b1f12", "--flux-muted": "#9a6b53", "--flux-border": "rgba(120,60,20,.16)", "--flux-accent": "#ff6a3d", "--flux-material": "rgba(255,239,224,.8)" } },
|
|
389
|
+
{ id: "forest", label: "Forest", dark: true, tokens: { "--flux-bg": "#0f1a14", "--flux-surface": "#162720", "--flux-surface-2": "#1f3a2e", "--flux-fg": "#e4f3ea", "--flux-muted": "#8fb3a0", "--flux-border": "rgba(150,230,190,.16)", "--flux-accent": "#34d399", "--flux-material": "rgba(22,39,32,.74)" } },
|
|
390
|
+
{ id: "graphite", label: "Graphite (high contrast)", tokens: { "--flux-bg": "#ffffff", "--flux-surface": "#f0f0f0", "--flux-surface-2": "#dcdcdc", "--flux-fg": "#000000", "--flux-muted": "#3a3a3a", "--flux-border": "#000000", "--flux-accent": "#0040dd", "--flux-material": "rgba(240,240,240,.95)" } },
|
|
391
|
+
] } },
|
|
392
|
+
{ point: "font", contribution: { fonts: [
|
|
393
|
+
{ id: "editorial", label: "Editorial", stack: '"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif' },
|
|
394
|
+
{ id: "geometric", label: "Geometric", stack: '"Avenir Next", Avenir, "Century Gothic", Futura, "Trebuchet MS", sans-serif' },
|
|
395
|
+
{ id: "readable", label: "Easy reading", stack: '"Atkinson Hyperlegible", Verdana, Tahoma, sans-serif' },
|
|
396
|
+
] } },
|
|
397
|
+
],
|
|
398
|
+
});
|
|
399
|
+
`,
|
|
400
|
+
"plugins/language/README.md": "# Language\n\nChoose the language of the whole interface: menus, search, notifications, settings and the plugin manager.\n\n## Languages included\n\n| Language | Code |\n| --- | --- |\n| English | `en` |\n| Fran\xE7ais | `fr` |\n| Espa\xF1ol | `es` |\n| Deutsch | `de` |\n\nOpen **Settings \u2192 Language** and pick one. The change is instant and remembered.\n\n## Add your own language\n\n1. Copy `src/messages.ts` and translate the values.\n2. Add the language to the list in `src/renderer.tsx`.\n3. Build with `fluxplugin build`.\n\n> [!NOTE]\n> Any plugin can ship translations \u2014 the app merges them. Missing words fall back to English, so a partial translation is fine.\n\n## Right-to-left languages\n\nSet `rtl: true` on a language (Arabic, Hebrew\u2026) and the whole interface mirrors automatically.\n",
|
|
401
|
+
"plugins/language/assets/icon.svg": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><rect width="96" height="96" rx="22" fill="hsl(205 72% 50%)"/><text x="48" y="64" font-family="system-ui,sans-serif" font-size="52" font-weight="700" text-anchor="middle" fill="#fff">L</text></svg>\n',
|
|
402
|
+
"plugins/language/plugin.json": '{\n "id": "com.fluxplugin.language",\n "name": "Language",\n "version": "1.0.0",\n "description": "Use the app in English, French, Spanish or German \\u2014 and add your own language.",\n "author": "fluxplugin",\n "license": "MIT",\n "icon": "./assets/icon.svg",\n "readme": "./README.md",\n "categories": [\n "Personalization"\n ],\n "keywords": [\n "language",\n "translation",\n "i18n",\n "fran\\u00e7ais",\n "espa\\u00f1ol",\n "deutsch"\n ],\n "links": {},\n "renderer": "./dist/renderer.js",\n "activationEvents": [\n "onStartup"\n ],\n "permissions": [\n "ui"\n ],\n "engines": {\n "fluxplugin": "^1.0.0"\n }\n}',
|
|
403
|
+
"plugins/language/src/messages.ts": `/** Translations. Keys are the ones the app and its plugins pass to useT(); English is the built-in fallback. */
|
|
404
|
+
export const messages: Record<string, Record<string, string>> = {
|
|
405
|
+
"fr": {
|
|
406
|
+
"menu.dashboard": "Tableau de bord",
|
|
407
|
+
"menu.plugins": "Extensions",
|
|
408
|
+
"menu.settings": "R\xE9glages",
|
|
409
|
+
"theme.toggle": "Changer d'apparence",
|
|
410
|
+
"theme.system": "Syst\xE8me",
|
|
411
|
+
"theme.light": "Clair",
|
|
412
|
+
"theme.dark": "Sombre",
|
|
413
|
+
"search.placeholder": "Rechercher commandes, pages\u2026",
|
|
414
|
+
"search.label": "Rechercher",
|
|
415
|
+
"search.commands": "Commandes",
|
|
416
|
+
"search.pages": "Pages",
|
|
417
|
+
"search.noResults": "Aucun r\xE9sultat",
|
|
418
|
+
"notifications.title": "Notifications",
|
|
419
|
+
"notifications.clear": "Tout effacer",
|
|
420
|
+
"notifications.empty": "Tout est \xE0 jour",
|
|
421
|
+
"palette.placeholder": "Saisir une commande\u2026",
|
|
422
|
+
"palette.empty": "Aucune commande correspondante",
|
|
423
|
+
"sidebar.expand": "D\xE9velopper la barre lat\xE9rale",
|
|
424
|
+
"sidebar.collapse": "R\xE9duire la barre lat\xE9rale",
|
|
425
|
+
"settings.title": "R\xE9glages",
|
|
426
|
+
"settings.empty": "Aucun r\xE9glage",
|
|
427
|
+
"settings.general": "G\xE9n\xE9ral",
|
|
428
|
+
"settings.shortcuts": "Raccourcis",
|
|
429
|
+
"settings.appearance": "Apparence",
|
|
430
|
+
"settings.language": "Langue",
|
|
431
|
+
"settings.about": "\xC0 propos",
|
|
432
|
+
"settings.app": "Application",
|
|
433
|
+
"settings.plugins": "Extensions",
|
|
434
|
+
"settings.engine": "Moteur",
|
|
435
|
+
"settings.sidebar": "Barre lat\xE9rale",
|
|
436
|
+
"settings.sidebarHint": "R\xE9duit la barre lat\xE9rale en ic\xF4nes. Raccourci : Ctrl/Cmd+B.",
|
|
437
|
+
"settings.compactSidebar": "Barre lat\xE9rale compacte",
|
|
438
|
+
"plugins.title": "Extensions",
|
|
439
|
+
"plugins.installed": "Install\xE9es",
|
|
440
|
+
"plugins.discover": "D\xE9couvrir",
|
|
441
|
+
"plugins.updates": "Mises \xE0 jour",
|
|
442
|
+
"plugins.search": "Rechercher des extensions",
|
|
443
|
+
"plugins.none": "Aucune extension install\xE9e",
|
|
444
|
+
"plugins.searching": "Recherche dans les catalogues\u2026",
|
|
445
|
+
"plugins.upToDate": "Tout est \xE0 jour.",
|
|
446
|
+
"plugins.storeEmpty": "Aucune extension trouv\xE9e dans vos catalogues.",
|
|
447
|
+
"plugins.installedTag": "Install\xE9e",
|
|
448
|
+
"plugins.get": "Obtenir",
|
|
449
|
+
"plugins.update": "Mettre \xE0 jour",
|
|
450
|
+
"plugins.open": "Ouvrir",
|
|
451
|
+
"plugins.back": "Extensions",
|
|
452
|
+
"plugins.notFound": "Cette extension n'est pas install\xE9e.",
|
|
453
|
+
"plugins.enable": "Activer",
|
|
454
|
+
"plugins.enabled": "Activ\xE9e",
|
|
455
|
+
"plugins.disabled": "D\xE9sactiv\xE9e",
|
|
456
|
+
"plugins.reload": "Recharger",
|
|
457
|
+
"plugins.uninstall": "D\xE9sinstaller",
|
|
458
|
+
"plugins.video": "Vid\xE9o",
|
|
459
|
+
"plugins.about": "\xC0 propos",
|
|
460
|
+
"plugins.permissions": "Autorisations",
|
|
461
|
+
"plugins.permissionsFooter": "Une extension n'obtient que ce qu'elle d\xE9clare. Les acc\xE8s sensibles sont signal\xE9s.",
|
|
462
|
+
"plugins.noPermissions": "Aucun acc\xE8s particulier",
|
|
463
|
+
"plugins.sensitive": "Sensible",
|
|
464
|
+
"plugins.information": "Informations",
|
|
465
|
+
"plugins.developer": "D\xE9veloppeur",
|
|
466
|
+
"plugins.version": "Version",
|
|
467
|
+
"plugins.license": "Licence",
|
|
468
|
+
"plugins.dependencies": "N\xE9cessite",
|
|
469
|
+
"plugins.keywords": "Mots-cl\xE9s",
|
|
470
|
+
"plugins.settings": "R\xE9glages",
|
|
471
|
+
"plugins.saved": "Enregistr\xE9",
|
|
472
|
+
"plugins.reset": "R\xE9tablir les valeurs par d\xE9faut",
|
|
473
|
+
"plugins.logs": "Journal",
|
|
474
|
+
"plugins.noLogs": "Aucun journal",
|
|
475
|
+
"plugins.installedVersion": "Install\xE9e : {v}",
|
|
476
|
+
"state.ready": "Active",
|
|
477
|
+
"state.error": "Erreur",
|
|
478
|
+
"state.disabled": "D\xE9sactiv\xE9e",
|
|
479
|
+
"appearance.mode": "Mode",
|
|
480
|
+
"appearance.theme": "Th\xE8me",
|
|
481
|
+
"appearance.themeDefault": "Par d\xE9faut",
|
|
482
|
+
"appearance.accent": "Couleur d'accentuation",
|
|
483
|
+
"appearance.accentDefault": "Selon le th\xE8me",
|
|
484
|
+
"appearance.text": "Texte",
|
|
485
|
+
"appearance.font": "Police",
|
|
486
|
+
"appearance.customFont": "Votre propre police",
|
|
487
|
+
"appearance.customFontHint": "Toute police install\xE9e sur votre ordinateur",
|
|
488
|
+
"appearance.fontSize": "Taille du texte",
|
|
489
|
+
"appearance.lineHeight": "Interligne",
|
|
490
|
+
"appearance.preview": "Portez ce vieux whisky au juge blond qui fume.",
|
|
491
|
+
"appearance.layout": "Disposition",
|
|
492
|
+
"appearance.density": "Espacement",
|
|
493
|
+
"appearance.comfortable": "Confortable",
|
|
494
|
+
"appearance.compact": "Compact",
|
|
495
|
+
"appearance.radius": "Coins",
|
|
496
|
+
"appearance.sharp": "Droits",
|
|
497
|
+
"appearance.default": "Par d\xE9faut",
|
|
498
|
+
"appearance.round": "Arrondis",
|
|
499
|
+
"appearance.translucency": "Transparence",
|
|
500
|
+
"appearance.motion": "R\xE9duire les animations",
|
|
501
|
+
"appearance.reset": "R\xE9initialiser l'apparence",
|
|
502
|
+
"language.follow": "Suivre la langue du syst\xE8me"
|
|
503
|
+
},
|
|
504
|
+
"es": {
|
|
505
|
+
"menu.dashboard": "Panel",
|
|
506
|
+
"menu.plugins": "Extensiones",
|
|
507
|
+
"menu.settings": "Ajustes",
|
|
508
|
+
"theme.toggle": "Cambiar apariencia",
|
|
509
|
+
"theme.system": "Sistema",
|
|
510
|
+
"theme.light": "Claro",
|
|
511
|
+
"theme.dark": "Oscuro",
|
|
512
|
+
"search.placeholder": "Buscar comandos, p\xE1ginas\u2026",
|
|
513
|
+
"search.label": "Buscar",
|
|
514
|
+
"search.commands": "Comandos",
|
|
515
|
+
"search.pages": "P\xE1ginas",
|
|
516
|
+
"search.noResults": "Sin resultados",
|
|
517
|
+
"notifications.title": "Notificaciones",
|
|
518
|
+
"notifications.clear": "Borrar todo",
|
|
519
|
+
"notifications.empty": "Todo al d\xEDa",
|
|
520
|
+
"palette.placeholder": "Escribe un comando\u2026",
|
|
521
|
+
"palette.empty": "Ning\xFAn comando coincide",
|
|
522
|
+
"sidebar.expand": "Expandir barra lateral",
|
|
523
|
+
"sidebar.collapse": "Contraer barra lateral",
|
|
524
|
+
"settings.title": "Ajustes",
|
|
525
|
+
"settings.empty": "Sin ajustes",
|
|
526
|
+
"settings.general": "General",
|
|
527
|
+
"settings.shortcuts": "Atajos",
|
|
528
|
+
"settings.appearance": "Apariencia",
|
|
529
|
+
"settings.language": "Idioma",
|
|
530
|
+
"settings.about": "Acerca de",
|
|
531
|
+
"settings.app": "Aplicaci\xF3n",
|
|
532
|
+
"settings.plugins": "Extensiones",
|
|
533
|
+
"settings.engine": "Motor",
|
|
534
|
+
"settings.sidebar": "Barra lateral",
|
|
535
|
+
"settings.sidebarHint": "Reduce la barra lateral a iconos. Atajo: Ctrl/Cmd+B.",
|
|
536
|
+
"settings.compactSidebar": "Barra lateral compacta",
|
|
537
|
+
"plugins.title": "Extensiones",
|
|
538
|
+
"plugins.installed": "Instaladas",
|
|
539
|
+
"plugins.discover": "Descubrir",
|
|
540
|
+
"plugins.updates": "Actualizaciones",
|
|
541
|
+
"plugins.search": "Buscar extensiones",
|
|
542
|
+
"plugins.none": "No hay extensiones instaladas",
|
|
543
|
+
"plugins.searching": "Buscando en los cat\xE1logos\u2026",
|
|
544
|
+
"plugins.upToDate": "Todo est\xE1 actualizado.",
|
|
545
|
+
"plugins.storeEmpty": "No se encontraron extensiones en tus cat\xE1logos.",
|
|
546
|
+
"plugins.installedTag": "Instalada",
|
|
547
|
+
"plugins.get": "Obtener",
|
|
548
|
+
"plugins.update": "Actualizar",
|
|
549
|
+
"plugins.open": "Abrir",
|
|
550
|
+
"plugins.back": "Extensiones",
|
|
551
|
+
"plugins.notFound": "Esta extensi\xF3n no est\xE1 instalada.",
|
|
552
|
+
"plugins.enable": "Activar",
|
|
553
|
+
"plugins.enabled": "Activada",
|
|
554
|
+
"plugins.disabled": "Desactivada",
|
|
555
|
+
"plugins.reload": "Recargar",
|
|
556
|
+
"plugins.uninstall": "Desinstalar",
|
|
557
|
+
"plugins.video": "V\xEDdeo",
|
|
558
|
+
"plugins.about": "Descripci\xF3n",
|
|
559
|
+
"plugins.permissions": "Permisos",
|
|
560
|
+
"plugins.permissionsFooter": "Las extensiones solo obtienen lo que declaran. Los accesos sensibles se destacan.",
|
|
561
|
+
"plugins.noPermissions": "Sin accesos especiales",
|
|
562
|
+
"plugins.sensitive": "Sensible",
|
|
563
|
+
"plugins.information": "Informaci\xF3n",
|
|
564
|
+
"plugins.developer": "Desarrollador",
|
|
565
|
+
"plugins.version": "Versi\xF3n",
|
|
566
|
+
"plugins.license": "Licencia",
|
|
567
|
+
"plugins.dependencies": "Requiere",
|
|
568
|
+
"plugins.keywords": "Palabras clave",
|
|
569
|
+
"plugins.settings": "Ajustes",
|
|
570
|
+
"plugins.saved": "Guardado",
|
|
571
|
+
"plugins.reset": "Restablecer valores",
|
|
572
|
+
"plugins.logs": "Registro",
|
|
573
|
+
"plugins.noLogs": "Sin registros",
|
|
574
|
+
"plugins.installedVersion": "Instalada: {v}",
|
|
575
|
+
"state.ready": "Activa",
|
|
576
|
+
"state.error": "Error",
|
|
577
|
+
"state.disabled": "Desactivada",
|
|
578
|
+
"appearance.mode": "Modo",
|
|
579
|
+
"appearance.theme": "Tema",
|
|
580
|
+
"appearance.themeDefault": "Predeterminado",
|
|
581
|
+
"appearance.accent": "Color de \xE9nfasis",
|
|
582
|
+
"appearance.accentDefault": "Seg\xFAn el tema",
|
|
583
|
+
"appearance.text": "Texto",
|
|
584
|
+
"appearance.font": "Fuente",
|
|
585
|
+
"appearance.customFont": "Tu propia fuente",
|
|
586
|
+
"appearance.customFontHint": "Cualquier fuente instalada en tu equipo",
|
|
587
|
+
"appearance.fontSize": "Tama\xF1o del texto",
|
|
588
|
+
"appearance.lineHeight": "Interlineado",
|
|
589
|
+
"appearance.preview": "El veloz murci\xE9lago hind\xFA com\xEDa feliz cardillo y kiwi.",
|
|
590
|
+
"appearance.layout": "Disposici\xF3n",
|
|
591
|
+
"appearance.density": "Espaciado",
|
|
592
|
+
"appearance.comfortable": "C\xF3modo",
|
|
593
|
+
"appearance.compact": "Compacto",
|
|
594
|
+
"appearance.radius": "Esquinas",
|
|
595
|
+
"appearance.sharp": "Rectas",
|
|
596
|
+
"appearance.default": "Predeterminadas",
|
|
597
|
+
"appearance.round": "Redondeadas",
|
|
598
|
+
"appearance.translucency": "Transparencia",
|
|
599
|
+
"appearance.motion": "Reducir movimiento",
|
|
600
|
+
"appearance.reset": "Restablecer apariencia",
|
|
601
|
+
"language.follow": "Seguir el idioma del sistema"
|
|
602
|
+
},
|
|
603
|
+
"de": {
|
|
604
|
+
"menu.dashboard": "\xDCbersicht",
|
|
605
|
+
"menu.plugins": "Erweiterungen",
|
|
606
|
+
"menu.settings": "Einstellungen",
|
|
607
|
+
"theme.toggle": "Darstellung wechseln",
|
|
608
|
+
"theme.system": "System",
|
|
609
|
+
"theme.light": "Hell",
|
|
610
|
+
"theme.dark": "Dunkel",
|
|
611
|
+
"search.placeholder": "Befehle, Seiten suchen\u2026",
|
|
612
|
+
"search.label": "Suchen",
|
|
613
|
+
"search.commands": "Befehle",
|
|
614
|
+
"search.pages": "Seiten",
|
|
615
|
+
"search.noResults": "Keine Ergebnisse",
|
|
616
|
+
"notifications.title": "Mitteilungen",
|
|
617
|
+
"notifications.clear": "Alle l\xF6schen",
|
|
618
|
+
"notifications.empty": "Alles erledigt",
|
|
619
|
+
"palette.placeholder": "Befehl eingeben\u2026",
|
|
620
|
+
"palette.empty": "Kein passender Befehl",
|
|
621
|
+
"sidebar.expand": "Seitenleiste erweitern",
|
|
622
|
+
"sidebar.collapse": "Seitenleiste einklappen",
|
|
623
|
+
"settings.title": "Einstellungen",
|
|
624
|
+
"settings.empty": "Keine Einstellungen",
|
|
625
|
+
"settings.general": "Allgemein",
|
|
626
|
+
"settings.shortcuts": "Tastenk\xFCrzel",
|
|
627
|
+
"settings.appearance": "Darstellung",
|
|
628
|
+
"settings.language": "Sprache",
|
|
629
|
+
"settings.about": "Info",
|
|
630
|
+
"settings.app": "Anwendung",
|
|
631
|
+
"settings.plugins": "Erweiterungen",
|
|
632
|
+
"settings.engine": "Engine",
|
|
633
|
+
"settings.sidebar": "Seitenleiste",
|
|
634
|
+
"settings.sidebarHint": "Reduziert die Seitenleiste auf Symbole. K\xFCrzel: Strg/Cmd+B.",
|
|
635
|
+
"settings.compactSidebar": "Kompakte Seitenleiste",
|
|
636
|
+
"plugins.title": "Erweiterungen",
|
|
637
|
+
"plugins.installed": "Installiert",
|
|
638
|
+
"plugins.discover": "Entdecken",
|
|
639
|
+
"plugins.updates": "Updates",
|
|
640
|
+
"plugins.search": "Erweiterungen suchen",
|
|
641
|
+
"plugins.none": "Keine Erweiterungen installiert",
|
|
642
|
+
"plugins.searching": "Kataloge werden durchsucht\u2026",
|
|
643
|
+
"plugins.upToDate": "Alles ist aktuell.",
|
|
644
|
+
"plugins.storeEmpty": "Keine Erweiterungen in Ihren Katalogen gefunden.",
|
|
645
|
+
"plugins.installedTag": "Installiert",
|
|
646
|
+
"plugins.get": "Laden",
|
|
647
|
+
"plugins.update": "Aktualisieren",
|
|
648
|
+
"plugins.open": "\xD6ffnen",
|
|
649
|
+
"plugins.back": "Erweiterungen",
|
|
650
|
+
"plugins.notFound": "Diese Erweiterung ist nicht installiert.",
|
|
651
|
+
"plugins.enable": "Aktivieren",
|
|
652
|
+
"plugins.enabled": "Aktiviert",
|
|
653
|
+
"plugins.disabled": "Deaktiviert",
|
|
654
|
+
"plugins.reload": "Neu laden",
|
|
655
|
+
"plugins.uninstall": "Deinstallieren",
|
|
656
|
+
"plugins.video": "Video",
|
|
657
|
+
"plugins.about": "Beschreibung",
|
|
658
|
+
"plugins.permissions": "Berechtigungen",
|
|
659
|
+
"plugins.permissionsFooter": "Erweiterungen erhalten nur, was sie angeben. Sensible Zugriffe sind markiert.",
|
|
660
|
+
"plugins.noPermissions": "Keine besonderen Zugriffe",
|
|
661
|
+
"plugins.sensitive": "Sensibel",
|
|
662
|
+
"plugins.information": "Informationen",
|
|
663
|
+
"plugins.developer": "Entwickler",
|
|
664
|
+
"plugins.version": "Version",
|
|
665
|
+
"plugins.license": "Lizenz",
|
|
666
|
+
"plugins.dependencies": "Ben\xF6tigt",
|
|
667
|
+
"plugins.keywords": "Stichw\xF6rter",
|
|
668
|
+
"plugins.settings": "Einstellungen",
|
|
669
|
+
"plugins.saved": "Gespeichert",
|
|
670
|
+
"plugins.reset": "Auf Standard zur\xFCcksetzen",
|
|
671
|
+
"plugins.logs": "Protokoll",
|
|
672
|
+
"plugins.noLogs": "Kein Protokoll",
|
|
673
|
+
"plugins.installedVersion": "Installiert: {v}",
|
|
674
|
+
"state.ready": "Aktiv",
|
|
675
|
+
"state.error": "Fehler",
|
|
676
|
+
"state.disabled": "Deaktiviert",
|
|
677
|
+
"appearance.mode": "Modus",
|
|
678
|
+
"appearance.theme": "Thema",
|
|
679
|
+
"appearance.themeDefault": "Standard",
|
|
680
|
+
"appearance.accent": "Akzentfarbe",
|
|
681
|
+
"appearance.accentDefault": "Vom Thema",
|
|
682
|
+
"appearance.text": "Text",
|
|
683
|
+
"appearance.font": "Schrift",
|
|
684
|
+
"appearance.customFont": "Eigene Schrift",
|
|
685
|
+
"appearance.customFontHint": "Jede auf Ihrem Computer installierte Schrift",
|
|
686
|
+
"appearance.fontSize": "Textgr\xF6\xDFe",
|
|
687
|
+
"appearance.lineHeight": "Zeilenabstand",
|
|
688
|
+
"appearance.preview": "Zw\xF6lf Boxk\xE4mpfer jagen Viktor quer \xFCber den gro\xDFen Sylter Deich.",
|
|
689
|
+
"appearance.layout": "Layout",
|
|
690
|
+
"appearance.density": "Abst\xE4nde",
|
|
691
|
+
"appearance.comfortable": "Komfortabel",
|
|
692
|
+
"appearance.compact": "Kompakt",
|
|
693
|
+
"appearance.radius": "Ecken",
|
|
694
|
+
"appearance.sharp": "Eckig",
|
|
695
|
+
"appearance.default": "Standard",
|
|
696
|
+
"appearance.round": "Rund",
|
|
697
|
+
"appearance.translucency": "Transparenz",
|
|
698
|
+
"appearance.motion": "Bewegung reduzieren",
|
|
699
|
+
"appearance.reset": "Darstellung zur\xFCcksetzen",
|
|
700
|
+
"language.follow": "Systemsprache verwenden"
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
`,
|
|
704
|
+
"plugins/language/src/renderer.tsx": 'import { definePlugin } from "fluxplugin";\nimport { Group, Icon, Row, useI18n } from "fluxplugin/react";\nimport { messages } from "./messages";\n\n// To add a language: translate src/messages.ts, then add it here.\nconst languages = [\n { code: "fr", label: "French", nativeLabel: "Fran\xE7ais" },\n { code: "es", label: "Spanish", nativeLabel: "Espa\xF1ol" },\n { code: "de", label: "German", nativeLabel: "Deutsch" },\n];\n\nfunction LanguageSection() {\n const { locale, setLocale, languages: all, t } = useI18n();\n return (\n <>\n <Group title={t("settings.language", "Language")}>\n {all.map((l) => (\n <Row key={l.code} data-testid={"lang-" + l.code} title={l.nativeLabel ?? l.label} subtitle={l.label} onClick={() => setLocale(l.code)} right={locale === l.code ? <Icon name="check" size={18} /> : undefined} />\n ))}\n </Group>\n <Group>\n <Row title={t("language.follow", "Follow the system language")} onClick={() => setLocale(navigator.language)} />\n </Group>\n </>\n );\n}\n\nexport default definePlugin({\n extensions: [\n { point: "settings", contribution: { sections: [{ id: "language", title: "Language", icon: "language", order: 20, component: LanguageSection }] } },\n { point: "i18n", contribution: { languages } },\n ...Object.entries(messages).map(([locale, m]) => ({ point: "i18n", contribution: { messages: [{ locale, messages: m }] } })),\n ],\n});\n',
|
|
705
|
+
"src/app/main/framework.ts": 'import { MainFramework, type StoreSpec } from "fluxplugin/main";\nimport config from "../../../fluxplugin.config.json"; // bundled into the app at build time\nimport { CHANNELS } from "../shared/constants";\nimport { registerOperations } from "./operations";\n\nexport interface FrameworkOptions {\n pluginDirs: string[];\n /** Where "Get" installs plugins (default: the last of pluginDirs). */\n installDir?: string;\n dataDir: string;\n dev?: boolean;\n assetUrl?: (id: string, rel: string) => string;\n}\n\n/** Creates the plugin framework. Shared by the Electron entry (index.ts) and the browser entry (web.ts). */\nexport function createFramework(opts: FrameworkOptions): MainFramework {\n const fw = new MainFramework({\n pluginDirs: opts.pluginDirs,\n installDir: opts.installDir ?? opts.pluginDirs[opts.pluginDirs.length - 1],\n dataDir: opts.dataDir,\n hotReload: opts.dev,\n assetUrl: opts.assetUrl,\n // Where "Plugins \u2192 Available" looks. Default: GitHub (every repo that published with `fluxplugin publish`).\n // Edit "stores" in fluxplugin.config.json to add your own store (github catalog, http, local) \u2014 see docs/STORE.md.\n stores: config.stores as StoreSpec[],\n requireApproval: false, // true \u2192 users must approve each plugin\'s permissions\n allowedRendererChannels: [CHANNELS.createItem, CHANNELS.setTitleBarOverlay],\n });\n registerOperations(fw);\n return fw;\n}\n',
|
|
706
|
+
"src/app/main/index.ts": '// Electron entry (main process). Keep it thin: wiring only, logic lives in the other files of this folder.\nimport { app, ipcMain, Menu, protocol, webContents } from "electron";\nimport path from "node:path";\nimport { PLUGIN_SCHEME, attachElectron, registerPluginProtocol } from "fluxplugin/main";\nimport { APP_NAME } from "../shared/constants";\nimport { createFramework } from "./framework";\nimport { pluginPaths, projectRoot } from "./paths";\nimport { createMainWindow } from "./window";\nimport { registerWindowApi } from "./window-api";\n\n// Own data folder per app. In dev Electron is started as `electron dist/main.cjs` and would otherwise be called "Electron",\n// sharing plugin state (enabled/disabled, permissions, settings) with every other app run that way.\napp.setName(APP_NAME);\n\nprotocol.registerSchemesAsPrivileged([PLUGIN_SCHEME]); // must run before the app is ready\n\napp.whenReady().then(async () => {\n const dev = !!process.env["FLUXPLUGIN_DEV"];\n // The menu bar is drawn by the renderer (<TitleBar />). Windows/Linux: no native menu. macOS keeps a minimal one\n // so the system shortcuts (copy/paste/quit/hide) keep working.\n Menu.setApplicationMenu(process.platform === "darwin" ? Menu.buildFromTemplate([{ role: "appMenu" }, { role: "editMenu" }, { role: "windowMenu" }]) : null);\n\n const paths = pluginPaths(projectRoot(__dirname), app.getPath("userData"), app.isPackaged, process.resourcesPath);\n const fw = createFramework({\n pluginDirs: [paths.bundled, paths.user],\n installDir: paths.user,\n dataDir: path.join(app.getPath("userData"), "plugins-data"),\n dev,\n });\n registerPluginProtocol(fw, protocol);\n attachElectron(fw, { ipcMain: ipcMain as never, webContents: webContents as never });\n await fw.start();\n\n const win = createMainWindow(__dirname);\n registerWindowApi(fw, win);\n if (dev) win.webContents.on("before-input-event", (_e, input) => input.key === "F12" && input.type === "keyDown" && win.webContents.toggleDevTools());\n app.on("before-quit", () => void fw.stop());\n});\n\napp.on("window-all-closed", () => app.quit());\n',
|
|
255
707
|
"src/app/main/operations.ts": 'import { s } from "fluxplugin";\nimport type { MainFramework } from "fluxplugin/main";\nimport { CHANNELS, OPERATIONS } from "../shared/constants";\nimport type { Item } from "../shared/types";\n\n/**\n * Host operations. Each one runs through the hook pipeline, so plugins can validate, normalize or veto it\n * (before.item.create, transform.item.create, ...) without touching this file.\n */\nexport function registerOperations(fw: MainFramework): void {\n fw.manager.ipc.handle(\n CHANNELS.createItem,\n (input: { name: string }) =>\n fw.manager.hooks.run(OPERATIONS.createItem, input, (i) => {\n const item: Item = { id: Math.random().toString(36).slice(2, 8), name: i.name };\n fw.manager.events.emit("item.created", item, "host");\n return item as unknown as Record<string, unknown>;\n }),\n "host",\n { input: s.object({ name: s.string() }) },\n );\n}\n',
|
|
256
|
-
"src/app/main/paths.ts": 'import path from "node:path";\n\n/** Project root: set by `fluxplugin dev|start`; falls back to the folder above dist/ (packaged app). */\nexport function projectRoot(distDir: string): string {\n return process.env["FLUXPLUGIN_PROJECT"] ?? path.resolve(distDir, "..");\n}\n\n/** Plugins
|
|
708
|
+
"src/app/main/paths.ts": 'import path from "node:path";\n\n/** Project root: set by `fluxplugin dev|start`; falls back to the folder above dist/ (packaged app). */\nexport function projectRoot(distDir: string): string {\n return process.env["FLUXPLUGIN_PROJECT"] ?? path.resolve(distDir, "..");\n}\n\nexport interface PluginPaths {\n /** Plugins shipped with the app: <project>/plugins in dev, <resources>/plugins in the packaged app (read-only). */\n bundled: string;\n /** Plugins installed by the user (writable): inside the app\'s data folder, removed with it when the app is uninstalled. */\n user: string;\n}\n\nexport function pluginPaths(root: string, userData: string, packaged: boolean, resourcesPath: string): PluginPaths {\n return { bundled: packaged ? path.join(resourcesPath, "plugins") : path.join(root, "plugins"), user: path.join(userData, "plugins") };\n}\n',
|
|
257
709
|
"src/app/main/web.ts": '// Browser entry: `fluxplugin dev --web` (no Electron needed).\nimport path from "node:path";\nimport { serveWeb } from "fluxplugin/main";\nimport { APP_NAME } from "../shared/constants";\nimport { createFramework } from "./framework";\n\nconst root = process.env["FLUXPLUGIN_PROJECT"] ?? process.cwd();\nconst fw = createFramework({\n pluginDirs: [path.join(root, "plugins")],\n dataDir: path.join(root, ".flux-data"),\n dev: true,\n assetUrl: (id, rel) => "/@plugins/" + id + "/" + rel.replace("./", ""),\n});\nawait fw.start();\nconst srv = await serveWeb(fw, { distDir: path.join(root, "dist"), port: Number(process.env["PORT"] ?? 5173) });\nconsole.log(APP_NAME + " -> http://127.0.0.1:" + srv.port);\n',
|
|
258
710
|
"src/app/main/window-api.ts": 'import type { BrowserWindow } from "electron";\nimport { s } from "fluxplugin";\nimport type { MainFramework } from "fluxplugin/main";\nimport { CHANNELS, TITLEBAR_HEIGHT } from "../shared/constants";\n\n/** Host operations that need the BrowserWindow. Add your own (maximize, always-on-top, \u2026) the same way. */\nexport function registerWindowApi(fw: MainFramework, win: BrowserWindow): void {\n fw.manager.ipc.handle(\n CHANNELS.setTitleBarOverlay,\n (i: { color: string; symbolColor: string }) => {\n // Only Windows/Linux have the native-controls overlay; on macOS the traffic lights need no colouring.\n if (process.platform !== "darwin" && !win.isDestroyed()) {\n try {\n win.setTitleBarOverlay({ color: i.color, symbolColor: i.symbolColor, height: TITLEBAR_HEIGHT });\n } catch {\n /* colour not parseable: keep the previous one */\n }\n }\n return true;\n },\n "host",\n { input: s.object({ color: s.string(), symbolColor: s.string() }) },\n );\n}\n',
|
|
259
711
|
"src/app/main/window.ts": `import { BrowserWindow, shell } from "electron";
|
|
@@ -295,7 +747,7 @@ export function createMainWindow(distDir: string): BrowserWindow {
|
|
|
295
747
|
`,
|
|
296
748
|
"src/app/preload/index.ts": 'import { contextBridge, ipcRenderer } from "electron";\nimport { exposeFluxBridge } from "fluxplugin/preload";\n\n// The only thing renderer code receives from the preload: window.__fluxplugin\nexposeFluxBridge(contextBridge, ipcRenderer);\n',
|
|
297
749
|
"src/app/renderer/App.tsx": 'import type { FluxBridge } from "fluxplugin";\nimport type { RendererFramework } from "fluxplugin/react";\nimport { AppProviders } from "./providers/AppProviders";\nimport { AppShell } from "./shell/AppShell";\n\nexport function App({ framework, bridge }: { framework: RendererFramework; bridge: FluxBridge }) {\n return (\n <AppProviders framework={framework} bridge={bridge}>\n <AppShell />\n </AppProviders>\n );\n}\n',
|
|
298
|
-
"src/app/renderer/bootstrap.ts": 'import React from "react";\nimport * as jsxRuntime from "react/jsx-runtime";\nimport * as ReactDOMClient from "react-dom/client";\nimport * as flux from "fluxplugin";\nimport * as fluxReact from "fluxplugin/react";\nimport type { FluxBridge } from "fluxplugin";\nimport { RendererFramework, createHttpBridge, declareHostPoints, installSharedModules } from "fluxplugin/react";\nimport { declareExtensionPoints } from "./extension-points";\nimport { registerHostMenus } from "./menus";\nimport { registerHostRoutes } from "./routes";\n\n/**\n * Infrastructure layer: connects to the main process, shares singletons with plugin bundles,\n * declares what plugins may extend, registers the host\'s own routes, then starts the plugins.\n */\nexport async function bootstrap(): Promise<{ framework: RendererFramework; bridge: FluxBridge }> {\n // One copy of React and of the runtime: plugin bundles import these from globalThis.\n installSharedModules({\n react: React,\n "react/jsx-runtime": jsxRuntime,\n "react/jsx-dev-runtime": jsxRuntime,\n "react-dom/client": ReactDOMClient,\n fluxplugin: flux,\n "fluxplugin/react": fluxReact,\n });\n\n
|
|
750
|
+
"src/app/renderer/bootstrap.ts": 'import React from "react";\nimport * as jsxRuntime from "react/jsx-runtime";\nimport * as ReactDOMClient from "react-dom/client";\nimport * as flux from "fluxplugin";\nimport * as fluxReact from "fluxplugin/react";\nimport type { FluxBridge } from "fluxplugin";\nimport { RendererFramework, createHttpBridge, createStaticBridge, declareHostPoints, installSharedModules } from "fluxplugin/react";\nimport { declareExtensionPoints } from "./extension-points";\nimport { registerHostMenus } from "./menus";\nimport { registerHostSettings } from "./settings";\nimport { registerHostRoutes } from "./routes";\n\n/**\n * Infrastructure layer: connects to the main process, shares singletons with plugin bundles,\n * declares what plugins may extend, registers the host\'s own routes, then starts the plugins.\n */\nexport async function bootstrap(): Promise<{ framework: RendererFramework; bridge: FluxBridge }> {\n // One copy of React and of the runtime: plugin bundles import these from globalThis.\n installSharedModules({\n react: React,\n "react/jsx-runtime": jsxRuntime,\n "react/jsx-dev-runtime": jsxRuntime,\n "react-dom/client": ReactDOMClient,\n fluxplugin: flux,\n "fluxplugin/react": fluxReact,\n });\n\n const bridge = await pickBridge();\n const framework = new RendererFramework({ bridge });\n\n declareHostPoints(framework); // built-in "theme" and "titlebar" (search providers) extension points\n declareExtensionPoints(framework); // BEFORE plugins start\n registerHostRoutes(framework);\n registerHostMenus(framework);\n registerHostSettings(framework);\n await framework.start();\n return { framework, bridge };\n}\n\n/** Electron: the preload bridge. Browser dev mode: HTTP bridge to `fluxplugin dev --web`. Static website export: bootstrap.json only. */\nasync function pickBridge(): Promise<FluxBridge> {\n const injected = (window as unknown as { __fluxplugin?: FluxBridge }).__fluxplugin;\n if (injected) return injected;\n try {\n const r = await fetch("/flux/bootstrap");\n if (r.ok && (r.headers.get("content-type") ?? "").includes("json")) return createHttpBridge();\n } catch {\n /* no server behind this page */\n }\n return createStaticBridge("./bootstrap.json");\n}\n',
|
|
299
751
|
"src/app/renderer/extension-points.ts": 'import { s } from "fluxplugin";\nimport type { RendererFramework } from "fluxplugin/react";\nimport { EXTENSION_POINTS } from "../shared/constants";\n\n/** Declare what plugins may contribute. Contributions are validated against these schemas. */\nexport function declareExtensionPoints(framework: RendererFramework): void {\n framework.manager.extensionPoints.declare({\n name: EXTENSION_POINTS.dashboard,\n contributions: {\n toolbar: s.object({ id: s.string(), label: s.string(), command: s.string().optional() }),\n },\n });\n}\n',
|
|
300
752
|
"src/app/renderer/hooks/useCreateItem.ts": 'import { useState } from "react";\nimport { useBridge } from "../providers/AppProviders";\nimport { itemsApi } from "../services/items";\n\n/** Calls the host operation (which plugins may rewrite or veto through hooks) and exposes the outcome. */\nexport function useCreateItem() {\n const bridge = useBridge();\n const [result, setResult] = useState("");\n const create = async (name: string) => {\n try {\n setResult(JSON.stringify(await itemsApi.create(bridge, name)));\n } catch (e) {\n setResult("blocked: " + (e as Error).message);\n }\n };\n return { create, result };\n}\n',
|
|
301
753
|
"src/app/renderer/index.html": `<!doctype html>
|
|
@@ -303,7 +755,7 @@ export function createMainWindow(distDir: string): BrowserWindow {
|
|
|
303
755
|
<head>
|
|
304
756
|
<meta charset="utf-8" />
|
|
305
757
|
<title>__APP_NAME__</title>
|
|
306
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'self' fluxplugin:; script-src 'self' fluxplugin:; style-src 'self' 'unsafe-inline' fluxplugin:; connect-src 'self'; img-src 'self' data: fluxplugin:; frame-src https://www.youtube-nocookie.com https://player.vimeo.com" />
|
|
758
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self' fluxplugin:; script-src 'self' fluxplugin:; style-src 'self' 'unsafe-inline' fluxplugin:; font-src 'self' data: https: fluxplugin:; connect-src 'self'; img-src 'self' data: blob: https: http: file: fluxplugin:; media-src 'self' blob: data: https: file: fluxplugin:; frame-src https://www.youtube-nocookie.com https://player.vimeo.com https://www.dailymotion.com https://www.loom.com https://streamable.com" />
|
|
307
759
|
<link rel="stylesheet" href="./renderer.css" />
|
|
308
760
|
</head>
|
|
309
761
|
<body>
|
|
@@ -325,14 +777,18 @@ export function registerHostMenus(framework: RendererFramework): void {
|
|
|
325
777
|
commands.register({ id: "host.reload", title: "Reload window", category: "View", keybinding: "Mod+R", execute: () => location.reload() }, "host");
|
|
326
778
|
commands.register({ id: "host.goHome", title: "Go to dashboard", category: "Go", execute: () => (location.hash = "#/") }, "host");
|
|
327
779
|
commands.register({ id: "host.goPlugins", title: "Manage plugins", category: "Go", execute: () => (location.hash = "#/plugins") }, "host");
|
|
780
|
+
commands.register({ id: "host.sidebar", title: "Toggle sidebar", category: "View", keybinding: "Mod+B", execute: () => window.dispatchEvent(new Event("flux:toggle-sidebar")) }, "host");
|
|
781
|
+
commands.register({ id: "host.goSettings", title: "Settings", category: "Go", keybinding: "Mod+,", execute: () => (location.hash = "#/settings") }, "host");
|
|
328
782
|
commands.register({ id: "host.docs", title: "Documentation", category: "Help", execute: () => void window.open("https://www.npmjs.com/package/fluxplugin", "_blank") }, "host");
|
|
329
783
|
|
|
330
784
|
menus.register({ id: "app.go", location: "application", label: "Go", order: 10, children: [
|
|
331
785
|
{ id: "app.go.home", label: "Dashboard", command: "host.goHome" },
|
|
332
786
|
{ id: "app.go.plugins", label: "Plugins", command: "host.goPlugins" },
|
|
787
|
+
{ id: "app.go.settings", label: "Settings", command: "host.goSettings" },
|
|
333
788
|
] }, "host");
|
|
334
789
|
menus.register({ id: "app.view", location: "application", label: "View", order: 20, children: [
|
|
335
790
|
{ id: "app.view.palette", label: "Command palette", command: "host.palette" },
|
|
791
|
+
{ id: "app.view.sidebar", label: "Toggle sidebar", command: "host.sidebar" },
|
|
336
792
|
{ id: "app.view.sep", label: "", separator: true },
|
|
337
793
|
{ id: "app.view.reload", label: "Reload", command: "host.reload" },
|
|
338
794
|
] }, "host");
|
|
@@ -340,23 +796,28 @@ export function registerHostMenus(framework: RendererFramework): void {
|
|
|
340
796
|
}
|
|
341
797
|
`,
|
|
342
798
|
"src/app/renderer/pages/DashboardPage.tsx": 'import { PluginSlot, useExtensionItems, usePluginFramework } from "fluxplugin/react";\nimport { EXTENSION_POINTS, SLOTS } from "../../shared/constants";\nimport type { ToolbarAction } from "../../shared/types";\nimport { useCreateItem } from "../hooks/useCreateItem";\n\nexport function DashboardPage() {\n const framework = usePluginFramework();\n const actions = useExtensionItems<ToolbarAction>(EXTENSION_POINTS.dashboard, "toolbar");\n const { create, result } = useCreateItem();\n return (\n <div>\n <h2>Dashboard</h2>\n <PluginSlot name={SLOTS.dashboardBefore} />\n <p className="hint">These buttons are contributed by plugins through a host-declared extension point.</p>\n <div className="row" data-testid="toolbar">\n {actions.map((a) => (\n <button key={a.id} className="btn" onClick={() => a.command && void framework.manager.commands.execute(a.command)}>\n {a.label}\n </button>\n ))}\n </div>\n <div className="row">\n <button className="btn" onClick={() => void create(" Ada ")}>Create item \u201C Ada \u201D (plugin hooks may rewrite it)</button>\n <button className="btn" onClick={() => void create(" ")}>Create empty item (a plugin may veto)</button>\n </div>\n <div className="row">\n <button className="btn" data-testid="notify" onClick={() => framework.manager.notifications.push({ title: "Hello from the host", body: "Plugins can do this too.", level: "success" })}>Show a notification</button>\n </div>\n <pre data-testid="hook-result" className="hint">{result}</pre>\n <PluginSlot name={SLOTS.dashboardAfter} />\n </div>\n );\n}\n',
|
|
343
|
-
"src/app/renderer/
|
|
344
|
-
"src/app/renderer/
|
|
345
|
-
|
|
799
|
+
"src/app/renderer/providers/AppProviders.tsx": 'import { createContext, useCallback, useContext, type ReactNode } from "react";\nimport type { FluxBridge } from "fluxplugin";\nimport { I18nProvider, PluginProvider, SidebarProvider, StyleHost, ThemeProvider, ToastHost, baseCss, type RendererFramework } from "fluxplugin/react";\nimport { setTitleBarOverlay } from "../services/window";\n\nconst BridgeContext = createContext<FluxBridge | null>(null);\n\n/** Gives pages access to the main-process bridge without importing globals. */\nexport function useBridge(): FluxBridge {\n const b = useContext(BridgeContext);\n if (!b) throw new Error("useBridge must be used inside <AppProviders>");\n return b;\n}\n\nexport function AppProviders({ framework, bridge, children }: { framework: RendererFramework; bridge: FluxBridge; children: ReactNode }) {\n // Keep the native window buttons in sync with the theme (light / dark / plugin themes).\n const onResolved = useCallback((r: { surface: string; fg: string }) => r.surface && setTitleBarOverlay(bridge, r.surface, r.fg), [bridge]);\n return (\n <PluginProvider framework={framework}>\n <BridgeContext.Provider value={bridge}>\n <style>{baseCss}</style>\n <StyleHost />\n <I18nProvider>\n <ThemeProvider onResolved={onResolved}>\n <SidebarProvider>\n {children}\n <ToastHost />\n </SidebarProvider>\n </ThemeProvider>\n </I18nProvider>\n </BridgeContext.Provider>\n </PluginProvider>\n );\n}\n',
|
|
800
|
+
"src/app/renderer/routes.ts": `import { SettingsPage, type RendererFramework } from "fluxplugin/react";
|
|
801
|
+
import { registerPluginManagerRoutes } from "fluxplugin/manager-ui";
|
|
346
802
|
import { DashboardPage } from "./pages/DashboardPage";
|
|
347
|
-
import { PluginsPage } from "./pages/PluginsPage";
|
|
348
803
|
|
|
349
804
|
/** The host's own pages (owner "host"). Plugins add theirs at runtime through the same registry. */
|
|
350
805
|
export function registerHostRoutes(framework: RendererFramework): void {
|
|
351
|
-
framework.manager.routes
|
|
352
|
-
|
|
806
|
+
const routes = framework.manager.routes;
|
|
807
|
+
routes.register({ path: "/", component: DashboardPage, meta: { title: "Dashboard" } }, "host");
|
|
808
|
+
// Plugin manager: list \u2192 /plugins/:id (installed) \u2192 /plugins/store/:id (store listing). Each opens in its own screen.
|
|
809
|
+
registerPluginManagerRoutes(framework);
|
|
810
|
+
// Settings: sections come from the host and from plugins (Appearance, Language\u2026).
|
|
811
|
+
routes.register({ path: "/settings", component: SettingsPage, meta: { title: "Settings", breadcrumb: "Settings" } }, "host");
|
|
812
|
+
routes.register({ path: "/settings/:section", component: SettingsPage, meta: { breadcrumb: (p) => p["section"] ?? "" } }, "host");
|
|
353
813
|
}
|
|
354
814
|
`,
|
|
355
815
|
"src/app/renderer/services/items.ts": 'import type { FluxBridge } from "fluxplugin";\nimport { CHANNELS } from "../../shared/constants";\nimport type { Item } from "../../shared/types";\n\n/** Service layer: the only place that knows the IPC channel names. UI code calls these functions. */\nexport const itemsApi = {\n create: (bridge: FluxBridge, name: string) => bridge.invoke(CHANNELS.createItem, { name }) as Promise<Item>,\n};\n',
|
|
356
816
|
"src/app/renderer/services/window.ts": 'import type { FluxBridge } from "fluxplugin";\nimport { CHANNELS } from "../../shared/constants";\n\n/** Colours the native window buttons (Windows/Linux). A no-op error in browser mode is ignored on purpose. */\nexport function setTitleBarOverlay(bridge: FluxBridge, color: string, symbolColor: string): void {\n void bridge.invoke(CHANNELS.setTitleBarOverlay, { color, symbolColor }).catch(() => undefined);\n}\n',
|
|
357
|
-
"src/app/renderer/
|
|
358
|
-
"src/app/renderer/shell/
|
|
359
|
-
"src/app/renderer/
|
|
817
|
+
"src/app/renderer/settings/index.tsx": 'import type { ComponentType } from "react";\nimport { Group, Row, Switch, useSidebar, useT, usePluginFramework, usePlugins, formatShortcut, useOS, type RendererFramework } from "fluxplugin/react";\nimport { APP_NAME } from "../../shared/constants";\n\nfunction GeneralSection() {\n const t = useT();\n const { collapsed, setCollapsed } = useSidebar();\n const plugins = usePlugins();\n return (\n <>\n <Group title={t("settings.about", "About")}>\n <Row title={t("settings.app", "Application")} right={APP_NAME} />\n <Row title={t("settings.plugins", "Plugins")} right={`${plugins.filter((p) => p.state === "ready").length} / ${plugins.length}`} />\n <Row title={t("settings.engine", "Engine")} right={navigator.userAgent.match(/(Electron|Chrome|Firefox|Safari)\\/[\\d.]+/)?.[0] ?? "\u2014"} />\n </Group>\n <Group title={t("settings.sidebar", "Sidebar")} footer={t("settings.sidebarHint", "Collapse the sidebar to icons. Shortcut: Ctrl/Cmd+B.")}>\n <Row title={t("settings.compactSidebar", "Compact sidebar")} right={<Switch checked={collapsed} onChange={setCollapsed} label="Compact sidebar" />} />\n </Group>\n </>\n );\n}\n\nfunction ShortcutsSection() {\n const fw = usePluginFramework();\n const os = useOS();\n const t = useT();\n const cmds = fw.manager.commands.list().filter((c) => c.keybinding);\n return (\n <Group title={t("settings.shortcuts", "Keyboard shortcuts")}>\n {cmds.length === 0 && <Row title="\u2014" />}\n {cmds.map((c) => <Row key={c.id} title={c.title} subtitle={c.category} right={<kbd>{formatShortcut(c.keybinding!, os)}</kbd>} />)}\n </Group>\n );\n}\n\n/** Host settings sections. Plugins contribute theirs to the same "settings" extension point. */\nexport function registerHostSettings(framework: RendererFramework): void {\n framework.manager.extensionPoints.contribute(\n "settings",\n { sections: [\n { id: "general", title: "General", icon: "settings", order: 0, component: GeneralSection as ComponentType },\n { id: "shortcuts", title: "Shortcuts", icon: "command", order: 90, component: ShortcutsSection as ComponentType },\n ] },\n "host",\n );\n}\n',
|
|
818
|
+
"src/app/renderer/shell/AppShell.tsx": 'import { Breadcrumbs, CommandPalette, PluginRoutes, PluginSlot, TitleBar, useKeybindings } from "fluxplugin/react";\nimport { APP_NAME, SLOTS } from "../../shared/constants";\nimport { Sidebar } from "./Sidebar";\n\n/** App shell with a fixed sidebar and independently scrollable content. */\nexport function AppShell() {\n useKeybindings();\n\n return (\n <div className="app">\n {/* Title bar */}\n <header className="app-titlebar">\n <TitleBar title={APP_NAME} logo={<PluginSlot name={SLOTS.brandBadges} />} searchPlaceholder="Search commands, pages\u2026" />\n </header>\n\n {/* Application body */}\n <div className="body">\n {/* Fixed sidebar: never scrolls with the main content (collapses to an icon rail) */}\n <aside className="app-side">\n <Sidebar />\n </aside>\n\n {/* Only this area scrolls */}\n <main className="main">\n <div className="main-inner">\n <Breadcrumbs />\n <PluginRoutes />\n </div>\n </main>\n </div>\n\n {/* Command palette */}\n <CommandPalette />\n </div>\n );\n}\n',
|
|
819
|
+
"src/app/renderer/shell/Sidebar.tsx": 'import { SidebarNav, type SidebarEntry } from "fluxplugin/react";\n\n// Host entries. Plugins add theirs between the two groups (routes with meta.menu, or menus.register({ location: "sidebar" })),\n// including sub-menus: `meta.menu.parent` or `children`. Labels are translated through `menu.<id>`.\nconst top: SidebarEntry[] = [{ id: "dashboard", label: "Dashboard", path: "/", icon: "home" }];\nconst bottom: SidebarEntry[] = [\n { id: "plugins", label: "Plugins", path: "/plugins", icon: "plug" },\n { id: "settings", label: "Settings", path: "/settings", icon: "settings" },\n];\n\nexport function Sidebar() {\n return <SidebarNav top={top} bottom={bottom} footer={<span>Ctrl/Cmd+K: commands</span>} />;\n}\n',
|
|
820
|
+
"src/app/renderer/styles/app.css": "body { margin: 0; }\n.app { display: grid; height: 100vh; grid-template-rows: 40px minmax(0, 1fr); overflow: hidden; }\n.app-titlebar { z-index: 50; min-width: 0; }\n.body { display: grid; min-height: 0; min-width: 0; grid-template-columns: var(--flux-sidebar-w, 232px) minmax(0, 1fr); overflow: hidden; transition: grid-template-columns .18s ease; }\n.app-side { height: 100%; min-height: 0; overflow: hidden; }\n.main { min-height: 0; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 24px 28px; }\n.main-inner { max-width: 900px; margin: 0 auto; }\n.row { display: flex; gap: 8px; margin: 12px 0; flex-wrap: wrap; }\n.btn { border: 1px solid var(--flux-border); background: var(--flux-bg); color: inherit; border-radius: 8px; padding: 6px 14px; cursor: pointer; font-weight: 500; }\n.btn:hover { background: var(--flux-surface); }\n.hint { color: var(--flux-muted); font-size: .86em; }\n",
|
|
360
821
|
"src/app/shared/constants.ts": '/** Names shared by the main process and the renderer. No imports here: this layer depends on nothing. */\nexport const APP_NAME = "__APP_NAME__";\n\n/** Height of the custom title bar in px (must match --flux-titlebar-h in CSS). */\nexport const TITLEBAR_HEIGHT = 40;\n\n/** IPC channels the host exposes to the renderer (also allow-listed in main/framework.ts). */\nexport const CHANNELS = {\n createItem: "host.item.create",\n /** Colours the native window buttons (Windows/Linux overlay) to match the current theme. */\n setTitleBarOverlay: "host.window.setTitleBarOverlay",\n} as const;\n\n/** Declarative extension points the host offers to plugins (see renderer/extension-points.ts).\n * The built-in "theme" and "titlebar" points are declared by declareHostPoints(). */\nexport const EXTENSION_POINTS = {\n dashboard: "dashboard",\n} as const;\n\n/** UI slots where plugins can inject components (see <PluginSlot name=... />). */\nexport const SLOTS = {\n brandBadges: "header.badges",\n titlebarLeft: "titlebar.left",\n titlebarRight: "titlebar.right",\n dashboardBefore: "dashboard.before",\n dashboardAfter: "dashboard.after",\n} as const;\n\n/** Hook pipeline operations plugins can intercept: before./after./transform./validate.<operation>. */\nexport const OPERATIONS = {\n createItem: "item.create",\n} as const;\n',
|
|
361
822
|
"src/app/shared/types.ts": 'export interface Item {\n id: string;\n name: string;\n}\n\n/** Shape of a contribution to the "dashboard" extension point (validated in renderer/extension-points.ts). */\nexport interface ToolbarAction {\n id: string;\n label: string;\n command?: string;\n}\n'
|
|
362
823
|
};
|
|
@@ -366,7 +827,7 @@ var HOST_TAILWIND_FILES = {
|
|
|
366
827
|
<head>
|
|
367
828
|
<meta charset="utf-8" />
|
|
368
829
|
<title>__APP_NAME__</title>
|
|
369
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'self' fluxplugin:; script-src 'self' fluxplugin:; style-src 'self' 'unsafe-inline' fluxplugin:; connect-src 'self'; img-src 'self' data: fluxplugin:; frame-src https://www.youtube-nocookie.com https://player.vimeo.com" />
|
|
830
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self' fluxplugin:; script-src 'self' fluxplugin:; style-src 'self' 'unsafe-inline' fluxplugin:; font-src 'self' data: https: fluxplugin:; connect-src 'self'; img-src 'self' data: blob: https: http: file: fluxplugin:; media-src 'self' blob: data: https: file: fluxplugin:; frame-src https://www.youtube-nocookie.com https://player.vimeo.com https://www.dailymotion.com https://www.loom.com https://streamable.com" />
|
|
370
831
|
<link rel="stylesheet" href="./renderer.css" />
|
|
371
832
|
<link rel="stylesheet" href="./tailwind.css" />
|
|
372
833
|
</head>
|
|
@@ -377,9 +838,8 @@ var HOST_TAILWIND_FILES = {
|
|
|
377
838
|
</html>
|
|
378
839
|
`,
|
|
379
840
|
"src/app/renderer/pages/DashboardPage.tsx": 'import { PluginSlot, useExtensionItems, usePluginFramework } from "fluxplugin/react";\nimport { EXTENSION_POINTS, SLOTS } from "../../shared/constants";\nimport type { ToolbarAction } from "../../shared/types";\nimport { useCreateItem } from "../hooks/useCreateItem";\n\nexport function DashboardPage() {\n const framework = usePluginFramework();\n const actions = useExtensionItems<ToolbarAction>(EXTENSION_POINTS.dashboard, "toolbar");\n const { create, result } = useCreateItem();\n return (\n <div>\n <h2>Dashboard</h2>\n <PluginSlot name={SLOTS.dashboardBefore} />\n <p className="text-xs text-muted">These buttons are contributed by plugins through a host-declared extension point.</p>\n <div className="my-3 flex flex-wrap gap-2" data-testid="toolbar">\n {actions.map((a) => (\n <button key={a.id} className="cursor-pointer rounded-lg border border-border bg-bg px-3 py-1.5 hover:bg-surface" onClick={() => a.command && void framework.manager.commands.execute(a.command)}>\n {a.label}\n </button>\n ))}\n </div>\n <div className="my-3 flex flex-wrap gap-2">\n <button className="cursor-pointer rounded-lg border border-border bg-bg px-3 py-1.5 hover:bg-surface" onClick={() => void create(" Ada ")}>Create item \u201C Ada \u201D (plugin hooks may rewrite it)</button>\n <button className="cursor-pointer rounded-lg border border-border bg-bg px-3 py-1.5 hover:bg-surface" onClick={() => void create(" ")}>Create empty item (a plugin may veto)</button>\n </div>\n <div className="my-3 flex flex-wrap gap-2">\n <button className="cursor-pointer rounded-lg border border-border bg-bg px-3 py-1.5 hover:bg-surface" data-testid="notify" onClick={() => framework.manager.notifications.push({ title: "Hello from the host", body: "Plugins can do this too.", level: "success" })}>Show a notification</button>\n </div>\n <pre data-testid="hook-result" className="text-xs text-muted">{result}</pre>\n <PluginSlot name={SLOTS.dashboardAfter} />\n </div>\n );\n}\n',
|
|
380
|
-
"src/app/renderer/shell/AppShell.tsx": 'import {\n Breadcrumbs,\n CommandPalette,\n PluginRoutes,\n PluginSlot,\n TitleBar,\n useKeybindings,\n} from "fluxplugin/react";\n\nimport { APP_NAME, SLOTS } from "../../shared/constants";\nimport { Sidebar } from "./Sidebar";\n\n/** App shell with a fixed sidebar and independently scrollable content. */\nexport function AppShell() {\n useKeybindings();\n\n return (\n <div className="app grid h-screen grid-rows-[40px_minmax(0,1fr)] overflow-hidden">\n {/* Title bar */}\n <header className="z-50 min-w-0">\n <TitleBar\n title={APP_NAME}\n logo={<PluginSlot name={SLOTS.brandBadges} />}\n searchPlaceholder="Search commands, pages
|
|
381
|
-
"src/app/renderer/
|
|
382
|
-
"src/app/renderer/styles/app.css": "body { margin: 0; font: 14px system-ui; background: var(--flux-bg); color: var(--flux-fg); }\n",
|
|
841
|
+
"src/app/renderer/shell/AppShell.tsx": 'import {\n Breadcrumbs,\n CommandPalette,\n PluginRoutes,\n PluginSlot,\n TitleBar,\n useKeybindings,\n} from "fluxplugin/react";\n\nimport { APP_NAME, SLOTS } from "../../shared/constants";\nimport { Sidebar } from "./Sidebar";\n\n/** App shell with a fixed sidebar and independently scrollable content. */\nexport function AppShell() {\n useKeybindings();\n\n return (\n <div className="app grid h-screen grid-rows-[40px_minmax(0,1fr)] overflow-hidden">\n {/* Title bar */}\n <header className="z-50 min-w-0">\n <TitleBar\n title={APP_NAME}\n logo={<PluginSlot name={SLOTS.brandBadges} />}\n searchPlaceholder="Search commands, pages..."\n />\n </header>\n\n {/* Application body (the sidebar width follows --flux-sidebar-w: it collapses to an icon rail) */}\n <div className="body grid min-h-0 min-w-0 grid-cols-[var(--flux-sidebar-w,232px)_minmax(0,1fr)] overflow-hidden transition-[grid-template-columns] duration-200">\n {/* Fixed sidebar: never scrolls with the main content */}\n <aside className="h-full min-h-0 overflow-hidden border-r border-transparent">\n <Sidebar />\n </aside>\n\n {/* Only this area scrolls */}\n <main className="main min-h-0 min-w-0 overflow-y-auto overflow-x-hidden p-6">\n <div className="mx-auto max-w-3xl">\n <Breadcrumbs />\n\n <PluginRoutes />\n </div>\n </main>\n </div>\n\n {/* Command palette */}\n <CommandPalette />\n </div>\n );\n}\n',
|
|
842
|
+
"src/app/renderer/styles/app.css": "body { margin: 0; }\n",
|
|
383
843
|
"src/app/renderer/styles/tailwind.css": '/* Tailwind v4, compiled by `fluxplugin dev|build` to dist/tailwind.css.\n Colors come from the theme variables, so Tailwind utilities follow light / dark / plugin themes automatically:\n use bg-surface, text-fg, border-border, text-muted, bg-accent \u2026 */\n@import "tailwindcss";\n\n/* Plugins are shipped as built JS: scan their dist so the classes they use are generated. Plugins that users install later\n should ship their own precompiled CSS (fluxplugin plugin new my-plugin --tailwindcss). */\n@source "../../../../plugins/*/dist";\n\n@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));\n\n@theme inline {\n --color-bg: var(--flux-bg);\n --color-fg: var(--flux-fg);\n --color-surface: var(--flux-surface);\n --color-border: var(--flux-border);\n --color-accent: var(--flux-accent);\n --color-muted: var(--flux-muted);\n}\n'
|
|
384
844
|
};
|
|
385
845
|
|
|
@@ -390,7 +850,16 @@ var pkg = (v) => JSON.stringify(
|
|
|
390
850
|
version: "1.0.0",
|
|
391
851
|
private: true,
|
|
392
852
|
type: "module",
|
|
393
|
-
scripts: {
|
|
853
|
+
scripts: {
|
|
854
|
+
dev: "fluxplugin dev",
|
|
855
|
+
"dev:host": "fluxplugin dev --host ../my-app # run it inside your app while you work (edit the path)",
|
|
856
|
+
build: "fluxplugin build",
|
|
857
|
+
package: "fluxplugin package",
|
|
858
|
+
validate: "fluxplugin validate",
|
|
859
|
+
release: "fluxplugin publish"
|
|
860
|
+
},
|
|
861
|
+
// Add any library here (react-markdown, chart.js, a UI kit…): it is bundled into dist/, the host app does not need it.
|
|
862
|
+
dependencies: {},
|
|
394
863
|
devDependencies: { fluxplugin: "^1.0.0", "@types/react": "^19.0.0", react: "^19.0.0", typescript: "^5.6.0" }
|
|
395
864
|
},
|
|
396
865
|
null,
|
|
@@ -658,6 +1127,28 @@ function withTailwind(files, v) {
|
|
|
658
1127
|
files["src/renderer.tsx"] = files["src/renderer.tsx"].split(`className="${v.slug}-card"`).join('className="rounded-xl border border-border bg-surface p-4"');
|
|
659
1128
|
return files;
|
|
660
1129
|
}
|
|
1130
|
+
function standaloneExtras(v) {
|
|
1131
|
+
return {
|
|
1132
|
+
".gitignore": "node_modules\ndist\n*.tgz\n.DS_Store\n",
|
|
1133
|
+
"CONTRIBUTING.md": `# Developing ${v.name}
|
|
1134
|
+
|
|
1135
|
+
This is a normal React + TypeScript project. It has its own \`package.json\` and its own libraries.
|
|
1136
|
+
|
|
1137
|
+
\`\`\`bash
|
|
1138
|
+
npm install # your libraries + fluxplugin tooling
|
|
1139
|
+
npm install chart.js # add anything you need: it is bundled into dist/, the app never needs it
|
|
1140
|
+
npm run dev # rebuilds on save
|
|
1141
|
+
npm run dev:host # same, and copies the result into a host app (edit the path) which hot-reloads it
|
|
1142
|
+
npm run build # dist/ \u2014 this is all that ships (no src/, no .ts, no sourcemaps)
|
|
1143
|
+
npm run package # ${v.id}-1.0.0.tgz
|
|
1144
|
+
npm run release # publish to the plugin store (GitHub)
|
|
1145
|
+
\`\`\`
|
|
1146
|
+
|
|
1147
|
+
**Shared with the host** (never bundled twice): \`react\`, \`react-dom\`, \`fluxplugin\`, \`fluxplugin/react\`. Everything else you import is yours.
|
|
1148
|
+
CSS imported in the renderer (including from libraries) is emitted as \`dist/renderer.css\` and loaded automatically; images and fonts are inlined.
|
|
1149
|
+
`
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
661
1152
|
|
|
662
1153
|
// src/cli/host-templates.ts
|
|
663
1154
|
function hostFiles(v, withSample = true) {
|
|
@@ -669,16 +1160,21 @@ function hostFiles(v, withSample = true) {
|
|
|
669
1160
|
private: true,
|
|
670
1161
|
type: "module",
|
|
671
1162
|
main: "dist/main.cjs",
|
|
1163
|
+
description: `${v.name} \u2014 built with fluxplugin`,
|
|
1164
|
+
author: "Your name",
|
|
672
1165
|
scripts: {
|
|
673
1166
|
dev: "fluxplugin dev",
|
|
674
1167
|
"dev:web": "fluxplugin dev --web",
|
|
675
1168
|
build: "fluxplugin build",
|
|
676
1169
|
start: "fluxplugin start",
|
|
1170
|
+
dist: "fluxplugin dist",
|
|
1171
|
+
"export:web": "fluxplugin export",
|
|
1172
|
+
"plugins:package": "fluxplugin package --all",
|
|
677
1173
|
"plugin:new": "fluxplugin plugin new",
|
|
678
1174
|
typecheck: "tsc --noEmit"
|
|
679
1175
|
},
|
|
680
1176
|
dependencies: { fluxplugin: v.fluxSpec, react: "^19.0.0", "react-dom": "^19.0.0" },
|
|
681
|
-
devDependencies: { electron: v.electron ?? "^44.5.1", typescript: "~5.9.3", "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", ...v.tailwind ? { tailwindcss: "^4.0.0", "@tailwindcss/cli": "^4.0.0" } : {} },
|
|
1177
|
+
devDependencies: { electron: v.electron ?? "^44.5.1", "electron-builder": "^26.0.0", typescript: "~5.9.3", "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", ...v.tailwind ? { tailwindcss: "^4.0.0", "@tailwindcss/cli": "^4.0.0" } : {} },
|
|
682
1178
|
// npm 11.16+/12 and pnpm 10 block dependency install scripts unless allowed; Electron needs its postinstall to download the binary.
|
|
683
1179
|
allowScripts: { electron: true, esbuild: true },
|
|
684
1180
|
pnpm: { onlyBuiltDependencies: ["electron", "esbuild"] }
|
|
@@ -687,7 +1183,7 @@ function hostFiles(v, withSample = true) {
|
|
|
687
1183
|
2
|
|
688
1184
|
),
|
|
689
1185
|
// "stores" is used by the CLI (fluxplugin search/install) AND by your app (Plugins → Available). Add your own store here.
|
|
690
|
-
"fluxplugin.config.json": JSON.stringify({ pluginsDir: "plugins", outDir: "dist", stores: [{ type: "github", topic: "fluxplugin-plugin" }] }, null, 2),
|
|
1186
|
+
"fluxplugin.config.json": JSON.stringify({ pluginsDir: "plugins", outDir: "dist", stores: v.stores ?? [{ type: "github", topic: "fluxplugin-plugin" }] }, null, 2),
|
|
691
1187
|
"tsconfig.json": JSON.stringify(
|
|
692
1188
|
{ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "Bundler", jsx: "react-jsx", strict: true, noEmit: true, skipLibCheck: true, resolveJsonModule: true, lib: ["ES2022", "DOM", "DOM.Iterable"], types: ["node"] }, include: ["src"] },
|
|
693
1189
|
null,
|
|
@@ -696,7 +1192,7 @@ function hostFiles(v, withSample = true) {
|
|
|
696
1192
|
".gitignore": "node_modules\ndist\n.flux-data\n*.tgz\n",
|
|
697
1193
|
"README.md": readme2(v.name)
|
|
698
1194
|
};
|
|
699
|
-
for (const [rel, content] of Object.entries(HOST_APP_FILES)) files[rel] = content.split("__APP_NAME__").join(v.name);
|
|
1195
|
+
for (const [rel, content] of Object.entries(HOST_APP_FILES)) files[rel] = content.split("__APP_NAME__").join(v.name).split("__APP_ID__").join(v.name.replace(/[^a-z0-9]/gi, ""));
|
|
700
1196
|
if (v.tailwind) for (const [rel, content] of Object.entries(HOST_TAILWIND_FILES)) files[rel] = content.split("__APP_NAME__").join(v.name);
|
|
701
1197
|
if (withSample) {
|
|
702
1198
|
const sample = TEMPLATES.dashboard(varsFromName("hello"));
|
|
@@ -914,6 +1410,136 @@ async function storeAdd(pkgFile, dir, baseUrl) {
|
|
|
914
1410
|
await fs6.writeFile(idxFile, JSON.stringify(idx, null, 2));
|
|
915
1411
|
return { id: m.id, version: m.version, sha256 };
|
|
916
1412
|
}
|
|
1413
|
+
async function storeBuild(dir, opts = {}) {
|
|
1414
|
+
const log2 = opts.log ?? console.log;
|
|
1415
|
+
const root = path7.resolve(dir);
|
|
1416
|
+
const pluginsRel = opts.pluginsDir ?? (await fs6.stat(path7.join(root, "plugins")).catch(() => null) ? "plugins" : ".");
|
|
1417
|
+
const base = path7.join(root, pluginsRel);
|
|
1418
|
+
const out = path7.resolve(opts.out ?? path7.join(root, INDEX));
|
|
1419
|
+
const plugins = [];
|
|
1420
|
+
const tmpRoot = await fs6.mkdtemp(path7.join((await import("node:os")).tmpdir(), "flux-store-"));
|
|
1421
|
+
try {
|
|
1422
|
+
for (const name of (await fs6.readdir(base)).sort()) {
|
|
1423
|
+
const folder = path7.join(base, name);
|
|
1424
|
+
let raw;
|
|
1425
|
+
try {
|
|
1426
|
+
raw = JSON.parse(await fs6.readFile(path7.join(folder, "plugin.json"), "utf8"));
|
|
1427
|
+
} catch {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
const v = validateManifest(raw);
|
|
1431
|
+
if (!v.manifest) {
|
|
1432
|
+
log2(`! skipped ${name}: ${v.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
const m = v.manifest;
|
|
1436
|
+
const stage = path7.join(tmpRoot, name);
|
|
1437
|
+
const files = (await copyPluginRuntime(folder, stage)).sort();
|
|
1438
|
+
const hashes = {};
|
|
1439
|
+
for (const f of files) hashes[f] = crypto2.createHash("sha256").update(await fs6.readFile(path7.join(stage, f))).digest("hex");
|
|
1440
|
+
const rel = (p) => p ? p.replace(/^\.\//, "") : void 0;
|
|
1441
|
+
const relFolder = path7.relative(root, folder).split(path7.sep).join("/") + "/";
|
|
1442
|
+
const readme3 = await fs6.readFile(path7.join(folder, rel(m.readme) ?? "README.md"), "utf8").then((s) => s.slice(0, 2e4), () => void 0);
|
|
1443
|
+
plugins.push({
|
|
1444
|
+
id: m.id,
|
|
1445
|
+
name: m.name,
|
|
1446
|
+
version: m.version,
|
|
1447
|
+
description: m.description,
|
|
1448
|
+
author: typeof m.author === "string" ? m.author : m.author?.name,
|
|
1449
|
+
permissions: m.permissions ?? [],
|
|
1450
|
+
folder: relFolder,
|
|
1451
|
+
files,
|
|
1452
|
+
hashes,
|
|
1453
|
+
homepage: m.homepage,
|
|
1454
|
+
license: m.license,
|
|
1455
|
+
icon: m.icon ? relFolder + rel(m.icon) : void 0,
|
|
1456
|
+
banner: m.banner ? relFolder + rel(m.banner) : void 0,
|
|
1457
|
+
screenshots: (m.screenshots ?? []).map((s) => relFolder + rel(s)),
|
|
1458
|
+
video: m.video?.startsWith("./") ? relFolder + rel(m.video) : m.video,
|
|
1459
|
+
categories: m.categories,
|
|
1460
|
+
keywords: m.keywords,
|
|
1461
|
+
links: m.links,
|
|
1462
|
+
readme: readme3
|
|
1463
|
+
});
|
|
1464
|
+
log2(` ${m.id}@${m.version} (${files.length} files)`);
|
|
1465
|
+
}
|
|
1466
|
+
} finally {
|
|
1467
|
+
await fs6.rm(tmpRoot, { recursive: true, force: true });
|
|
1468
|
+
}
|
|
1469
|
+
await fs6.writeFile(out, JSON.stringify({ plugins }, null, 2));
|
|
1470
|
+
return { count: plugins.length, file: out };
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/cli/dist.ts
|
|
1474
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1475
|
+
import { existsSync as existsSync3, promises as fs7 } from "node:fs";
|
|
1476
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
1477
|
+
import path8 from "node:path";
|
|
1478
|
+
async function pluginFolders(dir) {
|
|
1479
|
+
const out = [];
|
|
1480
|
+
for (const n of (await fs7.readdir(dir).catch(() => [])).sort()) if (existsSync3(path8.join(dir, n, "plugin.json"))) out.push(path8.join(dir, n));
|
|
1481
|
+
return out;
|
|
1482
|
+
}
|
|
1483
|
+
async function stagePlugins(cwd, outDir, log2 = console.log) {
|
|
1484
|
+
const cfg = await loadConfig({}, cwd);
|
|
1485
|
+
await fs7.rm(outDir, { recursive: true, force: true });
|
|
1486
|
+
const dirs = await pluginFolders(cfg.pluginsDir);
|
|
1487
|
+
for (const d of dirs) {
|
|
1488
|
+
const files = await copyPluginRuntime(d, path8.join(outDir, path8.basename(d)));
|
|
1489
|
+
log2(` ${path8.basename(d)}: ${files.length} runtime files`);
|
|
1490
|
+
}
|
|
1491
|
+
await fs7.mkdir(outDir, { recursive: true });
|
|
1492
|
+
return dirs.length;
|
|
1493
|
+
}
|
|
1494
|
+
async function distApp(cwd, opts = {}) {
|
|
1495
|
+
const log2 = opts.log ?? console.log;
|
|
1496
|
+
log2("1/3 building the app\u2026");
|
|
1497
|
+
await buildHost(cwd, { log: log2 });
|
|
1498
|
+
log2("2/3 staging plugins (runtime files only)\u2026");
|
|
1499
|
+
const n = await stagePlugins(cwd, path8.join(cwd, ".flux-build/plugins"), log2);
|
|
1500
|
+
log2(`3/3 packaging (${n} plugin(s) bundled)\u2026`);
|
|
1501
|
+
let cli;
|
|
1502
|
+
try {
|
|
1503
|
+
const pj = createRequire2(path8.join(cwd, "package.json")).resolve("electron-builder/package.json");
|
|
1504
|
+
const bin = JSON.parse(await fs7.readFile(pj, "utf8")).bin;
|
|
1505
|
+
cli = path8.join(path8.dirname(pj), typeof bin === "string" ? bin : bin?.["electron-builder"] ?? "cli.js");
|
|
1506
|
+
} catch {
|
|
1507
|
+
throw new Error('electron-builder is not installed: run "npm install -D electron-builder"');
|
|
1508
|
+
}
|
|
1509
|
+
const args = [cli, "--config", existsSync3(path8.join(cwd, "electron-builder.yml")) ? "electron-builder.yml" : "package.json"];
|
|
1510
|
+
if (opts.dir) args.push("--dir");
|
|
1511
|
+
for (const t of ["win", "mac", "linux"]) if (opts[t]) args.push(`--${t}`);
|
|
1512
|
+
return await new Promise((resolve) => {
|
|
1513
|
+
const child = spawn2(process.execPath, args, { cwd, stdio: "inherit", env: { ...process.env, CSC_IDENTITY_AUTO_DISCOVERY: process.env["CSC_IDENTITY_AUTO_DISCOVERY"] ?? "false" } });
|
|
1514
|
+
child.on("exit", (c) => resolve(c ?? 1));
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
async function exportWebsite(cwd, outDir, log2 = console.log) {
|
|
1518
|
+
await buildHost(cwd, { log: log2 });
|
|
1519
|
+
const out = path8.resolve(outDir);
|
|
1520
|
+
await fs7.rm(out, { recursive: true, force: true });
|
|
1521
|
+
await fs7.mkdir(out, { recursive: true });
|
|
1522
|
+
for (const f of ["index.html", "renderer.js", "renderer.css", "tailwind.css"]) if (existsSync3(path8.join(cwd, "dist", f))) await fs7.copyFile(path8.join(cwd, "dist", f), path8.join(out, f));
|
|
1523
|
+
const cfg = await loadConfig({}, cwd);
|
|
1524
|
+
let count = 0;
|
|
1525
|
+
for (const d of await pluginFolders(cfg.pluginsDir)) {
|
|
1526
|
+
const id = JSON.parse(await fs7.readFile(path8.join(d, "plugin.json"), "utf8")).id;
|
|
1527
|
+
await copyPluginRuntime(d, path8.join(out, "plugins", id));
|
|
1528
|
+
count++;
|
|
1529
|
+
}
|
|
1530
|
+
const tmp = await fs7.mkdtemp(path8.join((await import("node:os")).tmpdir(), "flux-export-"));
|
|
1531
|
+
try {
|
|
1532
|
+
const fw = new MainFramework({ pluginDirs: [path8.join(out, "plugins")], dataDir: tmp, assetUrl: (id, rel) => `./plugins/${id}/${rel.replace(/^\.\//, "")}` });
|
|
1533
|
+
await fw.manager.init();
|
|
1534
|
+
await fw.scan();
|
|
1535
|
+
await fs7.writeFile(path8.join(out, "bootstrap.json"), JSON.stringify(fw.bootstrap(), null, 2));
|
|
1536
|
+
} finally {
|
|
1537
|
+
await fs7.rm(tmp, { recursive: true, force: true });
|
|
1538
|
+
}
|
|
1539
|
+
await fs7.writeFile(path8.join(out, "README.txt"), "Static export. Serve this folder over HTTP(S) (e.g. `npx serve .`); opening index.html from disk will not work.\nOnly the browser side runs here: plugins that need main-process code are listed but their IPC calls fail.\n");
|
|
1540
|
+
log2(`exported ${count} plugin(s) \u2192 ${out}`);
|
|
1541
|
+
return { plugins: count, out };
|
|
1542
|
+
}
|
|
917
1543
|
|
|
918
1544
|
// src/cli/cli.ts
|
|
919
1545
|
var HELP = `fluxplugin <command>
|
|
@@ -923,26 +1549,31 @@ var HELP = `fluxplugin <command>
|
|
|
923
1549
|
dev [--web] run your app with plugin hot reload (Electron, or a browser with --web)
|
|
924
1550
|
build bundle your app (in an app folder) or a plugin (in a plugin folder)
|
|
925
1551
|
start [--web] run the built app
|
|
926
|
-
|
|
1552
|
+
dist [--win|--mac|--linux] [--unpacked] build the app into an installer/executable (electron-builder)
|
|
1553
|
+
export [--out website] extract the app as a static website
|
|
1554
|
+
plugin new <name> [-t basic|dashboard|backend] [--tailwindcss] [--standalone] scaffold a plugin (its own project when outside an app)
|
|
1555
|
+
dev --host <app> (in a plugin project) rebuild on save and sync into a host app that hot-reloads it
|
|
927
1556
|
validate [dir] check a plugin manifest, entries and integrity
|
|
928
1557
|
publish [dir] [--repo o/r] [--dry-run] publish a plugin to the store (GitHub release + topic)
|
|
929
1558
|
search [query] search the configured stores
|
|
930
1559
|
install <id[@version]|dir|.tgz> install a plugin from a store, a folder or a package
|
|
931
1560
|
store init <dir> create your own store (index.json) \u2014 host it anywhere
|
|
932
1561
|
store add <pkg.tgz> [--dir d] [--base-url url] add a package to your store
|
|
1562
|
+
store build [dir] [--out index.json] build the catalog JSON from plugin folders (repo layout)
|
|
1563
|
+
package --all [--out release/plugins] package every plugin of the app
|
|
933
1564
|
package [dir] [--out d] [--sign-key file] build + integrity (+ signature) + .tgz
|
|
934
1565
|
install <dir|.tgz> [--dir plugins] uninstall <id> enable <id> disable <id> list
|
|
935
1566
|
keygen [--out name] create an ed25519 signing key pair
|
|
936
1567
|
tailwind [--dir plugins] [--out file] generate @source lines for Tailwind v4`;
|
|
937
1568
|
var log = (...a) => console.log(...a);
|
|
938
1569
|
async function readManifest(root) {
|
|
939
|
-
return JSON.parse(await
|
|
1570
|
+
return JSON.parse(await fs8.readFile(path9.join(root, "plugin.json"), "utf8"));
|
|
940
1571
|
}
|
|
941
1572
|
async function run(argv) {
|
|
942
1573
|
const { values: f, positionals: pos } = parseArgs({
|
|
943
1574
|
args: argv,
|
|
944
1575
|
allowPositionals: true,
|
|
945
|
-
options: { template: { type: "string", short: "t" }, out: { type: "string" }, dir: { type: "string" }, data: { type: "string" }, "sign-key": { type: "string" }, help: { type: "boolean", short: "h" }, web: { type: "boolean" }, port: { type: "string" }, name: { type: "string" }, "no-sample": { type: "boolean" }, tailwindcss: { type: "boolean" }, repo: { type: "string" }, token: { type: "string" }, "dry-run": { type: "boolean" }, "base-url": { type: "string" } }
|
|
1576
|
+
options: { template: { type: "string", short: "t" }, out: { type: "string" }, dir: { type: "string" }, data: { type: "string" }, "sign-key": { type: "string" }, help: { type: "boolean", short: "h" }, web: { type: "boolean" }, port: { type: "string" }, name: { type: "string" }, "no-sample": { type: "boolean" }, tailwindcss: { type: "boolean" }, repo: { type: "string" }, token: { type: "string" }, "dry-run": { type: "boolean" }, "base-url": { type: "string" }, all: { type: "boolean" }, win: { type: "boolean" }, mac: { type: "boolean" }, linux: { type: "boolean" }, unpacked: { type: "boolean" }, store: { type: "string" }, host: { type: "string" }, standalone: { type: "boolean" } }
|
|
946
1577
|
});
|
|
947
1578
|
let [cmd, arg] = pos;
|
|
948
1579
|
if (cmd === "store") {
|
|
@@ -956,20 +1587,20 @@ async function run(argv) {
|
|
|
956
1587
|
arg = pos[2];
|
|
957
1588
|
}
|
|
958
1589
|
if (!cmd || f.help) return log(HELP), cmd ? 0 : 1;
|
|
959
|
-
const root =
|
|
1590
|
+
const root = path9.resolve(arg ?? ".");
|
|
960
1591
|
try {
|
|
961
1592
|
switch (cmd) {
|
|
962
1593
|
case "init": {
|
|
963
|
-
const target =
|
|
964
|
-
const entries = await
|
|
1594
|
+
const target = path9.resolve(arg ?? ".");
|
|
1595
|
+
const entries = await fs8.readdir(target).catch(() => []);
|
|
965
1596
|
if (entries.length) throw new Error(`${target} is not empty`);
|
|
966
|
-
const name = (f.name ??
|
|
967
|
-
const files = hostFiles({ name, fluxSpec: process.env["FLUXPLUGIN_SPEC"] ?? `^${await ownVersion()}`, electron: await latestElectron(), tailwind: !!f.tailwindcss }, !f["no-sample"]);
|
|
1597
|
+
const name = (f.name ?? path9.basename(target)).toLowerCase().replace(/[^a-z0-9-_.]+/g, "-");
|
|
1598
|
+
const files = hostFiles({ name, fluxSpec: process.env["FLUXPLUGIN_SPEC"] ?? `^${await ownVersion()}`, electron: await latestElectron(), tailwind: !!f.tailwindcss, stores: parseStoreSpec(f.store) }, !f["no-sample"]);
|
|
968
1599
|
for (const [rel2, content] of Object.entries(files)) {
|
|
969
|
-
await
|
|
970
|
-
await
|
|
1600
|
+
await fs8.mkdir(path9.dirname(path9.join(target, rel2)), { recursive: true });
|
|
1601
|
+
await fs8.writeFile(path9.join(target, rel2), content);
|
|
971
1602
|
}
|
|
972
|
-
const rel =
|
|
1603
|
+
const rel = path9.relative(process.cwd(), target) || ".";
|
|
973
1604
|
log(`\u2714 created ${name} in ${target}
|
|
974
1605
|
|
|
975
1606
|
Next steps:
|
|
@@ -985,17 +1616,19 @@ Add a plugin: npm run plugin:new my-plugin -- -t backend`);
|
|
|
985
1616
|
if (!arg) throw new Error("usage: fluxplugin plugin new <name> [-t basic|dashboard|backend]");
|
|
986
1617
|
const tpl = f.template ?? "basic";
|
|
987
1618
|
if (!(tpl in TEMPLATES)) throw new Error(`unknown template "${tpl}"`);
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
const
|
|
1619
|
+
const inHost = await isHostProject(process.cwd());
|
|
1620
|
+
const standalone = !!f.standalone || !inHost;
|
|
1621
|
+
const base = inHost && !f.standalone && !path9.isAbsolute(arg) && !arg.includes("/") && !arg.includes("\\") ? path9.join((await loadConfig({}, process.cwd())).pluginsDir, arg) : path9.resolve(arg);
|
|
1622
|
+
if (await fs8.stat(base).catch(() => null)) throw new Error(`${base} already exists`);
|
|
1623
|
+
const vars = varsFromName(path9.basename(base));
|
|
991
1624
|
let files = TEMPLATES[tpl](vars);
|
|
992
1625
|
if (f.tailwindcss) files = withTailwind(files, vars);
|
|
1626
|
+
if (standalone) files = { ...files, ...standaloneExtras(vars) };
|
|
993
1627
|
for (const [rel, content] of Object.entries(files)) {
|
|
994
|
-
await
|
|
995
|
-
await
|
|
1628
|
+
await fs8.mkdir(path9.dirname(path9.join(base, rel)), { recursive: true });
|
|
1629
|
+
await fs8.writeFile(path9.join(base, rel), content);
|
|
996
1630
|
}
|
|
997
|
-
log(`\u2714 created ${tpl} plugin in ${base}
|
|
998
|
-
next: fluxplugin build ${path8.relative(process.cwd(), base)}`);
|
|
1631
|
+
log(`\u2714 created ${tpl} plugin in ${base}${standalone ? "\n\n cd " + path9.relative(process.cwd(), base) + "\n npm install\n npm run dev:host # run it inside your app while you work (edit the path in package.json)" : "\n next: fluxplugin build " + path9.relative(process.cwd(), base)}`);
|
|
999
1632
|
return 0;
|
|
1000
1633
|
}
|
|
1001
1634
|
case "build": {
|
|
@@ -1010,6 +1643,7 @@ Add a plugin: npm run plugin:new my-plugin -- -t backend`);
|
|
|
1010
1643
|
const cwd = process.cwd();
|
|
1011
1644
|
if (arg || !await isHostProject(cwd)) {
|
|
1012
1645
|
await buildPlugin(root, true);
|
|
1646
|
+
if (f.host) await linkPluginToHost(root, path9.resolve(f.host));
|
|
1013
1647
|
await new Promise(() => {
|
|
1014
1648
|
});
|
|
1015
1649
|
}
|
|
@@ -1027,7 +1661,7 @@ Add a plugin: npm run plugin:new my-plugin -- -t backend`);
|
|
|
1027
1661
|
}
|
|
1028
1662
|
case "start": {
|
|
1029
1663
|
const cwd = process.cwd();
|
|
1030
|
-
if (!await
|
|
1664
|
+
if (!await fs8.stat(path9.join(cwd, "dist/index.html")).catch(() => null)) throw new Error("nothing built yet: run `fluxplugin build` first");
|
|
1031
1665
|
const { child } = startApp(cwd, { web: f.web, port: f.port ? Number(f.port) : void 0 });
|
|
1032
1666
|
return await new Promise((r) => child.on("exit", (c) => r(c ?? 0)));
|
|
1033
1667
|
}
|
|
@@ -1035,8 +1669,32 @@ Add a plugin: npm run plugin:new my-plugin -- -t backend`);
|
|
|
1035
1669
|
return await doctor(process.cwd());
|
|
1036
1670
|
case "validate":
|
|
1037
1671
|
return await validate(root);
|
|
1038
|
-
case "package":
|
|
1039
|
-
|
|
1672
|
+
case "package": {
|
|
1673
|
+
if (f.all) {
|
|
1674
|
+
const cfg = await loadConfig(f);
|
|
1675
|
+
let n = 0;
|
|
1676
|
+
for (const d of (await fs8.readdir(cfg.pluginsDir).catch(() => [])).sort()) {
|
|
1677
|
+
if (!await fs8.stat(path9.join(cfg.pluginsDir, d, "plugin.json")).catch(() => null)) continue;
|
|
1678
|
+
await packagePlugin(path9.join(cfg.pluginsDir, d), { out: f.out ?? "release/plugins", signKey: f["sign-key"] });
|
|
1679
|
+
n++;
|
|
1680
|
+
}
|
|
1681
|
+
log(`\u2714 packaged ${n} plugin(s) in ${f.out ?? "release/plugins"}`);
|
|
1682
|
+
return 0;
|
|
1683
|
+
}
|
|
1684
|
+
await packagePlugin(root, { out: f.out, signKey: f["sign-key"] });
|
|
1685
|
+
return 0;
|
|
1686
|
+
}
|
|
1687
|
+
case "dist":
|
|
1688
|
+
return await distApp(process.cwd(), { win: f.win, mac: f.mac, linux: f.linux, dir: f.unpacked });
|
|
1689
|
+
case "export":
|
|
1690
|
+
await exportWebsite(process.cwd(), f.out ?? "website");
|
|
1691
|
+
return 0;
|
|
1692
|
+
case "store:build": {
|
|
1693
|
+
const r = await storeBuild(path9.resolve(arg ?? "."), { out: f.out });
|
|
1694
|
+
log(`\u2714 catalog with ${r.count} plugin(s) written to ${r.file}
|
|
1695
|
+
commit it with the plugin folders; point a store at the repo: { "type": "github", "repo": "owner/name" }`);
|
|
1696
|
+
return 0;
|
|
1697
|
+
}
|
|
1040
1698
|
case "publish":
|
|
1041
1699
|
await publishPlugin(root, { repo: f.repo, token: f.token, signKey: f["sign-key"], dryRun: f["dry-run"] });
|
|
1042
1700
|
return 0;
|
|
@@ -1053,31 +1711,31 @@ Add a plugin: npm run plugin:new my-plugin -- -t backend`);
|
|
|
1053
1711
|
return 0;
|
|
1054
1712
|
}
|
|
1055
1713
|
case "store:init":
|
|
1056
|
-
await storeInit(
|
|
1057
|
-
log(`\u2714 store created in ${
|
|
1714
|
+
await storeInit(path9.resolve(arg ?? "store"));
|
|
1715
|
+
log(`\u2714 store created in ${path9.resolve(arg ?? "store")} (see its README.md)`);
|
|
1058
1716
|
return 0;
|
|
1059
1717
|
case "store:add": {
|
|
1060
1718
|
if (!arg) throw new Error("usage: store add <package.tgz> [--dir store] [--base-url https://\u2026]");
|
|
1061
|
-
const r = await storeAdd(
|
|
1062
|
-
log(`\u2714 added ${r.id}@${r.version} to ${
|
|
1719
|
+
const r = await storeAdd(path9.resolve(arg), path9.resolve(f.dir ?? "store"), f["base-url"]);
|
|
1720
|
+
log(`\u2714 added ${r.id}@${r.version} to ${path9.resolve(f.dir ?? "store")}/index.json`);
|
|
1063
1721
|
return 0;
|
|
1064
1722
|
}
|
|
1065
1723
|
case "keygen": {
|
|
1066
1724
|
const k = generateSigningKeyPair();
|
|
1067
1725
|
const base = f.out ?? "flux-signing";
|
|
1068
|
-
await
|
|
1069
|
-
await
|
|
1726
|
+
await fs8.writeFile(`${base}.private.key`, k.privateKey, { mode: 384 });
|
|
1727
|
+
await fs8.writeFile(`${base}.public.key`, k.publicKey);
|
|
1070
1728
|
log(`wrote ${base}.private.key (keep secret) and ${base}.public.key (distribute to hosts as a trusted key)`);
|
|
1071
1729
|
return 0;
|
|
1072
1730
|
}
|
|
1073
1731
|
case "tailwind": {
|
|
1074
1732
|
const cfg = await loadConfig(f);
|
|
1075
|
-
const names = (await
|
|
1076
|
-
const lines = names.map((n) => `@source "${
|
|
1733
|
+
const names = (await fs8.readdir(cfg.pluginsDir).catch(() => [])).sort();
|
|
1734
|
+
const lines = names.map((n) => `@source "${path9.join(cfg.pluginsDir, n, "src").replace(/\\/g, "/")}";`);
|
|
1077
1735
|
const css = `/* generated by fluxplugin tailwind */
|
|
1078
1736
|
${lines.join("\n")}
|
|
1079
1737
|
`;
|
|
1080
|
-
if (f.out) await
|
|
1738
|
+
if (f.out) await fs8.writeFile(f.out, css);
|
|
1081
1739
|
else log(css);
|
|
1082
1740
|
return 0;
|
|
1083
1741
|
}
|
|
@@ -1104,7 +1762,7 @@ async function validate(root) {
|
|
|
1104
1762
|
v.warnings.forEach((w) => log(`! ${w}`));
|
|
1105
1763
|
if (v.manifest) {
|
|
1106
1764
|
for (const rel of [v.manifest.main, v.manifest.renderer, ...v.manifest.styles ?? []].filter(Boolean))
|
|
1107
|
-
if (!await
|
|
1765
|
+
if (!await fs8.stat(path9.join(root, rel)).catch(() => null)) log(`\u2716 missing file ${rel} (run build)`), bad = true;
|
|
1108
1766
|
try {
|
|
1109
1767
|
await verifyPlugin(root, raw, v.manifest);
|
|
1110
1768
|
} catch (e) {
|
|
@@ -1117,17 +1775,17 @@ async function validate(root) {
|
|
|
1117
1775
|
}
|
|
1118
1776
|
async function manage(cmd, arg, f) {
|
|
1119
1777
|
const cfg = await loadConfig(f);
|
|
1120
|
-
const state = new JsonStateFile(
|
|
1121
|
-
const inst = new PluginInstaller(cfg.pluginsDir,
|
|
1778
|
+
const state = new JsonStateFile(path9.join(cfg.dataDir, "plugins-state.json"));
|
|
1779
|
+
const inst = new PluginInstaller(cfg.pluginsDir, path9.join(cfg.dataDir, "backups"));
|
|
1122
1780
|
if (cmd === "list") {
|
|
1123
1781
|
const { disabled } = await state.readAll();
|
|
1124
1782
|
for (const m of await inst.installed()) log(`${disabled.includes(m.id) ? "\u25CB" : "\u25CF"} ${m.id}@${m.version} ${m.name}`);
|
|
1125
1783
|
return 0;
|
|
1126
1784
|
}
|
|
1127
1785
|
if (!arg) throw new Error(`usage: ${cmd} <${cmd === "install" ? "dir|.tgz" : "id"}>`);
|
|
1128
|
-
if (cmd === "install" && arg && !arg.endsWith(".tgz") && !await
|
|
1786
|
+
if (cmd === "install" && arg && !arg.endsWith(".tgz") && !await fs8.stat(arg).catch(() => null)) {
|
|
1129
1787
|
const [id, version] = arg.split("@");
|
|
1130
|
-
const store = new PluginInstaller(cfg.pluginsDir,
|
|
1788
|
+
const store = new PluginInstaller(cfg.pluginsDir, path9.join(cfg.dataDir, "backups"), createRegistries(cfg.stores));
|
|
1131
1789
|
const found = (await store.search(id)).filter((e) => e.id === id && (!version || e.version === version));
|
|
1132
1790
|
if (!found.length) throw new Error(`"${arg}" was not found in your stores (${cfg.stores.map((s) => s.type).join(", ")}). Try: fluxplugin search ${id}`);
|
|
1133
1791
|
const best = found.sort((a, b) => b.version.localeCompare(a.version, void 0, { numeric: true }))[0];
|
|
@@ -1137,16 +1795,16 @@ async function manage(cmd, arg, f) {
|
|
|
1137
1795
|
return 0;
|
|
1138
1796
|
}
|
|
1139
1797
|
if (cmd === "install") {
|
|
1140
|
-
const tmp =
|
|
1141
|
-
if (arg.endsWith(".tgz")) await extractTarGz(await
|
|
1142
|
-
else await copyDir(
|
|
1798
|
+
const tmp = path9.join(cfg.dataDir, ".install", String(Date.now()));
|
|
1799
|
+
if (arg.endsWith(".tgz")) await extractTarGz(await fs8.readFile(arg), tmp);
|
|
1800
|
+
else await copyDir(path9.resolve(arg), tmp);
|
|
1143
1801
|
const v = validateManifest(await readManifest(tmp));
|
|
1144
1802
|
if (!v.manifest) throw new Error("invalid plugin: " + v.issues.map((i) => i.message).join("; "));
|
|
1145
|
-
const reg = new LocalRegistry(
|
|
1803
|
+
const reg = new LocalRegistry(path9.dirname(tmp), "cli");
|
|
1146
1804
|
inst.addRegistry(reg);
|
|
1147
1805
|
const [entry] = (await reg.search(v.manifest.id)).filter((e) => e.id === v.manifest.id);
|
|
1148
1806
|
const r = await inst.install({ ...entry, location: tmp });
|
|
1149
|
-
await
|
|
1807
|
+
await fs8.rm(tmp, { recursive: true, force: true });
|
|
1150
1808
|
log(`installed ${r.id}@${r.version}${r.previousVersion ? ` (was ${r.previousVersion}; rollback available)` : ""}`);
|
|
1151
1809
|
} else if (cmd === "uninstall") {
|
|
1152
1810
|
await inst.uninstall(arg);
|
|
@@ -1160,7 +1818,7 @@ async function manage(cmd, arg, f) {
|
|
|
1160
1818
|
async function ownVersion() {
|
|
1161
1819
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
1162
1820
|
try {
|
|
1163
|
-
const j = JSON.parse(await
|
|
1821
|
+
const j = JSON.parse(await fs8.readFile(new URL(rel, import.meta.url), "utf8"));
|
|
1164
1822
|
if (j.name === "fluxplugin" && j.version) return j.version;
|
|
1165
1823
|
} catch {
|
|
1166
1824
|
}
|
|
@@ -1186,23 +1844,30 @@ async function doctor(cwd) {
|
|
|
1186
1844
|
return bad ? 1 : 0;
|
|
1187
1845
|
}
|
|
1188
1846
|
const e = ensureElectron(cwd, log);
|
|
1189
|
-
e.bin ? ok(`Electron binary: ${
|
|
1847
|
+
e.bin ? ok(`Electron binary: ${path9.relative(cwd, e.bin) || e.bin}`) : ko(`Electron: ${e.error}
|
|
1190
1848
|
fix: npm install -D electron`);
|
|
1191
1849
|
try {
|
|
1192
1850
|
(await import("esbuild")).version ? ok("esbuild") : ko("esbuild");
|
|
1193
1851
|
} catch {
|
|
1194
1852
|
ko("esbuild is not available");
|
|
1195
1853
|
}
|
|
1196
|
-
const hasTw = await
|
|
1854
|
+
const hasTw = await fs8.stat(path9.join(cwd, "src/app/renderer/styles/tailwind.css")).then(() => true, () => false);
|
|
1197
1855
|
if (hasTw) {
|
|
1198
1856
|
const { resolveTailwindCli } = await import("./chunks/tailwind-TJQVTBG5.js");
|
|
1199
1857
|
resolveTailwindCli([cwd]) ? ok("Tailwind CLI") : ko("Tailwind CLI missing: npm install -D tailwindcss @tailwindcss/cli");
|
|
1200
1858
|
}
|
|
1201
|
-
const pkg2 = JSON.parse(await
|
|
1859
|
+
const pkg2 = JSON.parse(await fs8.readFile(path9.join(cwd, "package.json"), "utf8").catch(() => "{}"));
|
|
1202
1860
|
if (!pkg2.allowScripts?.["electron"]) log(`! package.json has no "allowScripts": {"electron": true}. npm 12 blocks Electron's postinstall without it (fluxplugin downloads the binary for you, but add it for clean installs).`);
|
|
1203
1861
|
log(bad ? "\nSome checks failed." : "\nAll good.");
|
|
1204
1862
|
return bad ? 1 : 0;
|
|
1205
1863
|
}
|
|
1864
|
+
function parseStoreSpec(spec) {
|
|
1865
|
+
if (!spec) return void 0;
|
|
1866
|
+
const gh = /^github:([\w.-]+\/[\w.-]+)$/.exec(spec);
|
|
1867
|
+
if (gh) return [{ type: "github", repo: gh[1] }];
|
|
1868
|
+
if (/^https:\/\/\S+$/.test(spec)) return [spec.toLowerCase().split("?")[0].endsWith(".json") ? { type: "json", url: spec } : { type: "http", url: spec }];
|
|
1869
|
+
throw new Error('--store must be "github:owner/repo" or an https URL (a .json link, or a folder URL ending with /)');
|
|
1870
|
+
}
|
|
1206
1871
|
export {
|
|
1207
1872
|
SHARED_MODULES,
|
|
1208
1873
|
TEMPLATES,
|
|
@@ -1212,6 +1877,7 @@ export {
|
|
|
1212
1877
|
isHostProject,
|
|
1213
1878
|
run,
|
|
1214
1879
|
sharedModulesPlugin,
|
|
1880
|
+
standaloneExtras,
|
|
1215
1881
|
startApp,
|
|
1216
1882
|
varsFromName,
|
|
1217
1883
|
withTailwind
|