fluxplugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +50 -0
  3. package/dist/browser/chunks/chunk-Q75AOCXD.js +2544 -0
  4. package/dist/browser/chunks/chunk-ZZ25CRMR.js +759 -0
  5. package/dist/browser/index.js +138 -0
  6. package/dist/browser/manager-ui.js +165 -0
  7. package/dist/browser/react.js +73 -0
  8. package/dist/cli.js +1635 -0
  9. package/dist/node/chunks/chunk-BZ7NZF7I.js +990 -0
  10. package/dist/node/chunks/chunk-XPLGAEBH.js +2623 -0
  11. package/dist/node/cli-api.js +843 -0
  12. package/dist/node/index.js +138 -0
  13. package/dist/node/main.js +61 -0
  14. package/dist/node/preload.js +56 -0
  15. package/dist/types/cli/bin.d.ts +1 -0
  16. package/dist/types/cli/build.d.ts +7 -0
  17. package/dist/types/cli/cli.d.ts +2 -0
  18. package/dist/types/cli/config.d.ts +9 -0
  19. package/dist/types/cli/host-templates.d.ts +6 -0
  20. package/dist/types/cli/host.d.ts +16 -0
  21. package/dist/types/cli/index.d.ts +6 -0
  22. package/dist/types/cli/shared-modules.d.ts +8 -0
  23. package/dist/types/cli/templates.d.ts +10 -0
  24. package/dist/types/core/api.d.ts +11 -0
  25. package/dist/types/core/common.d.ts +69 -0
  26. package/dist/types/core/deps.d.ts +34 -0
  27. package/dist/types/core/events.d.ts +58 -0
  28. package/dist/types/core/hooks.d.ts +55 -0
  29. package/dist/types/core/index.d.ts +18 -0
  30. package/dist/types/core/ipc.d.ts +148 -0
  31. package/dist/types/core/manager.d.ts +155 -0
  32. package/dist/types/core/manifest.d.ts +88 -0
  33. package/dist/types/core/permissions.d.ts +45 -0
  34. package/dist/types/core/plugin.d.ts +214 -0
  35. package/dist/types/core/protocol.d.ts +86 -0
  36. package/dist/types/core/registry.d.ts +88 -0
  37. package/dist/types/core/routes.d.ts +105 -0
  38. package/dist/types/core/schema.d.ts +99 -0
  39. package/dist/types/core/semver.d.ts +16 -0
  40. package/dist/types/core/slots.d.ts +75 -0
  41. package/dist/types/core/storage.d.ts +55 -0
  42. package/dist/types/core/styles.d.ts +34 -0
  43. package/dist/types/main/adapters.d.ts +81 -0
  44. package/dist/types/main/discover.d.ts +21 -0
  45. package/dist/types/main/framework.d.ts +78 -0
  46. package/dist/types/main/fs-storage.d.ts +33 -0
  47. package/dist/types/main/index.d.ts +8 -0
  48. package/dist/types/main/registry.d.ts +75 -0
  49. package/dist/types/main/security.d.ts +24 -0
  50. package/dist/types/main/serve-web.d.ts +14 -0
  51. package/dist/types/main/tar.d.ts +10 -0
  52. package/dist/types/manager-ui/index.d.ts +7 -0
  53. package/dist/types/preload/index.d.ts +27 -0
  54. package/dist/types/react/base-css.d.ts +2 -0
  55. package/dist/types/react/components.d.ts +89 -0
  56. package/dist/types/react/http-bridge.d.ts +10 -0
  57. package/dist/types/react/index.d.ts +6 -0
  58. package/dist/types/react/palette.d.ts +10 -0
  59. package/dist/types/react/router.d.ts +47 -0
  60. package/dist/types/react/runtime.d.ts +99 -0
  61. package/docs/ARCHITECTURE.md +31 -0
  62. package/docs/PLUGIN-AUTHORS.md +11 -0
  63. package/docs/RELEASE.md +20 -0
  64. package/docs/SECURITY.md +18 -0
  65. package/docs/screenshots/command-palette.png +0 -0
  66. package/docs/screenshots/dashboard.png +0 -0
  67. package/docs/screenshots/plugin-details.png +0 -0
  68. package/docs/screenshots/plugin-manager.png +0 -0
  69. package/docs/screenshots/stats.png +0 -0
  70. package/package.json +112 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1635 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);
