@pipelab/core-node 1.0.0-beta.11 → 1.0.0-beta.13

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/dist/index.mjs CHANGED
@@ -2,11 +2,11 @@ import { createRequire } from "node:module";
2
2
  import path, { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
3
3
  import { homedir, platform, tmpdir } from "node:os";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import fs, { createWriteStream, existsSync, readdirSync, statSync } from "node:fs";
5
+ import fs, { createWriteStream, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
6
  import { WebSocket, WebSocketServer as WebSocketServer$1 } from "ws";
7
7
  import http from "node:http";
8
8
  import { nanoid } from "nanoid";
9
- import { SubscriptionRequiredError, WebSocketError, appSettingsMigrator, configRegistry, fileRepoMigrations, isRequired, isSupabaseAvailable, isWebSocketRequestMessage, processGraph, savedFileMigrator, supabase, useLogger, usePlugins } from "@pipelab/shared";
9
+ import { SubscriptionRequiredError, WebSocketError, appSettingsMigrator, configRegistry, fileRepoMigrations, isRequired, isSupabaseAvailable, isWebSocketRequestMessage, normalizePipelineConfig, processGraph, savedFileMigrator, supabase, transformUrl, useLogger, usePlugins } from "@pipelab/shared";
10
10
  import { getUiDevServerMissingWarning, uiDevPort, websocketPort } from "@pipelab/constants";
11
11
  import fs$1, { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
12
12
  import { execa } from "execa";
@@ -454,7 +454,7 @@ const registerFsHandlers = (_context) => {
454
454
  const ensure = async (filesPath, defaultContent = "{}") => {
455
455
  await mkdir(dirname(filesPath), { recursive: true });
456
456
  try {
457
- await access(filesPath);
457
+ if ((await stat(filesPath)).size === 0) await writeFile(filesPath, defaultContent);
458
458
  } catch {
459
459
  await writeFile(filesPath, defaultContent);
460
460
  }
@@ -601,7 +601,7 @@ async function getFolderSize(dirPath) {
601
601
  //#region src/config.ts
602
602
  const getMigrator = (name) => {
603
603
  if (configRegistry[name]) return configRegistry[name];
604
- if (name.startsWith("pipeline-")) return configRegistry["pipeline"];
604
+ if (name.startsWith("pipeline-") || path.isAbsolute(name) || name.endsWith(".json")) return configRegistry["pipeline"];
605
605
  };
606
606
  const setupConfigFile = async (name, options) => {
607
607
  const ctx = options.context;
@@ -624,11 +624,13 @@ const setupConfigFile = async (name, options) => {
624
624
  const { logger } = useLogger();
625
625
  let content = void 0;
626
626
  let originalJson = void 0;
627
+ let parseFailed = false;
627
628
  try {
628
629
  content = await fs$1.readFile(filesPath, "utf8");
629
630
  if (content !== void 0) originalJson = JSON.parse(content);
630
631
  } catch (e) {
631
632
  logger().error(`Error reading or parsing config ${name}:`, e);
633
+ parseFailed = true;
632
634
  }
633
635
  let json = void 0;
634
636
  try {
@@ -649,7 +651,9 @@ const setupConfigFile = async (name, options) => {
649
651
  logger().error(`Error migrating config ${name}:`, e);
650
652
  json = migrator.defaultValue;
651
653
  }
652
- if (originalJson?.version !== json?.version || content === void 0) try {
654
+ let normalized = false;
655
+ if (name.startsWith("pipeline-") || path.isAbsolute(name) || name.endsWith(".json") || name === "pipeline") normalized = normalizePipelineConfig(json);
656
+ if (originalJson?.version !== json?.version || normalized || content === void 0 || parseFailed) try {
653
657
  await fs$1.writeFile(filesPath, JSON.stringify(json));
654
658
  } catch (e) {
655
659
  logger().error(`Error saving migrated config ${name}:`, e);
@@ -740,6 +744,40 @@ const registerConfigHandlers = (context) => {
740
744
  });
741
745
  }
742
746
  });
747
+ handle("config:delete", async (_, { send, value }) => {
748
+ const { config: name } = value;
749
+ logger().info("config:delete", name);
750
+ try {
751
+ const filesPath = path.isAbsolute(name) ? name : context.getConfigPath(`${name}.json`);
752
+ await fs$1.rm(filesPath, { force: true });
753
+ const parsedPath = path.parse(filesPath);
754
+ const dirEntries = await fs$1.readdir(parsedPath.dir).catch(() => []);
755
+ const prefix = `${parsedPath.name}.v`;
756
+ const suffix = `.json`;
757
+ for (const entry of dirEntries) if (entry.startsWith(prefix) && entry.endsWith(suffix)) {
758
+ const backupPath = path.join(parsedPath.dir, entry);
759
+ await fs$1.rm(backupPath, { force: true }).catch((err) => {
760
+ logger().error(`Failed to delete backup ${backupPath}:`, err);
761
+ });
762
+ }
763
+ send({
764
+ type: "end",
765
+ data: {
766
+ type: "success",
767
+ result: { result: "ok" }
768
+ }
769
+ });
770
+ } catch (e) {
771
+ logger().error(`config:delete error for ${name}:`, e);
772
+ send({
773
+ type: "end",
774
+ data: {
775
+ type: "error",
776
+ ipcError: e instanceof Error ? e.message : `Unable to delete config ${name}`
777
+ }
778
+ });
779
+ }
780
+ });
743
781
  };
744
782
  //#endregion
745
783
  //#region src/handlers/build-history.ts
@@ -8843,6 +8881,35 @@ async function serveCommand(options, version, _dirname) {
8843
8881
  let rawAssetFolder;
8844
8882
  if (!isDev) rawAssetFolder = await fetchPipelabAsset("@pipelab/ui", releaseTag, { context });
8845
8883
  const server = http$1.createServer(async (request, response) => {
8884
+ if (request.url?.startsWith("/media-file/")) {
8885
+ const encodedPath = request.url.substring(12);
8886
+ const filePath = decodeURIComponent(encodedPath);
8887
+ const normalizedPath = filePath.startsWith("/") && filePath.match(/^\/[a-zA-Z]:/) ? filePath.substring(1) : filePath;
8888
+ if (existsSync(normalizedPath)) try {
8889
+ const content = await readFile(normalizedPath);
8890
+ let contentType = "application/octet-stream";
8891
+ if (normalizedPath.endsWith(".png")) contentType = "image/png";
8892
+ else if (normalizedPath.endsWith(".jpg") || normalizedPath.endsWith(".jpeg")) contentType = "image/jpeg";
8893
+ else if (normalizedPath.endsWith(".svg")) contentType = "image/svg+xml";
8894
+ else if (normalizedPath.endsWith(".gif")) contentType = "image/gif";
8895
+ else if (normalizedPath.endsWith(".webp")) contentType = "image/webp";
8896
+ response.writeHead(200, {
8897
+ "Content-Type": contentType,
8898
+ "Access-Control-Allow-Origin": "*"
8899
+ });
8900
+ response.end(content);
8901
+ return;
8902
+ } catch (e) {
8903
+ response.writeHead(500, { "Content-Type": "text/plain" });
8904
+ response.end(`Error reading file: ${e}`);
8905
+ return;
8906
+ }
8907
+ else {
8908
+ response.writeHead(404, { "Content-Type": "text/plain" });
8909
+ response.end(`File not found: ${normalizedPath}`);
8910
+ return;
8911
+ }
8912
+ }
8846
8913
  if (isDev) {
8847
8914
  response.writeHead(200, { "Content-Type": "text/html" });
8848
8915
  response.end(`
@@ -8943,7 +9010,9 @@ async function fetchPackage(packageName, versionOrRange, options) {
8943
9010
  const ctx = options.context;
8944
9011
  const baseDir = ctx.getPackagesPath(packageName);
8945
9012
  let resolvedVersion;
8946
- console.log(`[Fetcher] Resolving ${packageName}@${versionOrRange || "latest"}...`);
9013
+ let resolvedVersionOrRange = versionOrRange;
9014
+ if (resolvedVersionOrRange === "local") resolvedVersionOrRange = "latest";
9015
+ console.log(`[Fetcher] Resolving ${packageName}@${resolvedVersionOrRange || "latest"}...`);
8947
9016
  const resolveStart = Date.now();
8948
9017
  try {
8949
9018
  const cachePath = join(ctx.userDataPath, "cache", "pacote");
@@ -8954,7 +9023,7 @@ async function fetchPackage(packageName, versionOrRange, options) {
8954
9023
  }
8955
9024
  const packument = await packumentPromise;
8956
9025
  const versions = Object.keys(packument.versions);
8957
- const range = versionOrRange || "latest";
9026
+ const range = resolvedVersionOrRange || "latest";
8958
9027
  const foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
8959
9028
  if (!foundVersion) throw new Error(`Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(packument["dist-tags"] || {}).join(", ")})`);
8960
9029
  resolvedVersion = foundVersion;
@@ -8962,7 +9031,7 @@ async function fetchPackage(packageName, versionOrRange, options) {
8962
9031
  } catch (error) {
8963
9032
  console.warn(`[Fetcher] ${packageName}: remote resolution failed (${Date.now() - resolveStart}ms), trying local fallback...`);
8964
9033
  const fallbackStart = Date.now();
8965
- const fallbackVersion = await tryLocalFallback(versionOrRange, error, baseDir, packageName);
9034
+ const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, error, baseDir, packageName);
8966
9035
  if (fallbackVersion) {
8967
9036
  resolvedVersion = fallbackVersion;
8968
9037
  console.log(`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`);
@@ -9265,22 +9334,27 @@ async function crawlMonorepoPackages() {
9265
9334
  }
9266
9335
  return cache;
9267
9336
  }
9268
- async function findLatestLocalVersion(baseDir) {
9337
+ async function tryLocalFallback(versionOrRange, _error, baseDir, logPrefix) {
9269
9338
  if (!existsSync(baseDir)) return null;
9270
9339
  try {
9271
- return (await readdir(baseDir, { withFileTypes: true })).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name).sort((a, b) => b.localeCompare(a, void 0, {
9272
- numeric: true,
9273
- sensitivity: "base"
9274
- }))[0] || null;
9275
- } catch {
9276
- return null;
9277
- }
9278
- }
9279
- async function tryLocalFallback(_version, _error, baseDir, logPrefix) {
9280
- const latestLocal = await findLatestLocalVersion(baseDir);
9281
- if (latestLocal) {
9282
- console.info(`[Fetcher] ${logPrefix}: Using locally cached version: ${latestLocal}`);
9283
- return latestLocal;
9340
+ const localVersions = (await readdir(baseDir, { withFileTypes: true })).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name).filter((name) => !!semver.valid(name));
9341
+ if (localVersions.length === 0) return null;
9342
+ const range = versionOrRange || "latest";
9343
+ if (range === "latest") {
9344
+ const latestLocal = localVersions.sort((a, b) => semver.rcompare(a, b))[0] || null;
9345
+ if (latestLocal) {
9346
+ console.info(`[Fetcher] ${logPrefix}: Using locally cached latest version: ${latestLocal}`);
9347
+ return latestLocal;
9348
+ }
9349
+ } else {
9350
+ const matched = semver.maxSatisfying(localVersions, range);
9351
+ if (matched) {
9352
+ console.info(`[Fetcher] ${logPrefix}: Using locally cached matching version: ${matched} for range ${range}`);
9353
+ return matched;
9354
+ }
9355
+ }
9356
+ } catch (e) {
9357
+ console.warn(`[Fetcher] ${logPrefix}: Error during local fallback resolution:`, e);
9284
9358
  }
9285
9359
  return null;
9286
9360
  }
@@ -9314,50 +9388,6 @@ const checkParams = (definitionParams, elementParams) => {
9314
9388
  } else console.warn("Unexpected param \"" + param + "\"");
9315
9389
  for (const param of expected) if (isRequired(definitionParams[param])) throw new Error("Missing param \"" + param + "\"");
9316
9390
  };
9317
- const handleConditionExecute = async (nodeId, pluginId, params, cwd, context) => {
9318
- const ctx = context;
9319
- const { plugins } = usePlugins();
9320
- const { logger } = useLogger();
9321
- const node = plugins.value.find((plugin) => plugin.id === pluginId)?.nodes.find((node) => node.node.id === nodeId);
9322
- if (!node) return {
9323
- type: "error",
9324
- ipcError: "Node not found"
9325
- };
9326
- try {
9327
- if (!(await stat(cwd)).isDirectory()) throw new Error(`Execution directory "${cwd}" exists but is not a directory.`);
9328
- } catch (e) {
9329
- if (e.code === "ENOENT") await mkdir(cwd, { recursive: true });
9330
- else throw e;
9331
- }
9332
- try {
9333
- checkParams(node.node.params, params);
9334
- const resolvedInputs = params;
9335
- return {
9336
- type: "success",
9337
- result: {
9338
- outputs: {},
9339
- value: await node.runner({
9340
- inputs: resolvedInputs,
9341
- log: (...args) => {
9342
- logger().info(`[${node.node.name}]`, ...args);
9343
- },
9344
- meta: { definition: "" },
9345
- setMeta: () => {
9346
- logger().info("set meta defined here");
9347
- },
9348
- cwd,
9349
- context: ctx
9350
- })
9351
- }
9352
- };
9353
- } catch (e) {
9354
- logger().error("Error in condition execution:", e);
9355
- return {
9356
- type: "error",
9357
- ipcError: String(e)
9358
- };
9359
- }
9360
- };
9361
9391
  const handleActionExecute = async (nodeId, pluginId, params, mainWindow, send, abortSignal, cwd, cachePath, context) => {
9362
9392
  const ctx = context;
9363
9393
  const { plugins } = usePlugins();
@@ -9443,24 +9473,49 @@ const handleActionExecute = async (nodeId, pluginId, params, mainWindow, send, a
9443
9473
  };
9444
9474
  //#endregion
9445
9475
  //#region src/plugins-registry.ts
9446
- const DEFAULT_PLUGIN_IDS = [
9447
- "construct",
9448
- "filesystem",
9449
- "system",
9450
- "steam",
9451
- "itch",
9452
- "electron",
9453
- "discord",
9454
- "poki",
9455
- "nvpatch",
9456
- "tauri",
9457
- "minify",
9458
- "netlify"
9459
- ];
9476
+ const enhancePluginDefinition = async (plugin, packageDir, fallbackName, fallbackVersion) => {
9477
+ if (!plugin) return plugin;
9478
+ let packageName = fallbackName;
9479
+ let version = fallbackVersion;
9480
+ let pipelabMeta = null;
9481
+ let pkgDescription = "";
9482
+ try {
9483
+ const pkgJsonPath = join(packageDir, "package.json");
9484
+ if (existsSync(pkgJsonPath)) {
9485
+ const pkgContent = await readFile(pkgJsonPath, "utf8");
9486
+ const pkg = JSON.parse(pkgContent);
9487
+ if (pkg.name) packageName = pkg.name;
9488
+ if (pkg.version) version = pkg.version;
9489
+ if (pkg.description) pkgDescription = pkg.description;
9490
+ if (pkg.pipelab) pipelabMeta = pkg.pipelab;
9491
+ }
9492
+ } catch (e) {
9493
+ console.error(`[Plugins] Failed to read package.json in ${packageDir}:`, e);
9494
+ }
9495
+ plugin.packageName = packageName;
9496
+ plugin.id = packageName;
9497
+ plugin.version = fallbackVersion === "local" ? "local" : version;
9498
+ plugin.isOfficial = packageName.startsWith("@pipelab/");
9499
+ plugin.name = pipelabMeta?.name || packageName;
9500
+ plugin.description = pipelabMeta?.description || pkgDescription || "";
9501
+ if (pipelabMeta?.icon) if (typeof pipelabMeta.icon === "string") {
9502
+ let iconPath = pipelabMeta.icon;
9503
+ if (iconPath.startsWith(".")) iconPath = pathToFileURL(join(packageDir, iconPath)).href;
9504
+ plugin.icon = {
9505
+ type: "image",
9506
+ image: iconPath
9507
+ };
9508
+ } else plugin.icon = pipelabMeta.icon;
9509
+ else plugin.icon = {
9510
+ type: "icon",
9511
+ icon: "pi pi-box"
9512
+ };
9513
+ return plugin;
9514
+ };
9460
9515
  const loadPipelabPlugin = async (id, options) => {
9461
9516
  const start = Date.now();
9462
9517
  try {
9463
- const packageName = `@pipelab/plugin-${id}`;
9518
+ const packageName = id;
9464
9519
  const fetchStart = Date.now();
9465
9520
  const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, options.context.releaseTag, {
9466
9521
  context: options.context,
@@ -9480,13 +9535,68 @@ const loadPipelabPlugin = async (id, options) => {
9480
9535
  const importDuration = Date.now() - importStart;
9481
9536
  const totalDuration = Date.now() - start;
9482
9537
  console.log(`[Plugins] [${id}] Successfully loaded from: ${packageDir} (fetch: ${fetchDuration}ms, import: ${importDuration}ms, total: ${totalDuration}ms)`);
9483
- return pluginModule.default;
9538
+ const plugin = pluginModule.default;
9539
+ await enhancePluginDefinition(plugin, packageDir, packageName, options.context.releaseTag);
9540
+ return plugin;
9484
9541
  } catch (e) {
9485
9542
  console.error(`[Plugins] [${id}] CRITICAL: Failed to load after ${Date.now() - start}ms:`, e);
9486
9543
  if (e.code === "ERR_MODULE_NOT_FOUND") console.error(`[Plugins] [${id}] This usually means a dependency is missing in the plugin's node_modules.`);
9487
9544
  return null;
9488
9545
  }
9489
9546
  };
9547
+ const loadCustomPlugin = async (packageName, version, options) => {
9548
+ const start = Date.now();
9549
+ try {
9550
+ const fetchStart = Date.now();
9551
+ const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, version, {
9552
+ context: options.context,
9553
+ installDeps: false
9554
+ });
9555
+ const fetchDuration = Date.now() - fetchStart;
9556
+ console.log(`[Plugins] [${packageName}] Attempting to import custom plugin from: ${entryPoint}`);
9557
+ if (!existsSync(entryPoint)) {
9558
+ console.error(`[Plugins] [${packageName}] CRITICAL: Plugin entry point not found at ${entryPoint}`);
9559
+ return null;
9560
+ }
9561
+ const importStart = Date.now();
9562
+ const pluginModule = await import(pathToFileURL(entryPoint).href);
9563
+ const importDuration = Date.now() - importStart;
9564
+ const totalDuration = Date.now() - start;
9565
+ console.log(`[Plugins] [${packageName}] Successfully loaded custom plugin from: ${packageDir} (fetch: ${fetchDuration}ms, import: ${importDuration}ms, total: ${totalDuration}ms)`);
9566
+ const plugin = pluginModule.default;
9567
+ await enhancePluginDefinition(plugin, packageDir, packageName, version);
9568
+ return plugin;
9569
+ } catch (e) {
9570
+ console.error(`[Plugins] [${packageName}] CRITICAL: Failed to load after ${Date.now() - start}ms:`, e);
9571
+ return null;
9572
+ }
9573
+ };
9574
+ async function findInstalledPlugins(packagesDir) {
9575
+ const installed = [];
9576
+ if (!existsSync(packagesDir)) return installed;
9577
+ async function scan(dir, depth = 0) {
9578
+ if (depth > 4) return;
9579
+ try {
9580
+ const entries = await readdir(dir, { withFileTypes: true });
9581
+ if (entries.some((e) => e.isFile() && e.name === "package.json")) {
9582
+ try {
9583
+ const content = await readFile(join(dir, "package.json"), "utf8");
9584
+ const pkg = JSON.parse(content);
9585
+ if (pkg.name && pkg.name !== "pnpm") installed.push({
9586
+ name: pkg.name,
9587
+ version: pkg.version || "0.0.0",
9588
+ packageDir: dir,
9589
+ description: pkg.description || ""
9590
+ });
9591
+ } catch (e) {}
9592
+ return;
9593
+ }
9594
+ for (const entry of entries) if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) await scan(join(dir, entry.name), depth + 1);
9595
+ } catch (e) {}
9596
+ }
9597
+ await scan(packagesDir);
9598
+ return installed;
9599
+ }
9490
9600
  const builtInPlugins = async (options) => {
9491
9601
  console.log("[Plugins] Starting background plugin loading...");
9492
9602
  sendStartupProgress("Preparing environment...");
@@ -9498,17 +9608,64 @@ const builtInPlugins = async (options) => {
9498
9608
  const { webSocketServer } = await import("./index.mjs");
9499
9609
  (async () => {
9500
9610
  const totalStart = Date.now();
9501
- for (const id of DEFAULT_PLUGIN_IDS) {
9502
- sendStartupProgress(`Loading plugin: ${id}`);
9611
+ const pluginsToLoad = /* @__PURE__ */ new Map();
9612
+ for (const name of [
9613
+ "@pipelab/plugin-construct",
9614
+ "@pipelab/plugin-filesystem",
9615
+ "@pipelab/plugin-system",
9616
+ "@pipelab/plugin-steam",
9617
+ "@pipelab/plugin-itch",
9618
+ "@pipelab/plugin-electron",
9619
+ "@pipelab/plugin-discord",
9620
+ "@pipelab/plugin-poki",
9621
+ "@pipelab/plugin-nvpatch",
9622
+ "@pipelab/plugin-tauri",
9623
+ "@pipelab/plugin-minify",
9624
+ "@pipelab/plugin-netlify"
9625
+ ]) pluginsToLoad.set(name, "latest");
9626
+ if (isDev && projectRoot) {
9627
+ const pluginsDir = join(projectRoot, "plugins");
9628
+ if (existsSync(pluginsDir)) try {
9629
+ const entries = await readdir(pluginsDir, { withFileTypes: true });
9630
+ for (const entry of entries) if (entry.isDirectory()) {
9631
+ const pkgPath = join(pluginsDir, entry.name, "package.json");
9632
+ if (existsSync(pkgPath)) try {
9633
+ const pkgContent = await readFile(pkgPath, "utf-8");
9634
+ const pkg = JSON.parse(pkgContent);
9635
+ if (pkg.name && pkg.name.startsWith("@pipelab/plugin-") && pkg.name !== "@pipelab/plugin-core") pluginsToLoad.set(pkg.name, "local");
9636
+ } catch (e) {
9637
+ console.error(`[Plugins] Failed to parse package.json for ${entry.name}:`, e);
9638
+ }
9639
+ }
9640
+ } catch (e) {
9641
+ console.error(`[Plugins] Failed to scan local workspace plugins directory:`, e);
9642
+ }
9643
+ }
9644
+ try {
9645
+ const { setupConfigFile } = await import("./config-CEkOf95p.mjs");
9646
+ const settingsPlugins = (await (await setupConfigFile("settings", { context: options.context })).getConfig())?.plugins || [];
9647
+ for (const plugin of settingsPlugins) if (plugin.name) if (plugin.enabled) {
9648
+ if (!pluginsToLoad.has(plugin.name)) pluginsToLoad.set(plugin.name, "latest");
9649
+ } else pluginsToLoad.delete(plugin.name);
9650
+ } catch (e) {
9651
+ console.error(`[Plugins] Failed to load settings config on startup:`, e);
9652
+ }
9653
+ console.log(`[Plugins] Total plugins to load on startup:`, Array.from(pluginsToLoad.entries()));
9654
+ for (const [packageName, version] of pluginsToLoad.entries()) {
9655
+ sendStartupProgress(`Loading plugin: ${packageName}`);
9503
9656
  const pluginStart = Date.now();
9504
- const plugin = await loadPipelabPlugin(id, options);
9505
- console.log(`[Plugins] [${id}] loadPipelabPlugin loop step took ${Date.now() - pluginStart}ms`);
9506
- if (plugin) {
9507
- registerPlugins([plugin]);
9508
- webSocketServer.broadcast("plugin:loaded", { plugin });
9657
+ try {
9658
+ const plugin = await loadCustomPlugin(packageName, version, options);
9659
+ if (plugin) {
9660
+ registerPlugins([plugin]);
9661
+ webSocketServer.broadcast("plugin:loaded", { plugin });
9662
+ console.log(`[Plugins] Loaded ${packageName}@${version} in ${Date.now() - pluginStart}ms`);
9663
+ }
9664
+ } catch (err) {
9665
+ console.error(`[Plugins] Failed to load plugin ${packageName} at startup:`, err);
9509
9666
  }
9510
9667
  }
9511
- console.log(`[Plugins] All default plugins loaded in ${Date.now() - totalStart}ms.`);
9668
+ console.log(`[Plugins] All startup plugins loaded in ${Date.now() - totalStart}ms.`);
9512
9669
  sendStartupProgress("All plugins loaded.");
9513
9670
  setTimeout(() => {
9514
9671
  webSocketServer.broadcast("startup:progress", { type: "ready" });
@@ -9523,10 +9680,6 @@ const getFinalPlugins = () => {
9523
9680
  const finalPlugins = [];
9524
9681
  for (const plugin of plugins.value) {
9525
9682
  const finalNodes = [];
9526
- const transformUrl = (url) => {
9527
- if (url.startsWith("file://")) return url.replace("file://", "media://");
9528
- return url;
9529
- };
9530
9683
  const finalIcon = plugin.icon?.type === "image" ? {
9531
9684
  ...plugin.icon,
9532
9685
  image: transformUrl(plugin.icon.image)
@@ -9672,7 +9825,7 @@ const executeGraphWithHistory = async ({ graph, variables, projectName, projectP
9672
9825
  const c3toSteamPreset = async () => {
9673
9826
  return {
9674
9827
  data: {
9675
- version: "3.0.0",
9828
+ version: "5.0.0",
9676
9829
  name: "Construct 3 to Steam",
9677
9830
  description: "A basic project to get you started with Construct 3 and Steam",
9678
9831
  variables: [],
@@ -9680,8 +9833,9 @@ const c3toSteamPreset = async () => {
9680
9833
  triggers: [{
9681
9834
  type: "event",
9682
9835
  origin: {
9683
- pluginId: "system",
9684
- nodeId: "manual"
9836
+ pluginId: "@pipelab/plugin-system",
9837
+ nodeId: "manual",
9838
+ version: "latest"
9685
9839
  },
9686
9840
  uid: "manual-start",
9687
9841
  params: {}
@@ -9692,7 +9846,8 @@ const c3toSteamPreset = async () => {
9692
9846
  type: "action",
9693
9847
  origin: {
9694
9848
  nodeId: "export-construct-project",
9695
- pluginId: "construct"
9849
+ pluginId: "@pipelab/plugin-construct",
9850
+ version: "latest"
9696
9851
  },
9697
9852
  params: {
9698
9853
  file: {
@@ -9730,7 +9885,8 @@ const c3toSteamPreset = async () => {
9730
9885
  type: "action",
9731
9886
  origin: {
9732
9887
  nodeId: "unzip-file-node",
9733
- pluginId: "filesystem"
9888
+ pluginId: "@pipelab/plugin-filesystem",
9889
+ version: "latest"
9734
9890
  },
9735
9891
  params: { file: {
9736
9892
  editor: "editor",
@@ -9742,7 +9898,8 @@ const c3toSteamPreset = async () => {
9742
9898
  type: "action",
9743
9899
  origin: {
9744
9900
  nodeId: "electron:package:v2",
9745
- pluginId: "electron"
9901
+ pluginId: "@pipelab/plugin-electron",
9902
+ version: "latest"
9746
9903
  },
9747
9904
  params: {
9748
9905
  arch: {
@@ -9872,7 +10029,8 @@ const c3toSteamPreset = async () => {
9872
10029
  type: "action",
9873
10030
  origin: {
9874
10031
  nodeId: "steam-upload",
9875
- pluginId: "steam"
10032
+ pluginId: "@pipelab/plugin-steam",
10033
+ version: "latest"
9876
10034
  },
9877
10035
  params: {
9878
10036
  sdk: {
@@ -9915,7 +10073,8 @@ const c3toSteamPreset = async () => {
9915
10073
  type: "action",
9916
10074
  origin: {
9917
10075
  nodeId: "fs:open-in-explorer",
9918
- pluginId: "filesystem"
10076
+ pluginId: "@pipelab/plugin-filesystem",
10077
+ version: "latest"
9919
10078
  },
9920
10079
  params: { path: {
9921
10080
  editor: "editor",
@@ -9934,7 +10093,7 @@ const c3toSteamPreset = async () => {
9934
10093
  const newProjectPreset = async () => {
9935
10094
  return {
9936
10095
  data: {
9937
- version: "3.0.0",
10096
+ version: "5.0.0",
9938
10097
  name: "Empty project",
9939
10098
  description: "A default project with no tasks added",
9940
10099
  variables: [],
@@ -9942,8 +10101,9 @@ const newProjectPreset = async () => {
9942
10101
  triggers: [{
9943
10102
  type: "event",
9944
10103
  origin: {
9945
- pluginId: "system",
9946
- nodeId: "manual"
10104
+ pluginId: "@pipelab/plugin-system",
10105
+ nodeId: "manual",
10106
+ version: "latest"
9947
10107
  },
9948
10108
  uid: "manual-start",
9949
10109
  params: {}
@@ -9959,7 +10119,7 @@ const newProjectPreset = async () => {
9959
10119
  const moreToCome = async () => {
9960
10120
  return {
9961
10121
  data: {
9962
- version: "3.0.0",
10122
+ version: "5.0.0",
9963
10123
  name: "More to come!",
9964
10124
  description: "Do not hesitate to suggest templates you would see here",
9965
10125
  variables: [],
@@ -9967,8 +10127,9 @@ const moreToCome = async () => {
9967
10127
  triggers: [{
9968
10128
  type: "event",
9969
10129
  origin: {
9970
- pluginId: "system",
9971
- nodeId: "manual"
10130
+ pluginId: "@pipelab/plugin-system",
10131
+ nodeId: "manual",
10132
+ version: "latest"
9972
10133
  },
9973
10134
  uid: "manual-start",
9974
10135
  params: {}
@@ -10012,10 +10173,6 @@ const registerEngineHandlers = (context) => {
10012
10173
  }
10013
10174
  });
10014
10175
  });
10015
- handle("condition:execute", async (_, { value }) => {
10016
- const { nodeId, params, pluginId } = value;
10017
- await handleConditionExecute(nodeId, pluginId, params, await generateTempFolder(tmpdir()), context);
10018
- });
10019
10176
  let abortControllerGraph = void 0;
10020
10177
  const effectiveActionExecute = async (nodeId, pluginId, params, mainWindow, send, cwd, cachePath) => {
10021
10178
  try {
@@ -10397,6 +10554,241 @@ const registerSystemHandlers = (options) => {
10397
10554
  });
10398
10555
  };
10399
10556
  //#endregion
10557
+ //#region src/handlers/plugins.ts
10558
+ const localPluginsMap = /* @__PURE__ */ new Map();
10559
+ if (isDev && projectRoot) {
10560
+ const pluginsDir = join(projectRoot, "plugins");
10561
+ if (existsSync(pluginsDir)) try {
10562
+ const entries = readdirSync(pluginsDir, { withFileTypes: true });
10563
+ for (const entry of entries) if (entry.isDirectory()) {
10564
+ const pkgPath = join(pluginsDir, entry.name, "package.json");
10565
+ if (existsSync(pkgPath)) try {
10566
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
10567
+ if (pkg.name) localPluginsMap.set(pkg.name, join(pluginsDir, entry.name));
10568
+ } catch (e) {
10569
+ console.error(`[Plugins] Failed to parse package.json for ${entry.name}:`, e);
10570
+ }
10571
+ }
10572
+ } catch (e) {
10573
+ console.error(`[Plugins] Failed to scan local workspace plugins directory:`, e);
10574
+ }
10575
+ }
10576
+ /**
10577
+ * Resolves a requested plugin version.
10578
+ * If the plugin is present in the local workspace map during development,
10579
+ * its version is overridden to "local" so Pipelab loads it from the source files.
10580
+ */
10581
+ function resolvePluginVersion(packageName, requestedVersion) {
10582
+ if (isDev && projectRoot && process.env.PIPELAB_FORCE_NPM !== "true" && localPluginsMap.has(packageName)) return "local";
10583
+ return requestedVersion || "latest";
10584
+ }
10585
+ const registerPluginsHandlers = (context) => {
10586
+ const { handle } = useAPI();
10587
+ handle("plugin:search", async (_, { send, value }) => {
10588
+ try {
10589
+ const query = value.query || "";
10590
+ const text = query ? `${query} keywords:pipelab-plugin` : "keywords:pipelab-plugin";
10591
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=50`;
10592
+ const response = await fetch(url);
10593
+ if (!response.ok) throw new Error(`NPM registry search failed with status ${response.status}`);
10594
+ send({
10595
+ type: "end",
10596
+ data: {
10597
+ type: "success",
10598
+ result: { results: ((await response.json()).objects || []).map((obj) => ({
10599
+ name: obj.package.name,
10600
+ version: obj.package.version,
10601
+ description: obj.package.description,
10602
+ keywords: obj.package.keywords,
10603
+ date: obj.package.date
10604
+ })) }
10605
+ }
10606
+ });
10607
+ } catch (e) {
10608
+ send({
10609
+ type: "end",
10610
+ data: {
10611
+ type: "error",
10612
+ ipcError: e.message || "Failed to search npm registry"
10613
+ }
10614
+ });
10615
+ }
10616
+ });
10617
+ handle("plugin:get-details", async (_, { send, value }) => {
10618
+ try {
10619
+ const { packageName } = value;
10620
+ const packument = await pacote.packument(packageName, { fullMetadata: true });
10621
+ const latestVersion = packument["dist-tags"]?.latest || Object.keys(packument.versions).pop() || "0.0.0";
10622
+ const latestPkg = packument.versions[latestVersion];
10623
+ send({
10624
+ type: "end",
10625
+ data: {
10626
+ type: "success",
10627
+ result: {
10628
+ name: packument.name,
10629
+ latestVersion,
10630
+ versions: Object.keys(packument.versions).reverse(),
10631
+ description: latestPkg?.description
10632
+ }
10633
+ }
10634
+ });
10635
+ } catch (e) {
10636
+ send({
10637
+ type: "end",
10638
+ data: {
10639
+ type: "error",
10640
+ ipcError: e.message || "Failed to get plugin details"
10641
+ }
10642
+ });
10643
+ }
10644
+ });
10645
+ handle("plugin:install", async (_, { send, value }) => {
10646
+ try {
10647
+ const { packageName, version } = value;
10648
+ const mappedVersion = resolvePluginVersion(packageName, version);
10649
+ console.log(`[Plugins] Installing ${packageName}@${mappedVersion}...`);
10650
+ const { packageDir } = await fetchPipelabPlugin(packageName, mappedVersion, {
10651
+ context,
10652
+ installDeps: false
10653
+ });
10654
+ const plugin = await loadCustomPlugin(packageName, mappedVersion, { context });
10655
+ if (!plugin) throw new Error("Failed to load installed plugin module.");
10656
+ const { registerPlugins } = usePlugins();
10657
+ registerPlugins([plugin]);
10658
+ webSocketServer.broadcast("plugin:loaded", { plugin });
10659
+ send({
10660
+ type: "end",
10661
+ data: {
10662
+ type: "success",
10663
+ result: { result: "ok" }
10664
+ }
10665
+ });
10666
+ } catch (e) {
10667
+ console.error(`[Plugins] Installation failed for ${value.packageName}:`, e);
10668
+ send({
10669
+ type: "end",
10670
+ data: {
10671
+ type: "error",
10672
+ ipcError: e.message || "Failed to install plugin"
10673
+ }
10674
+ });
10675
+ }
10676
+ });
10677
+ handle("plugin:uninstall", async (_, { send, value }) => {
10678
+ try {
10679
+ const { packageName } = value;
10680
+ console.log(`[Plugins] Uninstalling plugin ${packageName}...`);
10681
+ const targetDir = context.getPackagesPath(packageName);
10682
+ if (existsSync(targetDir)) await rm(targetDir, {
10683
+ recursive: true,
10684
+ force: true
10685
+ });
10686
+ send({
10687
+ type: "end",
10688
+ data: {
10689
+ type: "success",
10690
+ result: { result: "ok" }
10691
+ }
10692
+ });
10693
+ } catch (e) {
10694
+ send({
10695
+ type: "end",
10696
+ data: {
10697
+ type: "error",
10698
+ ipcError: e.message || "Failed to uninstall plugin"
10699
+ }
10700
+ });
10701
+ }
10702
+ });
10703
+ handle("plugin:list-installed", async (_, { send }) => {
10704
+ try {
10705
+ const rawInstalled = await findInstalledPlugins(context.getPackagesPath());
10706
+ const DEFAULT_PLUGIN_IDS = [
10707
+ "construct",
10708
+ "filesystem",
10709
+ "system",
10710
+ "steam",
10711
+ "itch",
10712
+ "electron",
10713
+ "discord",
10714
+ "poki",
10715
+ "nvpatch",
10716
+ "tauri",
10717
+ "minify",
10718
+ "netlify"
10719
+ ];
10720
+ send({
10721
+ type: "end",
10722
+ data: {
10723
+ type: "success",
10724
+ result: { installed: rawInstalled.filter((item) => {
10725
+ return !DEFAULT_PLUGIN_IDS.some((id) => item.name === `@pipelab/plugin-${id}`);
10726
+ }).map((item) => ({
10727
+ name: item.name,
10728
+ version: item.version,
10729
+ description: item.description
10730
+ })) }
10731
+ }
10732
+ });
10733
+ } catch (e) {
10734
+ send({
10735
+ type: "end",
10736
+ data: {
10737
+ type: "error",
10738
+ ipcError: e.message || "Failed to list installed plugins"
10739
+ }
10740
+ });
10741
+ }
10742
+ });
10743
+ handle("plugin:ensure-loaded", async (_, { send, value }) => {
10744
+ const { pluginIds, plugins } = value;
10745
+ const { plugins: registeredPlugins, registerPlugins } = usePlugins();
10746
+ const loaded = [];
10747
+ const failed = [];
10748
+ const pluginsToEnsure = /* @__PURE__ */ new Set();
10749
+ if (Array.isArray(pluginIds)) {
10750
+ for (const id of pluginIds) if (id) pluginsToEnsure.add(id);
10751
+ }
10752
+ if (plugins && typeof plugins === "object") {
10753
+ for (const id of Object.keys(plugins)) if (id) pluginsToEnsure.add(id);
10754
+ }
10755
+ for (const packageName of pluginsToEnsure) {
10756
+ const mappedVersion = resolvePluginVersion(packageName);
10757
+ if (registeredPlugins.value.some((p) => {
10758
+ if (p.packageName !== packageName) return false;
10759
+ if (mappedVersion === "latest") return true;
10760
+ return p.version === mappedVersion;
10761
+ })) continue;
10762
+ try {
10763
+ console.log(`[Plugins] JIT loading plugin "${packageName}@${mappedVersion}" for pipeline...`);
10764
+ const plugin = await loadCustomPlugin(packageName, mappedVersion, { context });
10765
+ if (plugin) {
10766
+ registerPlugins([plugin]);
10767
+ webSocketServer.broadcast("plugin:loaded", { plugin });
10768
+ loaded.push(packageName);
10769
+ console.log(`[Plugins] JIT loaded "${packageName}" successfully.`);
10770
+ } else {
10771
+ console.warn(`[Plugins] JIT load for "${packageName}" returned no plugin.`);
10772
+ failed.push(packageName);
10773
+ }
10774
+ } catch (e) {
10775
+ console.error(`[Plugins] JIT load failed for "${packageName}":`, e);
10776
+ failed.push(packageName);
10777
+ }
10778
+ }
10779
+ send({
10780
+ type: "end",
10781
+ data: {
10782
+ type: "success",
10783
+ result: {
10784
+ loaded,
10785
+ failed
10786
+ }
10787
+ }
10788
+ });
10789
+ });
10790
+ };
10791
+ //#endregion
10400
10792
  //#region src/handlers/index.ts
10401
10793
  const registerAllHandlers = async (options) => {
10402
10794
  const context = options.context;
@@ -10408,6 +10800,7 @@ const registerAllHandlers = async (options) => {
10408
10800
  registerAgentsHandlers(context);
10409
10801
  registerAuthHandlers(context);
10410
10802
  registerSystemHandlers(options);
10803
+ registerPluginsHandlers(context);
10411
10804
  const { registerPlugins } = usePlugins();
10412
10805
  builtInPlugins({ context });
10413
10806
  };
@@ -52755,15 +53148,54 @@ async function runPipelineCommand(file, options, version) {
52755
53148
  */
52756
53149
  async function fetchPackageReleases(packageName, options = {}) {
52757
53150
  const { repo = "CynToolkit/pipelab", allowPrerelease = false } = options;
52758
- const url = `https://api.github.com/repos/${repo}/releases`;
52759
- console.log(`[GitHub] Fetching releases for ${packageName} from ${url}...`);
52760
53151
  try {
52761
- const response = await fetch(url, { headers: {
53152
+ const override = process.env.PIPELAB_OVERRIDE_RELEASE;
53153
+ if (override) {
53154
+ const targetTag = override.includes("@") ? override : `${packageName}@${override}`;
53155
+ console.log(`[GitHub] Fetching specific override release: ${targetTag}`);
53156
+ const response = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(targetTag)}`, { headers: {
53157
+ "User-Agent": "Pipelab-Desktop-Updater",
53158
+ Accept: "application/vnd.github.v3+json"
53159
+ } });
53160
+ if (response.ok) return [await response.json()];
53161
+ console.warn(`[GitHub] Override release tag ${targetTag} not found or error occurred`);
53162
+ }
53163
+ const matchingRefsUrl = `https://api.github.com/repos/${repo}/git/matching-refs/tags/${encodeURIComponent(packageName)}`;
53164
+ console.log(`[GitHub] Querying matching tags from ${matchingRefsUrl}...`);
53165
+ const response = await fetch(matchingRefsUrl, { headers: {
52762
53166
  "User-Agent": "Pipelab-Desktop-Updater",
52763
53167
  Accept: "application/vnd.github.v3+json"
52764
53168
  } });
52765
53169
  if (!response.ok) throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
52766
- return (await response.json()).filter((r) => r.tag_name.startsWith(`${packageName}@`)).filter((r) => allowPrerelease || !r.prerelease);
53170
+ const refs = await response.json();
53171
+ if (!Array.isArray(refs)) return [];
53172
+ const filteredTags = refs.map((r) => r.ref.replace("refs/tags/", "")).filter((tag) => tag.startsWith(`${packageName}@`)).filter((tag) => {
53173
+ const version = tag.split("@").pop();
53174
+ if (!version || !semver.valid(version)) return false;
53175
+ const isPrerelease = semver.prerelease(version) !== null;
53176
+ return allowPrerelease || !isPrerelease;
53177
+ });
53178
+ if (filteredTags.length === 0) return [];
53179
+ filteredTags.sort((a, b) => {
53180
+ const vA = a.split("@").pop() || "0.0.0";
53181
+ const vB = b.split("@").pop() || "0.0.0";
53182
+ return semver.rcompare(vA, vB);
53183
+ });
53184
+ for (const tag of filteredTags) {
53185
+ console.log(`[GitHub] Fetching release details for tag: ${tag}`);
53186
+ const releaseResponse = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`, { headers: {
53187
+ "User-Agent": "Pipelab-Desktop-Updater",
53188
+ Accept: "application/vnd.github.v3+json"
53189
+ } });
53190
+ if (releaseResponse.status === 404) {
53191
+ console.warn(`[GitHub] Tag "${tag}" has no associated Release, skipping.`);
53192
+ continue;
53193
+ }
53194
+ if (!releaseResponse.ok) throw new Error(`GitHub API error: ${releaseResponse.status} ${releaseResponse.statusText}`);
53195
+ return [await releaseResponse.json()];
53196
+ }
53197
+ console.warn(`[GitHub] No published Release found for any matching tag of "${packageName}".`);
53198
+ return [];
52767
53199
  } catch (error) {
52768
53200
  console.error(`[GitHub] Failed to fetch releases for ${packageName}:`, error);
52769
53201
  return [];
@@ -52806,6 +53238,6 @@ async function fetchLatestDesktopRelease(options = {}) {
52806
53238
  return latest;
52807
53239
  }
52808
53240
  //#endregion
52809
- export { BuildHistoryStorage, DEFAULT_NODE_VERSION, DEFAULT_PNPM_VERSION, PipelabContext, WebSocketServer, builtInPlugins, downloadFile, ensure, ensureNodeJS, ensurePNPM, executeGraphWithHistory, extractTarGz, extractZip, fetchLatestDesktopRelease, fetchLatestPackageRelease, fetchPackage, fetchPackageReleases, fetchPipelabAsset, fetchPipelabCli, fetchPipelabPlugin, generateTempFolder, getDefaultUserDataPath, getFinalPlugins, getFolderSize, getMigrator, handleActionExecute, handleConditionExecute, isDev, loadPipelabPlugin, projectRoot, registerAgentsHandlers, registerAllHandlers, registerConfigHandlers, registerEngineHandlers, registerFsHandlers, registerHistoryHandlers, registerMigrationHandlers, registerShellHandlers, runPipelineCommand, runPnpm, runWithLiveLogs, sendStartupProgress, sendStartupReady, serveCommand, setupConfigFile, useAPI, usePluginAPI, webSocketServer, zipFolder };
53241
+ export { BuildHistoryStorage, DEFAULT_NODE_VERSION, DEFAULT_PNPM_VERSION, PipelabContext, WebSocketServer, builtInPlugins, downloadFile, ensure, ensureNodeJS, ensurePNPM, executeGraphWithHistory, extractTarGz, extractZip, fetchLatestDesktopRelease, fetchLatestPackageRelease, fetchPackage, fetchPackageReleases, fetchPipelabAsset, fetchPipelabCli, fetchPipelabPlugin, findInstalledPlugins, generateTempFolder, getDefaultUserDataPath, getFinalPlugins, getFolderSize, getMigrator, handleActionExecute, isDev, loadCustomPlugin, loadPipelabPlugin, projectRoot, registerAgentsHandlers, registerAllHandlers, registerConfigHandlers, registerEngineHandlers, registerFsHandlers, registerHistoryHandlers, registerMigrationHandlers, registerShellHandlers, runPipelineCommand, runPnpm, runWithLiveLogs, sendStartupProgress, sendStartupReady, serveCommand, setupConfigFile, useAPI, usePluginAPI, webSocketServer, zipFolder };
52810
53242
 
52811
53243
  //# sourceMappingURL=index.mjs.map