3
+
4
+ // src/cli/cli.ts
5
+ import { parseArgs } from "node:util";
6
+ import { promises as fs8 } from "node:fs";
7
+ import path9 from "node:path";
8
+ import crypto2 from "node:crypto";
9
+
10
+ // src/core/schema.ts
11
+ var ok = (value) => ({ ok: true, value });
12
+ var fail = (path10, message) => ({ ok: false, issues: [{ path: path10, message }] });
13
+ var join = (p, k) => p ? typeof k === "number" ? `${p}[${k}]` : `${p}.${k}` : String(k);
14
+ function make(kind, parse, describe) {
15
+ const self = {
16
+ kind,
17
+ parse: (input, path10 = "") => parse(input, path10),
18
+ describe,
19
+ optional() {
20
+ return make(
21
+ kind,
22
+ (i, p) => i === void 0 ? ok(void 0) : parse(i, p),
23
+ () => ({ ...describe(), optional: true })
24
+ );
25
+ },
26
+ default(value) {
27
+ return make(
28
+ kind,
29
+ (i, p) => i === void 0 ? ok(value) : parse(i, p),
30
+ () => ({ ...describe(), default: value, optional: true })
31
+ );
32
+ }
33
+ };
34
+ return self;
35
+ }
36
+ var s = {
37
+ string(o = {}) {
38
+ return make(
39
+ "string",
40
+ (i, p) => {
41
+ if (typeof i !== "string") return fail(p, "expected string");
42
+ if (o.min !== void 0 && i.length < o.min) return fail(p, `must have at least ${o.min} characters`);
43
+ if (o.max !== void 0 && i.length > o.max) return fail(p, `must have at most ${o.max} characters`);
44
+ if (o.pattern && !o.pattern.test(i)) return fail(p, `does not match ${o.pattern}`);
45
+ return ok(i);
46
+ },
47
+ () => ({ type: "string", title: o.title, description: o.description, min: o.min, max: o.max })
48
+ );
49
+ },
50
+ number(o = {}) {
51
+ return make(
52
+ "number",
53
+ (i, p) => {
54
+ if (typeof i !== "number" || Number.isNaN(i)) return fail(p, "expected number");
55
+ if (o.int && !Number.isInteger(i)) return fail(p, "expected integer");
56
+ if (o.min !== void 0 && i < o.min) return fail(p, `must be >= ${o.min}`);
57
+ if (o.max !== void 0 && i > o.max) return fail(p, `must be <= ${o.max}`);
58
+ return ok(i);
59
+ },
60
+ () => ({ type: "number", title: o.title, description: o.description, min: o.min, max: o.max })
61
+ );
62
+ },
63
+ boolean(o = {}) {
64
+ return make(
65
+ "boolean",
66
+ (i, p) => typeof i === "boolean" ? ok(i) : fail(p, "expected boolean"),
67
+ () => ({ type: "boolean", ...o })
68
+ );
69
+ },
70
+ unknown() {
71
+ return make(
72
+ "unknown",
73
+ (i) => ok(i),
74
+ () => ({ type: "unknown" })
75
+ );
76
+ },
77
+ literal(v) {
78
+ return make(
79
+ "literal",
80
+ (i, p) => i === v ? ok(v) : fail(p, `expected ${JSON.stringify(v)}`),
81
+ () => ({ type: "literal", enum: [v] })
82
+ );
83
+ },
84
+ enum(values, o = {}) {
85
+ return make(
86
+ "enum",
87
+ (i, p) => typeof i === "string" && values.includes(i) ? ok(i) : fail(p, `expected one of ${values.join(", ")}`),
88
+ () => ({ type: "enum", enum: values, ...o })
89
+ );
90
+ },
91
+ array(item, o = {}) {
92
+ return make(
93
+ "array",
94
+ (i, p) => {
95
+ if (!Array.isArray(i)) return fail(p, "expected array");
96
+ if (o.min !== void 0 && i.length < o.min) return fail(p, `must have at least ${o.min} items`);
97
+ if (o.max !== void 0 && i.length > o.max) return fail(p, `must have at most ${o.max} items`);
98
+ const out = [];
99
+ const issues = [];
100
+ i.forEach((el, idx) => {
101
+ const r = item.parse(el, join(p, idx));
102
+ if (r.ok) out.push(r.value);
103
+ else issues.push(...r.issues);
104
+ });
105
+ return issues.length ? { ok: false, issues } : ok(out);
106
+ },
107
+ () => ({ type: "array", items: item.describe() })
108
+ );
109
+ },
110
+ record(value) {
111
+ return make(
112
+ "record",
113
+ (i, p) => {
114
+ if (typeof i !== "object" || i === null || Array.isArray(i)) return fail(p, "expected object");
115
+ const out = {};
116
+ const issues = [];
117
+ for (const [k, v] of Object.entries(i)) {
118
+ const r = value.parse(v, join(p, k));
119
+ if (r.ok) out[k] = r.value;
120
+ else issues.push(...r.issues);
121
+ }
122
+ return issues.length ? { ok: false, issues } : ok(out);
123
+ },
124
+ () => ({ type: "record", items: value.describe() })
125
+ );
126
+ },
127
+ object(shape, o = {}) {
128
+ return make(
129
+ "object",
130
+ (i, p) => {
131
+ if (typeof i !== "object" || i === null || Array.isArray(i)) return fail(p, "expected object");
132
+ const src = i;
133
+ const out = {};
134
+ const issues = [];
135
+ for (const [k, sch] of Object.entries(shape)) {
136
+ const r = sch.parse(src[k], join(p, k));
137
+ if (r.ok) {
138
+ if (r.value !== void 0) out[k] = r.value;
139
+ } else issues.push(...r.issues);
140
+ }
141
+ if (o.strict) {
142
+ for (const k of Object.keys(src)) if (!(k in shape)) issues.push({ path: join(p, k), message: "unknown property" });
143
+ }
144
+ return issues.length ? { ok: false, issues } : ok(out);
145
+ },
146
+ () => {
147
+ const properties = {};
148
+ const required = [];
149
+ for (const [k, sch] of Object.entries(shape)) {
150
+ const d = sch.describe();
151
+ properties[k] = d;
152
+ if (!d.optional) required.push(k);
153
+ }
154
+ return { type: "object", title: o.title, description: o.description, properties, required };
155
+ }
156
+ );
157
+ },
158
+ /** Accepts a value if any of the schemas matches. */
159
+ union(...options) {
160
+ return make(
161
+ "union",
162
+ (i, p) => {
163
+ for (const opt of options) {
164
+ const r = opt.parse(i, p);
165
+ if (r.ok) return r;
166
+ }
167
+ return fail(p, "does not match any allowed shape");
168
+ },
169
+ () => ({ type: "union" })
170
+ );
171
+ },
172
+ /** Escape hatch for values that cannot be validated structurally (functions, React components...). */
173
+ custom(check, message = "invalid value") {
174
+ return make(
175
+ "custom",
176
+ (i, p) => check(i) ? ok(i) : fail(p, message),
177
+ () => ({ type: "custom" })
178
+ );
179
+ },
180
+ fn() {
181
+ return s.custom((v) => typeof v === "function", "expected function");
182
+ }
183
+ };
184
+
185
+ // src/core/semver.ts
186
+ var SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
187
+ function parseVersion(v) {
188
+ const m = SEMVER_RE.exec(v.trim());
189
+ if (!m) return null;
190
+ return {
191
+ major: Number(m[1]),
192
+ minor: Number(m[2]),
193
+ patch: Number(m[3]),
194
+ prerelease: m[4] ? m[4].split(".") : []
195
+ };
196
+ }
197
+ function isValidVersion(v) {
198
+ return parseVersion(v) !== null;
199
+ }
200
+ function cmpIdent(a, b) {
201
+ const an = /^\d+$/.test(a);
202
+ const bn = /^\d+$/.test(b);
203
+ if (an && bn) return Math.sign(Number(a) - Number(b));
204
+ if (an) return -1;
205
+ if (bn) return 1;
206
+ return a < b ? -1 : a > b ? 1 : 0;
207
+ }
208
+ function compareVersions(a, b) {
209
+ const x = parseVersion(a);
210
+ const y = parseVersion(b);
211
+ if (!x || !y) throw new Error(`Invalid version: ${!x ? a : b}`);
212
+ for (const k of ["major", "minor", "patch"]) {
213
+ if (x[k] !== y[k]) return x[k] < y[k] ? -1 : 1;
214
+ }
215
+ if (!x.prerelease.length && !y.prerelease.length) return 0;
216
+ if (!x.prerelease.length) return 1;
217
+ if (!y.prerelease.length) return -1;
218
+ const len = Math.max(x.prerelease.length, y.prerelease.length);
219
+ for (let i = 0; i < len; i++) {
220
+ const p = x.prerelease[i];
221
+ const q = y.prerelease[i];
222
+ if (p === void 0) return -1;
223
+ if (q === void 0) return 1;
224
+ const c = cmpIdent(p, q);
225
+ if (c !== 0) return c;
226
+ }
227
+ return 0;
228
+ }
229
+ function parsePartial(token) {
230
+ const m = /^v?(\d+|x|X|\*)(?:\.(\d+|x|X|\*))?(?:\.(\d+|x|X|\*))?(?:-([0-9A-Za-z.-]+))?$/.exec(token);
231
+ if (!m) return null;
232
+ const num = (x) => x === void 0 || /^[xX*]$/.test(x) ? void 0 : Number(x);
233
+ const major = num(m[1]);
234
+ if (major === void 0) return { major: -1 };
235
+ return { major, minor: num(m[2]), patch: num(m[3]), pre: m[4] };
236
+ }
237
+ var ver = (a, b, c, pre) => `${a}.${b}.${c}${pre ? `-${pre}` : ""}`;
238
+ function expand(token) {
239
+ token = token.trim();
240
+ if (token === "" || token === "*" || token === "x" || token === "X") return [{ op: ">=", version: "0.0.0" }];
241
+ const prefix = /^(\^|~|>=|<=|>|<|=)?\s*(.+)$/.exec(token);
242
+ if (!prefix) return null;
243
+ const sym = prefix[1] ?? "";
244
+ const p = parsePartial(prefix[2]);
245
+ if (!p) return null;
246
+ if (p.major === -1) return [{ op: ">=", version: "0.0.0" }];
247
+ const { major, minor, patch, pre } = p;
248
+ const lo = ver(major, minor ?? 0, patch ?? 0, pre);
249
+ switch (sym) {
250
+ case "^": {
251
+ let hi;
252
+ if (major > 0 || minor === void 0) hi = ver(major + 1, 0, 0);
253
+ else if (minor > 0 || patch === void 0) hi = ver(0, minor + 1, 0);
254
+ else hi = ver(0, 0, patch + 1);
255
+ return [
256
+ { op: ">=", version: lo },
257
+ { op: "<", version: hi }
258
+ ];
259
+ }
260
+ case "~": {
261
+ const hi = minor === void 0 ? ver(major + 1, 0, 0) : ver(major, minor + 1, 0);
262
+ return [
263
+ { op: ">=", version: lo },
264
+ { op: "<", version: hi }
265
+ ];
266
+ }
267
+ case ">=":
268
+ return [{ op: ">=", version: lo }];
269
+ case ">":
270
+ if (minor === void 0) return [{ op: ">=", version: ver(major + 1, 0, 0) }];
271
+ if (patch === void 0) return [{ op: ">=", version: ver(major, minor + 1, 0) }];
272
+ return [{ op: ">", version: lo }];
273
+ case "<=":
274
+ if (minor === void 0) return [{ op: "<", version: ver(major + 1, 0, 0) }];
275
+ if (patch === void 0) return [{ op: "<", version: ver(major, minor + 1, 0) }];
276
+ return [{ op: "<=", version: lo }];
277
+ case "<":
278
+ return [{ op: "<", version: lo }];
279
+ default: {
280
+ if (minor === void 0)
281
+ return [
282
+ { op: ">=", version: ver(major, 0, 0) },
283
+ { op: "<", version: ver(major + 1, 0, 0) }
284
+ ];
285
+ if (patch === void 0)
286
+ return [
287
+ { op: ">=", version: ver(major, minor, 0) },
288
+ { op: "<", version: ver(major, minor + 1, 0) }
289
+ ];
290
+ return [{ op: "=", version: lo }];
291
+ }
292
+ }
293
+ }
294
+ function tokenize(set) {
295
+ const raw = set.trim().split(/\s+/).filter(Boolean);
296
+ const out = [];
297
+ for (let i = 0; i < raw.length; i++) {
298
+ const t = raw[i];
299
+ if (/^(\^|~|>=|<=|>|<|=)$/.test(t) && raw[i + 1]) out.push(t + raw[++i]);
300
+ else out.push(t);
301
+ }
302
+ return out;
303
+ }
304
+ function isValidRange(range) {
305
+ try {
306
+ for (const set of range.split("||")) for (const tok of tokenize(set)) if (!expand(tok)) return false;
307
+ return true;
308
+ } catch {
309
+ return false;
310
+ }
311
+ }
312
+ function satisfies(version, range) {
313
+ if (!isValidVersion(version)) return false;
314
+ const v = parseVersion(version);
315
+ for (const set of range.split("||")) {
316
+ const tokens = tokenize(set);
317
+ const comparators = [];
318
+ let valid = true;
319
+ for (const t of tokens.length ? tokens : ["*"]) {
320
+ const e = expand(t);
321
+ if (!e) {
322
+ valid = false;
323
+ break;
324
+ }
325
+ comparators.push(...e);
326
+ }
327
+ if (!valid) continue;
328
+ const allOk = comparators.every((c) => {
329
+ const r = compareVersions(version, c.version);
330
+ switch (c.op) {
331
+ case ">=":
332
+ return r >= 0;
333
+ case ">":
334
+ return r > 0;
335
+ case "<=":
336
+ return r <= 0;
337
+ case "<":
338
+ return r < 0;
339
+ case "=":
340
+ return r === 0;
341
+ }
342
+ });
343
+ const preOk = v.prerelease.length === 0 || comparators.some((c) => {
344
+ const cv = parseVersion(c.version);
345
+ return cv.prerelease.length > 0 && cv.major === v.major && cv.minor === v.minor && cv.patch === v.patch;
346
+ });
347
+ if (allOk && preOk) return true;
348
+ }
349
+ return false;
350
+ }
351
+
352
+ // src/core/manifest.ts
353
+ var FRAMEWORK_VERSION = "1.0.0";
354
+ var FRAMEWORK_ENGINE_KEY = "fluxplugin";
355
+ var BUILTIN_PERMISSIONS = [
356
+ "filesystem.read",
357
+ "filesystem.write",
358
+ "network",
359
+ "database",
360
+ "ipc",
361
+ "window",
362
+ "notifications",
363
+ "storage",
364
+ "services",
365
+ "menus",
366
+ "commands",
367
+ "routes",
368
+ "ui",
369
+ "events",
370
+ "hooks",
371
+ "styles",
372
+ "clipboard",
373
+ "shell"
374
+ ];
375
+ var ID_RE = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
376
+ var ACTIVATION_RE = /^(\*|onStartup|onCommand:.+|onRoute:.+|onEvent:.+|onService:.+|onSlot:.+|onIPC:.+)$/;
377
+ var permissionSchema = s.custom(
378
+ (v) => typeof v === "string" && (BUILTIN_PERMISSIONS.includes(v) || /^x\.[a-z0-9.-]+$/.test(v)),
379
+ `unknown permission (built-ins: ${BUILTIN_PERMISSIONS.join(", ")}; custom ones must start with "x.")`
380
+ );
381
+ var versionRange = s.custom((v) => typeof v === "string" && isValidRange(v), "expected a valid semver range");
382
+ var menuLocations = ["application", "context", "sidebar", "toolbar", "commandPalette", "tray", "settings"];
383
+ var manifestSchema = s.object({
384
+ id: s.string({ pattern: ID_RE, min: 3, max: 128, description: "Unique reverse-DNS style identifier, lowercase" }),
385
+ name: s.string({ min: 1, max: 128 }),
386
+ version: s.custom((v) => typeof v === "string" && isValidVersion(v), "expected a valid semver version"),
387
+ description: s.string().optional(),
388
+ author: s.union(s.string(), s.object({ name: s.string(), email: s.string().optional(), url: s.string().optional() })).optional(),
389
+ homepage: s.string().optional(),
390
+ repository: s.union(s.string(), s.object({ type: s.string().optional(), url: s.string() })).optional(),
391
+ license: s.string().optional(),
392
+ icon: s.string().optional(),
393
+ /** Entry executed in the Electron main process. */
394
+ main: s.string().optional(),
395
+ /** Entry executed in the preload context (sandbox-compatible, no Node). */
396
+ preload: s.string().optional(),
397
+ /** ESM entry loaded by the renderer. */
398
+ renderer: s.string().optional(),
399
+ styles: s.array(s.string()).optional(),
400
+ tailwind: s.object({
401
+ /** Globs (relative to the plugin root) scanned by Tailwind so utility classes are never purged. */
402
+ content: s.array(s.string()).optional(),
403
+ safelist: s.array(s.string()).optional(),
404
+ /** Path of a pre-compiled CSS file produced by the plugin's own Tailwind build. */
405
+ precompiled: s.string().optional()
406
+ }).optional(),
407
+ activationEvents: s.array(s.string({ pattern: ACTIVATION_RE })).default([]),
408
+ deactivationEvents: s.array(s.string()).default([]),
409
+ permissions: s.array(permissionSchema).default([]),
410
+ dependencies: s.record(versionRange).default({}),
411
+ optionalDependencies: s.record(versionRange).default({}),
412
+ peerDependencies: s.record(versionRange).default({}),
413
+ engines: s.record(versionRange).default({}),
414
+ /** Static, declarative metadata mirrored by the code-level registrations (lets the host display plugins without activating them). */
415
+ contributes: s.object({
416
+ routes: s.array(s.object({ path: s.string(), title: s.string().optional(), icon: s.string().optional(), permission: permissionSchema.optional() })).optional(),
417
+ commands: s.array(s.object({ id: s.string(), title: s.string(), category: s.string().optional() })).optional(),
418
+ menus: s.array(s.object({ location: s.enum(menuLocations), id: s.string(), label: s.string(), command: s.string().optional() })).optional(),
419
+ services: s.array(s.string()).optional(),
420
+ ipc: s.array(s.string()).optional(),
421
+ hooks: s.array(s.string()).optional(),
422
+ slots: s.array(s.string()).optional(),
423
+ configuration: s.unknown().optional()
424
+ }).default({}),
425
+ /** Optional integrity hash (sha256-<hex>) of the main bundle. Verified before load when present. */
426
+ integrity: s.record(s.string({ pattern: /^sha256-[0-9a-f]{64}$/ })).optional(),
427
+ signature: s.object({ algorithm: s.literal("ed25519"), publicKey: s.string(), value: s.string() }).optional()
428
+ });
429
+ function validateManifest(raw, opts = {}) {
430
+ const r = manifestSchema.parse(raw);
431
+ if (!r.ok) return { ok: false, issues: r.issues, warnings: [] };
432
+ const manifest = r.value;
433
+ const issues = [];
434
+ const warnings = [];
435
+ const fw = opts.frameworkVersion ?? FRAMEWORK_VERSION;
436
+ const required = manifest.engines?.[FRAMEWORK_ENGINE_KEY];
437
+ if (required && !satisfies(fw, required)) {
438
+ issues.push({ path: `engines.${FRAMEWORK_ENGINE_KEY}`, message: `requires framework ${required}, running ${fw}` });
439
+ }
440
+ if (!manifest.main && !manifest.renderer && !manifest.preload) {
441
+ issues.push({ path: "", message: "at least one of main, preload or renderer entry is required" });
442
+ }
443
+ if (!required) warnings.push(`engines.${FRAMEWORK_ENGINE_KEY} is not declared; compatibility cannot be verified`);
444
+ for (const dep of Object.keys(manifest.dependencies ?? {})) {
445
+ if (dep === manifest.id) issues.push({ path: `dependencies.${dep}`, message: "a plugin cannot depend on itself" });
446
+ }
447
+ return { ok: issues.length === 0, manifest: issues.length ? void 0 : manifest, issues, warnings };
448
+ }
449
+
450
+ // src/main/security.ts
451
+ import crypto from "node:crypto";
452
+ import { promises as fs } from "node:fs";
453
+ import path from "node:path";
454
+ var IntegrityError = class extends Error {
455
+ constructor(message) {
456
+ super(message);
457
+ this.name = "IntegrityError";
458
+ }
459
+ };
460
+ var sha256File = async (file) => "sha256-" + crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex");
461
+ function canonicalJson(v) {
462
+ if (Array.isArray(v)) return `[${v.map(canonicalJson).join(",")}]`;
463
+ if (v && typeof v === "object") {
464
+ const o = v;
465
+ return `{${Object.keys(o).filter((k) => o[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
466
+ }
467
+ return JSON.stringify(v);
468
+ }
469
+ function resolveInside(root, rel) {
470
+ const abs = path.resolve(root, rel);
471
+ if (abs !== root && !abs.startsWith(path.resolve(root) + path.sep)) throw new IntegrityError(`path escapes plugin directory: ${rel}`);
472
+ return abs;
473
+ }
474
+ async function computeIntegrity(root, manifest) {
475
+ const files = [manifest.main, manifest.preload, manifest.renderer, ...manifest.styles ?? []].filter((f) => !!f);
476
+ const out = {};
477
+ for (const f of files) {
478
+ const rel = f.replace(/^\.\//, "");
479
+ try {
480
+ out[rel] = await sha256File(resolveInside(root, rel));
481
+ } catch {
482
+ }
483
+ }
484
+ return out;
485
+ }
486
+ function generateSigningKeyPair() {
487
+ const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
488
+ return {
489
+ publicKey: publicKey.export({ type: "spki", format: "der" }).toString("base64"),
490
+ privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("base64")
491
+ };
492
+ }
493
+ var signedPayload = (m) => {
494
+ const { signature: _s, ...rest } = m;
495
+ return canonicalJson(rest);
496
+ };
497
+ function signManifest(manifest, privateKeyB64) {
498
+ const key = crypto.createPrivateKey({ key: Buffer.from(privateKeyB64, "base64"), format: "der", type: "pkcs8" });
499
+ const pub = crypto.createPublicKey(key).export({ type: "spki", format: "der" }).toString("base64");
500
+ const value = crypto.sign(null, Buffer.from(signedPayload(manifest)), key).toString("base64");
501
+ return { ...manifest, signature: { algorithm: "ed25519", publicKey: pub, value } };
502
+ }
503
+ async function verifyPlugin(root, raw, manifest, opts = {}) {
504
+ const warnings = [];
505
+ if (opts.verifyIntegrity !== false && manifest.integrity) {
506
+ for (const [rel, expected] of Object.entries(manifest.integrity)) {
507
+ let actual;
508
+ try {
509
+ actual = await sha256File(resolveInside(root, rel));
510
+ } catch {
511
+ throw new IntegrityError(`integrity: file "${rel}" is missing`);
512
+ }
513
+ if (actual !== expected) throw new IntegrityError(`integrity: "${rel}" does not match its recorded hash (file was modified)`);
514
+ }
515
+ } else if (opts.verifyIntegrity !== false && opts.requireSignature) {
516
+ throw new IntegrityError("integrity hashes are required but missing");
517
+ }
518
+ const sig = manifest.signature;
519
+ if (sig) {
520
+ let ok2 = false;
521
+ try {
522
+ const pub = crypto.createPublicKey({ key: Buffer.from(sig.publicKey, "base64"), format: "der", type: "spki" });
523
+ ok2 = crypto.verify(null, Buffer.from(signedPayload(raw)), pub, Buffer.from(sig.value, "base64"));
524
+ } catch {
525
+ ok2 = false;
526
+ }
527
+ if (!ok2) throw new IntegrityError("signature is invalid");
528
+ if (opts.trustedKeys && !opts.trustedKeys.includes(sig.publicKey)) {
529
+ if (opts.requireSignature) throw new IntegrityError("signed by an untrusted key");
530
+ warnings.push("signed by an untrusted key");
531
+ }
532
+ } else if (opts.requireSignature) {
533
+ throw new IntegrityError("plugin is not signed");
534
+ }
535
+ return warnings;
536
+ }
537
+
538
+ // src/main/discover.ts
539
+ var MANIFEST_FILE = "plugin.json";
540
+
541
+ // src/main/fs-storage.ts
542
+ import { promises as fs2 } from "node:fs";
543
+ import path2 from "node:path";
544
+ async function atomicWrite(file, data) {
545
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
546
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
547
+ await fs2.writeFile(tmp, data, "utf8");
548
+ await fs2.rename(tmp, file);
549
+ }
550
+ var JsonStateFile = class {
551
+ constructor(file) {
552
+ this.file = file;
553
+ }
554
+ file;
555
+ queue = Promise.resolve();
556
+ async readAll() {
557
+ try {
558
+ const j = JSON.parse(await fs2.readFile(this.file, "utf8"));
559
+ return { disabled: Array.isArray(j.disabled) ? j.disabled : [], approved: j.approved && typeof j.approved === "object" ? j.approved : {} };
560
+ } catch {
561
+ return { disabled: [], approved: {} };
562
+ }
563
+ }
564
+ /** Merges `patch(current)` into the file. Concurrent calls run one after the other. */
565
+ update(patch) {
566
+ const run2 = this.queue.then(async () => {
567
+ const cur = await this.readAll();
568
+ const p = typeof patch === "function" ? patch(cur) : patch;
569
+ await atomicWrite(this.file, JSON.stringify({ ...cur, ...p }, null, 2));
570
+ });
571
+ this.queue = run2.catch(() => void 0);
572
+ return run2;
573
+ }
574
+ /** Resolves once every queued write has hit the disk. */
575
+ async flush() {
576
+ await this.queue;
577
+ }
578
+ };
579
+
580
+ // src/main/registry.ts
581
+ import { promises as fs4 } from "node:fs";
582
+ import path4 from "node:path";
583
+
584
+ // src/main/tar.ts
585
+ import { promises as fs3 } from "node:fs";
586
+ import path3 from "node:path";
587
+ import zlib from "node:zlib";
588
+ import { promisify } from "node:util";
589
+ var gzip = promisify(zlib.gzip);
590
+ var gunzip = promisify(zlib.gunzip);
591
+ var BLOCK = 512;
592
+ function header(name, size, mode, type) {
593
+ const h = Buffer.alloc(BLOCK);
594
+ if (Buffer.byteLength(name) > 100) throw new Error(`path too long for tar: ${name}`);
595
+ h.write(name, 0, "utf8");
596
+ h.write(mode.toString(8).padStart(7, "0") + "\0", 100);
597
+ h.write("0000000\0", 108);
598
+ h.write("0000000\0", 116);
599
+ h.write(size.toString(8).padStart(11, "0") + "\0", 124);
600
+ h.write("00000000000\0", 136);
601
+ h.write(" ", 148);
602
+ h.write(type, 156);
603
+ h.write("ustar\0", 257);
604
+ h.write("00", 263);
605
+ let sum = 0;
606
+ for (const b of h) sum += b;
607
+ h.write(sum.toString(8).padStart(6, "0") + "\0 ", 148);
608
+ return h;
609
+ }
610
+ async function walk(dir, base = "") {
611
+ const out = [];
612
+ for (const e of (await fs3.readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
613
+ if (e.name === "node_modules" || e.name === ".git") continue;
614
+ const abs = path3.join(dir, e.name);
615
+ const rel = base ? `${base}/${e.name}` : e.name;
616
+ if (e.isDirectory()) {
617
+ out.push({ rel, abs, dir: true });
618
+ out.push(...await walk(abs, rel));
619
+ } else if (e.isFile()) out.push({ rel, abs, dir: false });
620
+ }
621
+ return out;
622
+ }
623
+ async function createTarGz(dir, prefix = "package") {
624
+ const chunks = [];
625
+ for (const f of await walk(dir)) {
626
+ const name = `${prefix}/${f.rel}${f.dir ? "/" : ""}`;
627
+ if (f.dir) chunks.push(header(name, 0, 493, "5"));
628
+ else {
629
+ const data = await fs3.readFile(f.abs);
630
+ chunks.push(header(name, data.length, 420, "0"), data, Buffer.alloc((BLOCK - data.length % BLOCK) % BLOCK));
631
+ }
632
+ }
633
+ chunks.push(Buffer.alloc(BLOCK * 2));
634
+ return gzip(Buffer.concat(chunks));
635
+ }
636
+ async function readTarGz(buf) {
637
+ const tar = await gunzip(buf);
638
+ const entries = [];
639
+ let off = 0;
640
+ while (off + BLOCK <= tar.length) {
641
+ const h = tar.subarray(off, off + BLOCK);
642
+ if (h.every((b) => b === 0)) break;
643
+ const name = h.toString("utf8", 0, 100).replace(/\0.*$/, "");
644
+ const size = parseInt(h.toString("utf8", 124, 135).replace(/\0.*$/, "").trim() || "0", 8);
645
+ const type = String.fromCharCode(h[156] ?? 48);
646
+ off += BLOCK;
647
+ if (type === "0" || type === "\0") entries.push({ name, type: "file", data: Buffer.from(tar.subarray(off, off + size)) });
648
+ else if (type === "5") entries.push({ name, type: "dir", data: Buffer.alloc(0) });
649
+ off += Math.ceil(size / BLOCK) * BLOCK;
650
+ }
651
+ return entries;
652
+ }
653
+ async function extractTarGz(buf, dest) {
654
+ const root = path3.resolve(dest);
655
+ await fs3.mkdir(root, { recursive: true });
656
+ for (const e of await readTarGz(buf)) {
657
+ const parts = e.name.split("/").filter(Boolean);
658
+ parts.shift();
659
+ if (!parts.length) continue;
660
+ if (parts.some((p) => p === ".." || p.includes("\0")) || path3.isAbsolute(e.name)) throw new Error(`unsafe path in archive: ${e.name}`);
661
+ const target = path3.resolve(root, ...parts);
662
+ if (target !== root && !target.startsWith(root + path3.sep)) throw new Error(`unsafe path in archive: ${e.name}`);
663
+ if (e.type === "dir") await fs3.mkdir(target, { recursive: true });
664
+ else {
665
+ await fs3.mkdir(path3.dirname(target), { recursive: true });
666
+ await fs3.writeFile(target, e.data);
667
+ }
668
+ }
669
+ }
670
+
671
+ // src/main/registry.ts
672
+ async function readManifest(dir) {
673
+ const raw = JSON.parse(await fs4.readFile(path4.join(dir, MANIFEST_FILE), "utf8"));
674
+ const v = validateManifest(raw);
675
+ if (!v.manifest) throw new Error(`invalid manifest: ${v.issues.map((i) => `${i.path}: ${i.message}`).join("; ")}`);
676
+ return { raw, manifest: v.manifest };
677
+ }
678
+ var toEntry = (m, location, source, sha256) => ({
679
+ id: m.id,
680
+ name: m.name,
681
+ version: m.version,
682
+ description: m.description,
683
+ author: typeof m.author === "string" ? m.author : m.author?.name,
684
+ permissions: m.permissions ?? [],
685
+ location,
686
+ source,
687
+ sha256
688
+ });
689
+ async function copyDir(src, dest) {
690
+ await fs4.mkdir(dest, { recursive: true });
691
+ for (const e of await fs4.readdir(src, { withFileTypes: true })) {
692
+ if (e.name === "node_modules" || e.name === ".git") continue;
693
+ const s2 = path4.join(src, e.name);
694
+ const d = path4.join(dest, e.name);
695
+ if (e.isDirectory()) await copyDir(s2, d);
696
+ else if (e.isFile()) await fs4.copyFile(s2, d);
697
+ }
698
+ }
699
+ var LocalRegistry = class {
700
+ constructor(dir, name = `local:${dir}`) {
701
+ this.dir = dir;
702
+ this.name = name;
703
+ }
704
+ dir;
705
+ name;
706
+ async search(query = "") {
707
+ const q = query.toLowerCase();
708
+ const out = [];
709
+ let names = [];
710
+ try {
711
+ names = await fs4.readdir(this.dir);
712
+ } catch {
713
+ return [];
714
+ }
715
+ for (const n of names.sort()) {
716
+ const root = path4.join(this.dir, n);
717
+ try {
718
+ const { manifest } = await readManifest(root);
719
+ if (!q || `${manifest.id} ${manifest.name} ${manifest.description ?? ""}`.toLowerCase().includes(q)) out.push(toEntry(manifest, root, this.name));
720
+ } catch {
721
+ }
722
+ }
723
+ return out;
724
+ }
725
+ async fetch(entry, dest) {
726
+ await copyDir(entry.location, dest);
727
+ return (await readManifest(dest)).manifest;
728
+ }
729
+ };
730
+ var PluginInstaller = class {
731
+ constructor(pluginsDir, backupDir, registries = []) {
732
+ this.pluginsDir = pluginsDir;
733
+ this.backupDir = backupDir;
734
+ this.registries = registries;
735
+ }
736
+ pluginsDir;
737
+ backupDir;
738
+ registries;
739
+ addRegistry(r) {
740
+ this.registries.push(r);
741
+ }
742
+ async search(query) {
743
+ const all = await Promise.allSettled(this.registries.map((r) => r.search(query)));
744
+ return all.flatMap((r) => r.status === "fulfilled" ? r.value : []);
745
+ }
746
+ async installed() {
747
+ const out = [];
748
+ for (const n of await fs4.readdir(this.pluginsDir).catch(() => [])) {
749
+ try {
750
+ out.push((await readManifest(path4.join(this.pluginsDir, n))).manifest);
751
+ } catch {
752
+ }
753
+ }
754
+ return out;
755
+ }
756
+ dirOf(id) {
757
+ return path4.join(this.pluginsDir, id.replace(/[^a-z0-9.-]/gi, "_"));
758
+ }
759
+ /** Installs (or updates) `entry`. The plugin directory is replaced atomically from a staging directory. */
760
+ async install(entry) {
761
+ const registry = this.registries.find((r) => r.name === entry.source);
762
+ if (!registry) throw new Error(`unknown registry "${entry.source}"`);
763
+ const stage = path4.join(this.backupDir, ".staging", `${entry.id}-${Date.now()}`);
764
+ await fs4.mkdir(path4.dirname(stage), { recursive: true });
765
+ try {
766
+ const manifest = await registry.fetch(entry, stage);
767
+ if (manifest.id !== entry.id) throw new Error(`registry entry "${entry.id}" contains plugin "${manifest.id}"`);
768
+ const target = this.dirOf(entry.id);
769
+ let previousVersion;
770
+ try {
771
+ previousVersion = (await readManifest(target)).manifest.version;
772
+ const bk = path4.join(this.backupDir, entry.id, previousVersion);
773
+ await fs4.rm(bk, { recursive: true, force: true });
774
+ await fs4.mkdir(path4.dirname(bk), { recursive: true });
775
+ await fs4.rename(target, bk);
776
+ } catch (e) {
777
+ if (e.code && e.code !== "ENOENT") throw e;
778
+ }
779
+ await fs4.mkdir(path4.dirname(target), { recursive: true });
780
+ await fs4.rename(stage, target);
781
+ return { id: entry.id, version: manifest.version, previousVersion };
782
+ } finally {
783
+ await fs4.rm(stage, { recursive: true, force: true });
784
+ }
785
+ }
786
+ /** Removes the plugin directory (backups are kept so the user can still roll back/reinstall). */
787
+ async uninstall(id) {
788
+ const dir = this.dirOf(id);
789
+ const { manifest } = await readManifest(dir);
790
+ const bk = path4.join(this.backupDir, id, manifest.version);
791
+ await fs4.rm(bk, { recursive: true, force: true });
792
+ await fs4.mkdir(path4.dirname(bk), { recursive: true });
793
+ await fs4.rename(dir, bk);
794
+ }
795
+ async versions(id) {
796
+ const names = await fs4.readdir(path4.join(this.backupDir, id)).catch(() => []);
797
+ return names.sort(compareVersions);
798
+ }
799
+ /** Restores the newest backup (or `version`) and keeps the replaced version as a backup. */
800
+ async rollback(id, version) {
801
+ const versions = await this.versions(id);
802
+ const target = version ?? versions[versions.length - 1];
803
+ if (!target || !versions.includes(target)) throw new Error(`no backup available for "${id}"${version ? ` @ ${version}` : ""}`);
804
+ const dir = this.dirOf(id);
805
+ let current;
806
+ try {
807
+ current = (await readManifest(dir)).manifest.version;
808
+ const bk = path4.join(this.backupDir, id, current);
809
+ await fs4.rm(bk, { recursive: true, force: true });
810
+ await fs4.rename(dir, bk);
811
+ } catch (e) {
812
+ if (e.code && e.code !== "ENOENT") throw e;
813
+ }
814
+ await fs4.rename(path4.join(this.backupDir, id, target), dir);
815
+ return { id, version: target, previousVersion: current };
816
+ }
817
+ /** Entries from registries that are newer than what is installed. */
818
+ async updates() {
819
+ const installed = new Map((await this.installed()).map((m) => [m.id, m.version]));
820
+ return (await this.search()).filter((e) => installed.has(e.id) && compareVersions(e.version, installed.get(e.id)) > 0);
821
+ }
822
+ };
823
+
824
+ // src/cli/build.ts
825
+ import { promises as fs5 } from "node:fs";
826
+ import path6 from "node:path";
827
+ import { build, context } from "esbuild";
828
+
829
+ // src/cli/shared-modules.ts
830
+ import path5 from "node:path";
831
+ var SHARED_MODULES = ["react", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom", "react-dom/client", "fluxplugin", "fluxplugin/react", "fluxplugin/manager-ui"];
832
+ function sharedModulesPlugin(shared = SHARED_MODULES) {
833
+ const set = new Set(shared);
834
+ return {
835
+ name: "flux-shared-modules",
836
+ setup(b) {
837
+ b.onResolve({ filter: /.*/ }, (args) => {
838
+ if (set.has(args.path)) return { path: args.path, namespace: "flux-shared" };
839
+ return void 0;
840
+ });
841
+ b.onLoad({ filter: /.*/, namespace: "flux-shared" }, (args) => ({
842
+ contents: `const m = globalThis.__FLUXPLUGIN_SHARED__ && globalThis.__FLUXPLUGIN_SHARED__[${JSON.stringify(args.path)}];
843
+ if (!m) throw new Error("Shared module ${args.path} is not provided by the host application (see installSharedModules).");
844
+ module.exports = m;`,
845
+ loader: "js",
846
+ resolveDir: path5.resolve(".")
847
+ }));
848
+ }
849
+ };
850
+ }
851
+
852
+ // src/cli/build.ts
853
+ var exists = (f) => fs5.access(f).then(() => true, () => false);
854
+ async function firstExisting(root, names) {
855
+ for (const n of names) if (await exists(path6.join(root, n))) return path6.join(root, n);
856
+ }
857
+ async function buildOptions(root) {
858
+ const raw = JSON.parse(await fs5.readFile(path6.join(root, "plugin.json"), "utf8"));
859
+ const m = validateManifest(raw).manifest;
860
+ if (!m) throw new Error("invalid plugin.json (run `fluxplugin validate`)");
861
+ const out = [];
862
+ const common = { bundle: true, sourcemap: true, logLevel: "warning", plugins: [sharedModulesPlugin()], jsx: "automatic", loader: { ".module.css": "local-css" } };
863
+ if (m.main) {
864
+ const entry = await firstExisting(root, ["src/main.ts", "src/main.js"]);
865
+ if (!entry) throw new Error("manifest declares `main` but src/main.ts was not found");
866
+ out.push({ ...common, entryPoints: [entry], outfile: path6.join(root, m.main), platform: "node", format: "esm", target: "node20", external: ["node:*", "electron"] });
867
+ }
868
+ if (m.renderer) {
869
+ const entry = await firstExisting(root, ["src/renderer.tsx", "src/renderer.ts", "src/renderer.jsx", "src/renderer.js"]);
870
+ if (!entry) throw new Error("manifest declares `renderer` but src/renderer.tsx was not found");
871
+ out.push({ ...common, entryPoints: [entry], outfile: path6.join(root, m.renderer), platform: "browser", format: "esm", target: "chrome120" });
872
+ }
873
+ for (const css of m.styles ?? []) {
874
+ const entry = path6.join(root, "src/styles", path6.basename(css));
875
+ if (await exists(entry)) out.push({ ...common, plugins: [], entryPoints: [entry], outfile: path6.join(root, css) });
876
+ }
877
+ return out;
878
+ }
879
+ async function buildPlugin(root, watch = false, log2 = console.log) {
880
+ const opts = await buildOptions(root);
881
+ if (!watch) {
882
+ for (const o of opts) await build(o);
883
+ log2(`built ${opts.length} bundle(s) in ${root}`);
884
+ return async () => {
885
+ };
886
+ }
887
+ const ctxs = await Promise.all(opts.map((o) => context(o)));
888
+ await Promise.all(ctxs.map((c) => c.watch()));
889
+ log2(`watching ${root} (${ctxs.length} bundles)`);
890
+ return async () => void await Promise.all(ctxs.map((c) => c.dispose()));
891
+ }
892
+
893
+ // src/cli/host.ts
894
+ import { spawn } from "node:child_process";
895
+ import { promises as fs7 } from "node:fs";
896
+ import { createRequire } from "node:module";
897
+ import path8 from "node:path";
898
+ import { build as build2, context as context2 } from "esbuild";
899
+
900
+ // src/cli/config.ts
901
+ import { promises as fs6 } from "node:fs";
902
+ import path7 from "node:path";
903
+ async function loadConfig(flags, cwd = process.cwd()) {
904
+ let file = {};
905
+ try {
906
+ file = JSON.parse(await fs6.readFile(path7.join(cwd, "fluxplugin.config.json"), "utf8"));
907
+ } catch {
908
+ }
909
+ return {
910
+ pluginsDir: path7.resolve(cwd, flags.dir ?? file.pluginsDir ?? "plugins"),
911
+ dataDir: path7.resolve(cwd, flags.data ?? file.dataDir ?? ".flux-data")
912
+ };
913
+ }
914
+
915
+ // src/cli/host.ts
916
+ var exists2 = (f) => fs7.access(f).then(() => true, () => false);
917
+ async function isHostProject(cwd) {
918
+ return await exists2(path8.join(cwd, "fluxplugin.config.json")) || await exists2(path8.join(cwd, "src/renderer.tsx")) && await exists2(path8.join(cwd, "src/framework.ts"));
919
+ }
920
+ var nodeBanner = { js: "import { createRequire as __r } from 'node:module'; const require = __r(import.meta.url);" };
921
+ function hostBuilds(cwd, outDir, dev) {
922
+ const common = { absWorkingDir: cwd, bundle: true, sourcemap: dev, logLevel: "warning", jsx: "automatic", define: { "process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production") } };
923
+ const entry = (f) => path8.join(cwd, "src", f);
924
+ return [
925
+ { ...common, entryPoints: [entry("renderer.tsx")], outfile: path8.join(outDir, "renderer.js"), platform: "browser", format: "esm", target: "chrome120" },
926
+ { ...common, entryPoints: [entry("main.ts")], outfile: path8.join(outDir, "main.cjs"), platform: "node", format: "cjs", target: "node20", external: ["electron"] },
927
+ { ...common, entryPoints: [entry("preload.ts")], outfile: path8.join(outDir, "preload.cjs"), platform: "node", format: "cjs", target: "node20", external: ["electron"] },
928
+ { ...common, entryPoints: [entry("web.ts")], outfile: path8.join(outDir, "web.mjs"), platform: "node", format: "esm", target: "node20", external: ["electron"], banner: nodeBanner }
929
+ ];
930
+ }
931
+ async function pluginDirs(cwd, pluginsDir) {
932
+ const names = await fs7.readdir(pluginsDir).catch(() => []);
933
+ const out = [];
934
+ for (const n of names.sort()) if (await exists2(path8.join(pluginsDir, n, "plugin.json"))) out.push(path8.join(pluginsDir, n));
935
+ void cwd;
936
+ return out;
937
+ }
938
+ async function buildHost(cwd, opts = {}) {
939
+ const log2 = opts.log ?? console.log;
940
+ const cfgFile = JSON.parse(await fs7.readFile(path8.join(cwd, "fluxplugin.config.json"), "utf8").catch(() => "{}"));
941
+ const cfg = await loadConfig({}, cwd);
942
+ const outDir = path8.resolve(cwd, cfgFile.outDir ?? "dist");
943
+ await fs7.mkdir(outDir, { recursive: true });
944
+ await fs7.copyFile(path8.join(cwd, "src/index.html"), path8.join(outDir, "index.html"));
945
+ const disposers = [];
946
+ const opts2 = hostBuilds(cwd, outDir, !!opts.watch);
947
+ if (opts.watch) {
948
+ const ctxs = await Promise.all(opts2.map((o) => context2(o)));
949
+ await Promise.all(ctxs.map((c) => c.watch()));
950
+ disposers.push(async () => void await Promise.all(ctxs.map((c) => c.dispose())));
951
+ } else for (const o of opts2) await build2(o);
952
+ for (const dir of await pluginDirs(cwd, cfg.pluginsDir)) {
953
+ try {
954
+ disposers.push(await buildPlugin(dir, !!opts.watch, log2));
955
+ } catch (e) {
956
+ log2(`! plugin ${path8.basename(dir)} not built: ${e.message}`);
957
+ }
958
+ }
959
+ log2(`host built \u2192 ${path8.relative(cwd, outDir) || "."}`);
960
+ return async () => void await Promise.all(disposers.map((d) => d()));
961
+ }
962
+ function electronBinary(cwd) {
963
+ try {
964
+ const p = createRequire(path8.join(cwd, "package.json"))("electron");
965
+ return typeof p === "string" ? p : void 0;
966
+ } catch {
967
+ return void 0;
968
+ }
969
+ }
970
+ function startApp(cwd, opts = {}) {
971
+ const bin = opts.web ? void 0 : electronBinary(cwd);
972
+ const env = { ...process.env, FLUXPLUGIN_PROJECT: cwd, ...opts.dev ? { FLUXPLUGIN_DEV: "1" } : {}, ...opts.port ? { PORT: String(opts.port) } : {} };
973
+ if (bin) return { child: spawn(bin, [path8.join(cwd, "dist/main.cjs")], { cwd, env, stdio: "inherit" }), mode: "electron" };
974
+ return { child: spawn(process.execPath, [path8.join(cwd, "dist/web.mjs")], { cwd, env, stdio: "inherit" }), mode: "web" };
975
+ }
976
+
977
+ // src/cli/templates.ts
978
+ var pkg = (v) => JSON.stringify(
979
+ {
980
+ name: v.id,
981
+ version: "1.0.0",
982
+ private: true,
983
+ type: "module",
984
+ scripts: { dev: "fluxplugin dev", build: "fluxplugin build", package: "fluxplugin package", validate: "fluxplugin validate" },
985
+ devDependencies: { fluxplugin: "^1.0.0", "@types/react": "^19.0.0", react: "^19.0.0", typescript: "^5.6.0" }
986
+ },
987
+ null,
988
+ 2
989
+ );
990
+ var tsconfig = `{
991
+ "compilerOptions": {
992
+ "target": "ES2022",
993
+ "module": "ESNext",
994
+ "moduleResolution": "Bundler",
995
+ "jsx": "react-jsx",
996
+ "strict": true,
997
+ "noEmit": true,
998
+ "skipLibCheck": true,
999
+ "lib": ["ES2022", "DOM"]
1000
+ },
1001
+ "include": ["src"]
1002
+ }
1003
+ `;
1004
+ var styles = (v) => `/* Styles are loaded when the plugin activates and removed when it is disabled. */
1005
+ .${v.slug}-card {
1006
+ border: 1px solid var(--flux-border, #e2e8f0);
1007
+ border-radius: 12px;
1008
+ padding: 16px;
1009
+ }
1010
+ `;
1011
+ function basic(v) {
1012
+ return {
1013
+ "plugin.json": JSON.stringify(
1014
+ {
1015
+ id: v.id,
1016
+ name: v.name,
1017
+ version: "1.0.0",
1018
+ description: `${v.name} plugin`,
1019
+ author: "Your name",
1020
+ main: "./dist/main.mjs",
1021
+ renderer: "./dist/renderer.js",
1022
+ styles: ["./dist/index.css"],
1023
+ activationEvents: [`onCommand:${v.slug}.hello`],
1024
+ permissions: ["commands", "routes", "menus", "ui", "styles", "events"],
1025
+ engines: { "fluxplugin": "^1.0.0" },
1026
+ contributes: { commands: [{ id: `${v.slug}.hello`, title: `${v.name}: Say hello` }] }
1027
+ },
1028
+ null,
1029
+ 2
1030
+ ),
1031
+ "package.json": pkg(v),
1032
+ "tsconfig.json": tsconfig,
1033
+ "src/main.ts": `import { definePlugin } from "fluxplugin";
1034
+
1035
+ export default definePlugin({
1036
+ onActivate(ctx) {
1037
+ ctx.logger.info("${v.name} (main) activated");
1038
+ },
1039
+ });
1040
+ `,
1041
+ "src/renderer.tsx": `import { definePlugin } from "fluxplugin";
1042
+
1043
+ function ${v.Pascal}Page() {
1044
+ return (
1045
+ <div className="${v.slug}-card">
1046
+ <h1>${v.name}</h1>
1047
+ <p>Your first plugin page. Edit <code>src/renderer.tsx</code> and save.</p>
1048
+ </div>
1049
+ );
1050
+ }
1051
+
1052
+ export default definePlugin({
1053
+ routes: [
1054
+ {
1055
+ path: "/${v.slug}",
1056
+ component: ${v.Pascal}Page,
1057
+ meta: { title: "${v.name}", menu: { label: "${v.name}" } },
1058
+ },
1059
+ ],
1060
+ commands: [
1061
+ {
1062
+ id: "${v.slug}.hello",
1063
+ title: "${v.name}: Say hello",
1064
+ execute: () => console.log("Hello from ${v.name}!"),
1065
+ },
1066
+ ],
1067
+ });
1068
+ `,
1069
+ "src/styles/index.css": styles(v),
1070
+ "README.md": `# ${v.name}
1071
+
1072
+ Created with \`fluxplugin create\`.
1073
+
1074
+ \`\`\`bash
1075
+ pnpm install
1076
+ pnpm dev # rebuild on change (the host hot-reloads it)
1077
+ pnpm build
1078
+ pnpm package # produces ${v.id}-1.0.0.tgz
1079
+ \`\`\`
1080
+ `
1081
+ };
1082
+ }
1083
+ function dashboard(v) {
1084
+ const files = basic(v);
1085
+ const m = JSON.parse(files["plugin.json"]);
1086
+ m.activationEvents = ["onStartup"];
1087
+ m.contributes = {};
1088
+ m.permissions = ["commands", "routes", "menus", "ui", "styles"];
1089
+ files["plugin.json"] = JSON.stringify(m, null, 2);
1090
+ files["src/renderer.tsx"] = `import { definePlugin } from "fluxplugin";
1091
+ import { usePluginState } from "fluxplugin/react";
1092
+
1093
+ function ${v.Pascal}Widget() {
1094
+ const [n, setN] = usePluginState("${v.id}", "clicks", 0);
1095
+ return (
1096
+ <div className="${v.slug}-card" data-testid="${v.slug}-widget">
1097
+ <strong>${v.name}</strong>
1098
+ <p>A widget injected into the host's <code>dashboard.after</code> slot.</p>
1099
+ <button onClick={() => setN((c) => c + 1)}>Clicked {n} times</button>
1100
+ </div>
1101
+ );
1102
+ }
1103
+
1104
+ function ${v.Pascal}Page() {
1105
+ return <div className="${v.slug}-card"><h1>${v.name}</h1><p>Registered at runtime by a plugin.</p></div>;
1106
+ }
1107
+
1108
+ export default definePlugin({
1109
+ routes: [{ path: "/${v.slug}", lazy: async () => ({ default: ${v.Pascal}Page }), meta: { title: "${v.name}", breadcrumb: "${v.name}", menu: { label: "${v.name}" } } }],
1110
+ components: [{ slot: "dashboard.after", component: ${v.Pascal}Widget, priority: 10 }],
1111
+ // Data contribution to the host's "dashboard" extension point (validated by the host's schema).
1112
+ extensions: [{ point: "dashboard", contribution: { toolbar: [{ id: "${v.slug}-hello", label: "${v.name}: hello", command: "${v.slug}.hello" }] } }],
1113
+ commands: [{ id: "${v.slug}.hello", title: "${v.name}: say hello", category: "${v.name}", execute: () => console.log("Hello from ${v.name}!") }],
1114
+ });
1115
+ `;
1116
+ return files;
1117
+ }
1118
+ function backend(v) {
1119
+ const files = basic(v);
1120
+ const m = JSON.parse(files["plugin.json"]);
1121
+ m.activationEvents = [];
1122
+ m.permissions = ["ipc", "services", "events", "commands", "routes", "menus", "ui", "styles", "storage"];
1123
+ m.contributes = {};
1124
+ files["plugin.json"] = JSON.stringify(m, null, 2);
1125
+ files["src/main.ts"] = `import { definePlugin, defineIPC, s } from "fluxplugin";
1126
+
1127
+ const Input = s.object({ name: s.string({ min: 1 }) });
1128
+ const Output = s.object({ message: s.string(), at: s.number() });
1129
+
1130
+ export const hello = defineIPC({
1131
+ name: "${v.slug}.hello",
1132
+ input: Input,
1133
+ output: Output,
1134
+ handler: async ({ name }, ctx) => ({ message: \`Hello \${name} from \${ctx.pluginId}\`, at: Date.now() }),
1135
+ });
1136
+
1137
+ export default definePlugin({
1138
+ ipc: [hello],
1139
+ // Exposed to the renderer as window.plugins.${v.slug}.hello(...)
1140
+ preload: { ${v.slug}: { hello: "${v.slug}.hello" } },
1141
+ services: { "${v.slug}": { now: () => Date.now() } },
1142
+ });
1143
+ `;
1144
+ files["src/renderer.tsx"] = `import { definePlugin } from "fluxplugin";
1145
+ import { usePluginIPC } from "fluxplugin/react";
1146
+
1147
+ function ${v.Pascal}Page() {
1148
+ const { data, call, loading, error } = usePluginIPC<{ name: string }, { message: string }>("${v.slug}.hello", { caller: "${v.id}" });
1149
+ return (
1150
+ <div className="${v.slug}-card">
1151
+ <button disabled={loading} onClick={() => void call({ name: "world" })}>Call main process</button>
1152
+ {data && <p>{data.message}</p>}
1153
+ {error && <p role="alert">{error.message}</p>}
1154
+ </div>
1155
+ );
1156
+ }
1157
+
1158
+ export default definePlugin({
1159
+ routes: [{ path: "/${v.slug}", component: ${v.Pascal}Page, meta: { title: "${v.name}", menu: { label: "${v.name}" } } }],
1160
+ });
1161
+ `;
1162
+ return files;
1163
+ }
1164
+ var TEMPLATES = { basic, dashboard, backend };
1165
+ function varsFromName(name) {
1166
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "my-plugin";
1167
+ const id = slug.includes(".") ? slug : `com.example.${slug}`;
1168
+ const short = slug.split(".").pop();
1169
+ const Pascal = short.split(/[^a-z0-9]+/i).filter(Boolean).map((p) => p[0].toUpperCase() + p.slice(1)).join("");
1170
+ return { id, name: short.split("-").map((p) => p[0].toUpperCase() + p.slice(1)).join(" "), slug: short.replace(/-/g, "_"), Pascal };
1171
+ }
1172
+
1173
+ // src/cli/host-templates.ts
1174
+ function hostFiles(v, withSample = true) {
1175
+ const files = {
1176
+ "package.json": JSON.stringify(
1177
+ {
1178
+ name: v.name,
1179
+ version: "0.1.0",
1180
+ private: true,
1181
+ type: "module",
1182
+ main: "dist/main.cjs",
1183
+ scripts: {
1184
+ dev: "fluxplugin dev",
1185
+ "dev:web": "fluxplugin dev --web",
1186
+ build: "fluxplugin build",
1187
+ start: "fluxplugin start",
1188
+ "plugin:new": "fluxplugin plugin new",
1189
+ typecheck: "tsc --noEmit"
1190
+ },
1191
+ dependencies: { fluxplugin: v.fluxSpec, react: "^19.0.0", "react-dom": "^19.0.0" },
1192
+ devDependencies: { electron: "^33.0.0", typescript: "~5.9.3", "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0" }
1193
+ },
1194
+ null,
1195
+ 2
1196
+ ),
1197
+ "fluxplugin.config.json": JSON.stringify({ pluginsDir: "plugins", outDir: "dist" }, null, 2),
1198
+ "tsconfig.json": JSON.stringify(
1199
+ { compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "Bundler", jsx: "react-jsx", strict: true, noEmit: true, skipLibCheck: true, lib: ["ES2022", "DOM", "DOM.Iterable"], types: ["node"] }, include: ["src"] },
1200
+ null,
1201
+ 2
1202
+ ),
1203
+ ".gitignore": "node_modules\ndist\n.flux-data\n*.tgz\n",
1204
+ "src/index.html": `<!doctype html>
1205
+ <html><head><meta charset="utf-8"><title>${v.name}</title>
1206
+ <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:">
1207
+ <style>body{margin:0;font:14px system-ui}</style></head>
1208
+ <body><div id="root"></div><script type="module" src="./renderer.js"></script></body></html>
1209
+ `,
1210
+ "src/framework.ts": `import { MainFramework } from "fluxplugin/main";
1211
+ import { s } from "fluxplugin";
1212
+
1213
+ /** Shared by Electron (main.ts) and browser mode (web.ts). Put host-side setup here. */
1214
+ export function createFramework(opts: { pluginDirs: string[]; dataDir: string; dev?: boolean; assetUrl?: (id: string, rel: string) => string }) {
1215
+ const fw = new MainFramework({
1216
+ pluginDirs: opts.pluginDirs,
1217
+ dataDir: opts.dataDir,
1218
+ hotReload: opts.dev,
1219
+ assetUrl: opts.assetUrl,
1220
+ requireApproval: false, // set true to make users approve each plugin's permissions
1221
+ allowedRendererChannels: ["host.item.create"],
1222
+ });
1223
+
1224
+ // A host operation wrapped in the hook pipeline: plugins can validate, normalize or veto it
1225
+ // (before./transform./after.item.create) without changing this file.
1226
+ fw.manager.ipc.handle(
1227
+ "host.item.create",
1228
+ (input: { name: string }) =>
1229
+ fw.manager.hooks.run("item.create", input, (i) => {
1230
+ const item = { id: Math.random().toString(36).slice(2, 8), name: i.name };
1231
+ fw.manager.events.emit("item.created", item, "host");
1232
+ return item as Record<string, unknown>;
1233
+ }),
1234
+ "host",
1235
+ { input: s.object({ name: s.string() }) },
1236
+ );
1237
+ return fw;
1238
+ }
1239
+ `,
1240
+ "src/main.ts": `// Electron main process.
1241
+ import { app, BrowserWindow, ipcMain, protocol, webContents } from "electron";
1242
+ import path from "node:path";
1243
+ import { PLUGIN_SCHEME, attachElectron, registerPluginProtocol } from "fluxplugin/main";
1244
+ import { createFramework } from "./framework.js";
1245
+
1246
+ protocol.registerSchemesAsPrivileged([PLUGIN_SCHEME]); // must run before app is ready
1247
+
1248
+ app.whenReady().then(async () => {
1249
+ const dev = !!process.env["FLUXPLUGIN_DEV"];
1250
+ const root = process.env["FLUXPLUGIN_PROJECT"] ?? path.resolve(__dirname, "..");
1251
+ const fw = createFramework({
1252
+ pluginDirs: [path.join(root, "plugins"), path.join(app.getPath("userData"), "plugins")], // bundled + user-installed
1253
+ dataDir: path.join(app.getPath("userData"), "plugins-data"),
1254
+ dev,
1255
+ });
1256
+ registerPluginProtocol(fw, protocol);
1257
+ attachElectron(fw, { ipcMain: ipcMain as never, webContents: webContents as never });
1258
+ await fw.start();
1259
+
1260
+ const win = new BrowserWindow({
1261
+ width: 1100,
1262
+ height: 760,
1263
+ webPreferences: { preload: path.join(__dirname, "preload.cjs"), contextIsolation: true, sandbox: true, nodeIntegration: false },
1264
+ });
1265
+ win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
1266
+ win.webContents.on("will-navigate", (e) => e.preventDefault());
1267
+ await win.loadFile(path.join(__dirname, "index.html"));
1268
+ app.on("before-quit", () => void fw.stop());
1269
+ });
1270
+ app.on("window-all-closed", () => app.quit());
1271
+ `,
1272
+ "src/preload.ts": `import { contextBridge, ipcRenderer } from "electron";
1273
+ import { exposeFluxBridge } from "fluxplugin/preload";
1274
+
1275
+ exposeFluxBridge(contextBridge, ipcRenderer); // the only thing renderer code receives: window.__fluxplugin
1276
+ `,
1277
+ "src/web.ts": `// Browser mode (no Electron): \`fluxplugin dev --web\`.
1278
+ import path from "node:path";
1279
+ import { serveWeb } from "fluxplugin/main";
1280
+ import { createFramework } from "./framework.js";
1281
+
1282
+ const root = process.env["FLUXPLUGIN_PROJECT"] ?? process.cwd();
1283
+ const fw = createFramework({
1284
+ pluginDirs: [path.join(root, "plugins")],
1285
+ dataDir: path.join(root, ".flux-data"),
1286
+ dev: true,
1287
+ assetUrl: (id, rel) => \`/@plugins/\${id}/\${rel.replace(/^\\.\\//, "")}\`,
1288
+ });
1289
+ await fw.start();
1290
+ const srv = await serveWeb(fw, { distDir: path.join(root, "dist"), port: Number(process.env["PORT"] ?? 5173) });
1291
+ console.log(\`${v.name} \u2192 http://127.0.0.1:\${srv.port}\`);
1292
+ `,
1293
+ "src/renderer.tsx": renderer(v.name),
1294
+ "README.md": `# ${v.name}
1295
+
1296
+ Created with [fluxplugin](https://www.npmjs.com/package/fluxplugin).
1297
+
1298
+ \`\`\`bash
1299
+ npm install
1300
+ npm run dev:web # in a browser, no Electron needed
1301
+ npm run dev # in Electron (hot reload for plugins)
1302
+ npm run build && npm start
1303
+ npm run plugin:new my-plugin -- -t backend # scaffold a plugin in ./plugins
1304
+ \`\`\`
1305
+
1306
+ * \`src/renderer.tsx\` \u2014 your React UI. \`<PluginSlot name="\u2026"/>\` and extension points are where plugins plug in.
1307
+ * \`src/framework.ts\` \u2014 host-side setup (permissions policy, host operations exposed to plugins, hooks).
1308
+ * \`plugins/\` \u2014 plugins loaded at startup; users can also install \`.tgz\` plugins from the Plugins page.
1309
+ `
1310
+ };
1311
+ if (withSample) {
1312
+ const sample = TEMPLATES.dashboard(varsFromName("hello"));
1313
+ for (const [rel, content] of Object.entries(sample)) {
1314
+ if (rel === "package.json" || rel === "tsconfig.json" || rel === "README.md") continue;
1315
+ files[`plugins/hello/${rel}`] = content;
1316
+ }
1317
+ }
1318
+ return files;
1319
+ }
1320
+ var renderer = (app) => `import React from "react";
1321
+ import { createRoot } from "react-dom/client";
1322
+ import * as jsxRuntime from "react/jsx-runtime";
1323
+ import * as ReactDOMClient from "react-dom/client";
1324
+ import * as flux from "fluxplugin";
1325
+ import * as fluxReact from "fluxplugin/react";
1326
+ import { s, type FluxBridge } from "fluxplugin";
1327
+ import {
1328
+ Breadcrumbs, CommandPalette, Link, PluginProvider, PluginRoutes, PluginSlot, RendererFramework, StyleHost,
1329
+ baseCss, createHttpBridge, installSharedModules, useExtensionItems, useKeybindings, useMenu,
1330
+ } from "fluxplugin/react";
1331
+ import { PluginManagerUI } from "fluxplugin/manager-ui";
1332
+
1333
+ // One copy of React and of the runtime, shared with plugin bundles (they import these from globalThis).
1334
+ installSharedModules({
1335
+ react: React, "react/jsx-runtime": jsxRuntime, "react/jsx-dev-runtime": jsxRuntime, "react-dom/client": ReactDOMClient,
1336
+ fluxplugin: flux, "fluxplugin/react": fluxReact,
1337
+ });
1338
+
1339
+ // Electron: the preload bridge. Browser mode: HTTP bridge to \`fluxplugin dev --web\`.
1340
+ const bridge: FluxBridge = (window as unknown as { __fluxplugin?: FluxBridge }).__fluxplugin ?? createHttpBridge();
1341
+ const framework = new RendererFramework({ bridge });
1342
+
1343
+ // Declare extension points BEFORE plugins start. Plugins contribute validated data to them.
1344
+ framework.manager.extensionPoints.declare({
1345
+ name: "dashboard",
1346
+ contributions: { toolbar: s.object({ id: s.string(), label: s.string(), command: s.string().optional() }) },
1347
+ });
1348
+
1349
+ const css = baseCss + \`
1350
+ body{background:var(--flux-bg);color:var(--flux-fg)}
1351
+ .app{display:grid;grid-template-columns:220px 1fr;min-height:100vh}
1352
+ .side{border-right:1px solid var(--flux-border);padding:16px;background:var(--flux-surface)}
1353
+ .side a{display:block;padding:7px 10px;border-radius:8px;color:inherit;text-decoration:none;margin:2px 0}
1354
+ .side a[data-active]{background:var(--flux-accent);color:#fff}.side h1{font-size:16px;margin:0 0 14px}
1355
+ .main{padding:24px;max-width:860px}.row{display:flex;gap:8px;margin:12px 0}
1356
+ .btn{border:1px solid var(--flux-border);background:var(--flux-bg);color:inherit;border-radius:8px;padding:6px 12px;cursor:pointer}
1357
+ .hint{color:var(--flux-muted);font-size:12px}\`;
1358
+
1359
+ function Dashboard() {
1360
+ const tools = useExtensionItems<{ id: string; label: string; command?: string }>("dashboard", "toolbar");
1361
+ const [out, setOut] = React.useState("");
1362
+ const create = async (name: string) => {
1363
+ try { setOut(JSON.stringify(await bridge.invoke("host.item.create", { name }))); }
1364
+ catch (e) { setOut("blocked: " + (e as Error).message); }
1365
+ };
1366
+ return (
1367
+ <div>
1368
+ <h2>Dashboard</h2>
1369
+ <PluginSlot name="dashboard.before" />
1370
+ <p className="hint">These buttons are contributed by plugins through a host-declared extension point.</p>
1371
+ <div className="row" data-testid="toolbar">
1372
+ {tools.map((t) => <button key={t.id} className="btn" onClick={() => t.command && void framework.manager.commands.execute(t.command)}>{t.label}</button>)}
1373
+ </div>
1374
+ <div className="row">
1375
+ <button className="btn" onClick={() => void create(" Ada ")}>Create item \u201C Ada \u201D (plugin hooks may rewrite it)</button>
1376
+ <button className="btn" onClick={() => void create(" ")}>Create empty item (a plugin may veto)</button>
1377
+ </div>
1378
+ <pre data-testid="hook-result" className="hint">{out}</pre>
1379
+ <PluginSlot name="dashboard.after" />
1380
+ </div>
1381
+ );
1382
+ }
1383
+
1384
+ function Shell() {
1385
+ useKeybindings();
1386
+ const { items, run } = useMenu("sidebar");
1387
+ return (
1388
+ <div className="app">
1389
+ <aside className="side">
1390
+ <h1>${"${APP}"} <PluginSlot name="header.badges" /></h1>
1391
+ <Link to="/">Dashboard</Link>
1392
+ {items.map((i) => <a key={i.id} href={"#" + i.path} onClick={(e) => { e.preventDefault(); run(i); }}>{i.label}</a>)}
1393
+ <Link to="/plugins">Plugins</Link>
1394
+ <p className="hint">Ctrl/Cmd+K: commands</p>
1395
+ </aside>
1396
+ <main className="main"><Breadcrumbs /><PluginRoutes /></main>
1397
+ <CommandPalette />
1398
+ </div>
1399
+ );
1400
+ }
1401
+
1402
+ framework.manager.routes.register({ path: "/", component: Dashboard, meta: { title: "Dashboard" } }, "host");
1403
+ framework.manager.routes.register({ path: "/plugins", component: PluginManagerUI, meta: { title: "Plugins", breadcrumb: "Plugins" } }, "host");
1404
+
1405
+ await framework.start();
1406
+ (window as unknown as Record<string, unknown>)["__framework"] = framework;
1407
+ createRoot(document.getElementById("root")!).render(
1408
+ <PluginProvider framework={framework}><style>{css}</style><StyleHost /><Shell /></PluginProvider>,
1409
+ );
1410
+ `.replace("${APP}", app);
1411
+
1412
+ // src/cli/cli.ts
1413
+ var HELP = `fluxplugin <command>
1414
+
1415
+ init [dir] create a new app (Electron + React) that supports plugins
1416
+ dev [--web] run your app with plugin hot reload (Electron, or a browser with --web)
1417
+ build bundle your app (in an app folder) or a plugin (in a plugin folder)
1418
+ start [--web] run the built app
1419
+ plugin new <name> [-t basic|dashboard|backend] scaffold a plugin (alias: create)
1420
+ validate [dir] check a plugin manifest, entries and integrity
1421
+ package [dir] [--out d] [--sign-key file] build + integrity (+ signature) + .tgz
1422
+ install <dir|.tgz> [--dir plugins] uninstall <id> enable <id> disable <id> list
1423
+ keygen [--out name] create an ed25519 signing key pair
1424
+ tailwind [--dir plugins] [--out file] generate @source lines for Tailwind v4`;
1425
+ var log = (...a) => console.log(...a);
1426
+ async function readManifest2(root) {
1427
+ return JSON.parse(await fs8.readFile(path9.join(root, "plugin.json"), "utf8"));
1428
+ }
1429
+ async function run(argv) {
1430
+ const { values: f, positionals: pos } = parseArgs({
1431
+ args: argv,
1432
+ allowPositionals: true,
1433
+ 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" } }
1434
+ });
1435
+ let [cmd, arg] = pos;
1436
+ if (cmd === "plugin") {
1437
+ const sub = pos[1];
1438
+ if (sub !== "new") throw new Error("usage: fluxplugin plugin new <name>");
1439
+ cmd = "create";
1440
+ arg = pos[2];
1441
+ }
1442
+ if (!cmd || f.help) return log(HELP), cmd ? 0 : 1;
1443
+ const root = path9.resolve(arg ?? ".");
1444
+ try {
1445
+ switch (cmd) {
1446
+ case "init": {
1447
+ const target = path9.resolve(arg ?? ".");
1448
+ const entries = await fs8.readdir(target).catch(() => []);
1449
+ if (entries.length) throw new Error(`${target} is not empty`);
1450
+ const name = (f.name ?? path9.basename(target)).toLowerCase().replace(/[^a-z0-9-_.]+/g, "-");
1451
+ const files = hostFiles({ name, fluxSpec: process.env["FLUXPLUGIN_SPEC"] ?? `^${await ownVersion()}` }, !f["no-sample"]);
1452
+ for (const [rel2, content] of Object.entries(files)) {
1453
+ await fs8.mkdir(path9.dirname(path9.join(target, rel2)), { recursive: true });
1454
+ await fs8.writeFile(path9.join(target, rel2), content);
1455
+ }
1456
+ const rel = path9.relative(process.cwd(), target) || ".";
1457
+ log(`\u2714 created ${name} in ${target}
1458
+
1459
+ Next steps:
1460
+ ${rel === "." ? "" : `cd ${rel}
1461
+ `}npm install
1462
+ npm run dev:web # browser mode (no Electron needed)
1463
+ npm run dev # Electron
1464
+
1465
+ Add a plugin: npm run plugin:new my-plugin -- -t backend`);
1466
+ return 0;
1467
+ }
1468
+ case "create": {
1469
+ if (!arg) throw new Error("usage: fluxplugin plugin new <name> [-t basic|dashboard|backend]");
1470
+ const tpl = f.template ?? "basic";
1471
+ if (!(tpl in TEMPLATES)) throw new Error(`unknown template "${tpl}"`);
1472
+ const base = await isHostProject(process.cwd()) && !path9.isAbsolute(arg) && !arg.includes("/") ? path9.join((await loadConfig({}, process.cwd())).pluginsDir, arg) : path9.resolve(arg);
1473
+ if (await fs8.stat(base).catch(() => null)) throw new Error(`${base} already exists`);
1474
+ const files = TEMPLATES[tpl](varsFromName(path9.basename(base)));
1475
+ for (const [rel, content] of Object.entries(files)) {
1476
+ await fs8.mkdir(path9.dirname(path9.join(base, rel)), { recursive: true });
1477
+ await fs8.writeFile(path9.join(base, rel), content);
1478
+ }
1479
+ log(`\u2714 created ${tpl} plugin in ${base}
1480
+ next: fluxplugin build ${path9.relative(process.cwd(), base)}`);
1481
+ return 0;
1482
+ }
1483
+ case "build": {
1484
+ if (!arg && await isHostProject(process.cwd())) {
1485
+ await buildHost(process.cwd());
1486
+ return 0;
1487
+ }
1488
+ await buildPlugin(root);
1489
+ return 0;
1490
+ }
1491
+ case "dev": {
1492
+ const cwd = process.cwd();
1493
+ if (arg || !await isHostProject(cwd)) {
1494
+ await buildPlugin(root, true);
1495
+ await new Promise(() => {
1496
+ });
1497
+ }
1498
+ const dispose = await buildHost(cwd, { watch: true });
1499
+ const { child, mode } = startApp(cwd, { web: f.web, dev: true, port: f.port ? Number(f.port) : void 0 });
1500
+ log(`running in ${mode} mode \u2014 edit a plugin and it reloads; Ctrl+C to stop`);
1501
+ const stop = () => {
1502
+ child.kill();
1503
+ void dispose().finally(() => process.exit(0));
1504
+ };
1505
+ process.on("SIGINT", stop);
1506
+ process.on("SIGTERM", stop);
1507
+ return await new Promise((r) => child.on("exit", (c) => r(c ?? 0)));
1508
+ }
1509
+ case "start": {
1510
+ const cwd = process.cwd();
1511
+ if (!await fs8.stat(path9.join(cwd, "dist/index.html")).catch(() => null)) throw new Error("nothing built yet: run `fluxplugin build` first");
1512
+ const { child } = startApp(cwd, { web: f.web, port: f.port ? Number(f.port) : void 0 });
1513
+ return await new Promise((r) => child.on("exit", (c) => r(c ?? 0)));
1514
+ }
1515
+ case "validate":
1516
+ return validate(root);
1517
+ case "package":
1518
+ return pack(root, f.out, f["sign-key"]);
1519
+ case "keygen": {
1520
+ const k = generateSigningKeyPair();
1521
+ const base = f.out ?? "flux-signing";
1522
+ await fs8.writeFile(`${base}.private.key`, k.privateKey, { mode: 384 });
1523
+ await fs8.writeFile(`${base}.public.key`, k.publicKey);
1524
+ log(`wrote ${base}.private.key (keep secret) and ${base}.public.key (distribute to hosts as a trusted key)`);
1525
+ return 0;
1526
+ }
1527
+ case "tailwind": {
1528
+ const cfg = await loadConfig(f);
1529
+ const names = (await fs8.readdir(cfg.pluginsDir).catch(() => [])).sort();
1530
+ const lines = names.map((n) => `@source "${path9.join(cfg.pluginsDir, n, "src").replace(/\\/g, "/")}";`);
1531
+ const css = `/* generated by fluxplugin tailwind */
1532
+ ${lines.join("\n")}
1533
+ `;
1534
+ if (f.out) await fs8.writeFile(f.out, css);
1535
+ else log(css);
1536
+ return 0;
1537
+ }
1538
+ case "install":
1539
+ case "uninstall":
1540
+ case "enable":
1541
+ case "disable":
1542
+ case "list":
1543
+ return manage(cmd, arg, f);
1544
+ default:
1545
+ log(HELP);
1546
+ return 1;
1547
+ }
1548
+ } catch (e) {
1549
+ console.error(`error: ${e.message}`);
1550
+ return 1;
1551
+ }
1552
+ }
1553
+ async function validate(root) {
1554
+ const raw = await readManifest2(root);
1555
+ const v = validateManifest(raw);
1556
+ let bad = !v.ok;
1557
+ v.issues.forEach((i) => log(`\u2716 ${i.path || "<root>"}: ${i.message}`));
1558
+ v.warnings.forEach((w) => log(`! ${w}`));
1559
+ if (v.manifest) {
1560
+ for (const rel of [v.manifest.main, v.manifest.renderer, ...v.manifest.styles ?? []].filter(Boolean))
1561
+ if (!await fs8.stat(path9.join(root, rel)).catch(() => null)) log(`\u2716 missing file ${rel} (run build)`), bad = true;
1562
+ try {
1563
+ await verifyPlugin(root, raw, v.manifest);
1564
+ } catch (e) {
1565
+ log(`\u2716 ${e.message}`);
1566
+ bad = true;
1567
+ }
1568
+ }
1569
+ log(bad ? "invalid" : `\u2714 ${v.manifest?.id}@${v.manifest?.version} is valid`);
1570
+ return bad ? 1 : 0;
1571
+ }
1572
+ async function pack(root, out = ".", keyFile) {
1573
+ await buildPlugin(root);
1574
+ let raw = await readManifest2(root);
1575
+ const m = validateManifest(raw).manifest;
1576
+ if (!m) throw new Error("invalid manifest");
1577
+ raw["integrity"] = await computeIntegrity(root, m);
1578
+ if (keyFile) raw = signManifest(raw, (await fs8.readFile(keyFile, "utf8")).trim());
1579
+ const stage = await fs8.mkdtemp(path9.join((await import("node:os")).tmpdir(), "flux-pack-"));
1580
+ await copyDir(root, stage);
1581
+ await fs8.rm(path9.join(stage, "src"), { recursive: true, force: true });
1582
+ await fs8.writeFile(path9.join(stage, "plugin.json"), JSON.stringify(raw, null, 2));
1583
+ const buf = await createTarGz(stage);
1584
+ await fs8.rm(stage, { recursive: true, force: true });
1585
+ await fs8.mkdir(out, { recursive: true });
1586
+ const file = path9.join(out, `${m.id}-${m.version}.tgz`);
1587
+ await fs8.writeFile(file, buf);
1588
+ log(`packaged ${file}
1589
+ sha256 ${crypto2.createHash("sha256").update(buf).digest("hex")}${keyFile ? "\n signed" : ""}`);
1590
+ return 0;
1591
+ }
1592
+ async function manage(cmd, arg, f) {
1593
+ const cfg = await loadConfig(f);
1594
+ const state = new JsonStateFile(path9.join(cfg.dataDir, "plugins-state.json"));
1595
+ const inst = new PluginInstaller(cfg.pluginsDir, path9.join(cfg.dataDir, "backups"));
1596
+ if (cmd === "list") {
1597
+ const { disabled } = await state.readAll();
1598
+ for (const m of await inst.installed()) log(`${disabled.includes(m.id) ? "\u25CB" : "\u25CF"} ${m.id}@${m.version} ${m.name}`);
1599
+ return 0;
1600
+ }
1601
+ if (!arg) throw new Error(`usage: ${cmd} <${cmd === "install" ? "dir|.tgz" : "id"}>`);
1602
+ if (cmd === "install") {
1603
+ const tmp = path9.join(cfg.dataDir, ".install", String(Date.now()));
1604
+ if (arg.endsWith(".tgz")) await extractTarGz(await fs8.readFile(arg), tmp);
1605
+ else await copyDir(path9.resolve(arg), tmp);
1606
+ const v = validateManifest(await readManifest2(tmp));
1607
+ if (!v.manifest) throw new Error("invalid plugin: " + v.issues.map((i) => i.message).join("; "));
1608
+ const reg = new LocalRegistry(path9.dirname(tmp), "cli");
1609
+ inst.addRegistry(reg);
1610
+ const [entry] = (await reg.search(v.manifest.id)).filter((e) => e.id === v.manifest.id);
1611
+ const r = await inst.install({ ...entry, location: tmp });
1612
+ await fs8.rm(tmp, { recursive: true, force: true });
1613
+ log(`installed ${r.id}@${r.version}${r.previousVersion ? ` (was ${r.previousVersion}; rollback available)` : ""}`);
1614
+ } else if (cmd === "uninstall") {
1615
+ await inst.uninstall(arg);
1616
+ log(`uninstalled ${arg}`);
1617
+ } else {
1618
+ await state.update((s2) => ({ disabled: cmd === "disable" ? [.../* @__PURE__ */ new Set([...s2.disabled, arg])] : s2.disabled.filter((x) => x !== arg) }));
1619
+ log(`${cmd}d ${arg}`);
1620
+ }
1621
+ return 0;
1622
+ }
1623
+ async function ownVersion() {
1624
+ for (const rel of ["../package.json", "../../package.json"]) {
1625
+ try {
1626
+ const j = JSON.parse(await fs8.readFile(new URL(rel, import.meta.url), "utf8"));
1627
+ if (j.name === "fluxplugin" && j.version) return j.version;
1628
+ } catch {
1629
+ }
1630
+ }
1631
+ return "1.0.0";
1632
+ }
1633
+
1634
+ // src/cli/bin.ts
1635
+ process.exit(await run(process.argv.slice(2)));