@pipelab/core-node 1.0.0-beta.2 → 1.0.0-beta.22

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 (45) hide show
  1. package/.oxfmtrc.json +1 -1
  2. package/CHANGELOG.md +173 -0
  3. package/dist/config-Bi0ORcTK.mjs +2 -0
  4. package/dist/config-CFgGRD9U.mjs +265 -0
  5. package/dist/config-CFgGRD9U.mjs.map +1 -0
  6. package/dist/index.d.mts +74 -53
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +1705 -576
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +9 -5
  11. package/scratch/simulate-updates.ts +120 -0
  12. package/src/config.test.ts +232 -0
  13. package/src/config.ts +111 -50
  14. package/src/context.ts +117 -5
  15. package/src/handler-func.ts +2 -77
  16. package/src/handlers/build-history.ts +60 -90
  17. package/src/handlers/config.ts +265 -55
  18. package/src/handlers/engine.ts +32 -35
  19. package/src/handlers/history.ts +0 -40
  20. package/src/handlers/index.ts +4 -0
  21. package/src/handlers/migration.ts +621 -0
  22. package/src/handlers/plugins.ts +305 -0
  23. package/src/handlers/system.ts +7 -2
  24. package/src/index.ts +9 -2
  25. package/src/plugins-registry.ts +280 -38
  26. package/src/presets/c3toSteam.ts +13 -7
  27. package/src/presets/demo.ts +9 -9
  28. package/src/presets/list.ts +0 -6
  29. package/src/presets/moreToCome.ts +3 -2
  30. package/src/presets/newProject.ts +3 -2
  31. package/src/presets/test-c3-offline.ts +15 -6
  32. package/src/presets/test-c3-unzip.ts +19 -8
  33. package/src/runner.ts +2 -8
  34. package/src/server.ts +45 -4
  35. package/src/types/runner.ts +2 -30
  36. package/src/utils/fs-extras.ts +6 -24
  37. package/src/utils/github.test.ts +211 -0
  38. package/src/utils/github.ts +90 -10
  39. package/src/utils/remote.test.ts +209 -0
  40. package/src/utils/remote.ts +325 -87
  41. package/src/utils/storage.ts +2 -1
  42. package/src/utils.ts +20 -24
  43. package/src/migrations.ts +0 -72
  44. package/src/presets/if.ts +0 -69
  45. package/src/presets/loop.ts +0 -65
package/dist/index.mjs CHANGED
@@ -1,20 +1,18 @@
1
+ import { a as setupPipelineConfigFileByName, c as setupSettingsConfigFile, d as extractTarGz, f as extractZip, h as zipFolder, i as setupConnectionsConfigFile, l as downloadFile, m as runWithLiveLogs, n as deletePipelineConfigFileByPath, o as setupPipelineConfigFileByPath, p as getFolderSize, s as setupProjectsConfigFile, t as deletePipelineConfigFileByName, u as ensure } from "./config-CFgGRD9U.mjs";
1
2
  import { createRequire } from "node:module";
2
3
  import path, { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
3
- import { homedir, platform, tmpdir } from "node:os";
4
+ import { homedir, platform } from "node:os";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import fs, { createWriteStream, existsSync } from "node:fs";
6
+ import fs, { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
7
+ import fs$1, { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
8
+ import { DEFAULT_NODE_VERSION, DEFAULT_PNPM_VERSION, SandboxFolder, getUiDevServerMissingWarning, uiDevPort, websocketPort } from "@pipelab/constants";
6
9
  import { WebSocket, WebSocketServer as WebSocketServer$1 } from "ws";
7
10
  import http from "node:http";
8
11
  import { nanoid } from "nanoid";
9
- import { SubscriptionRequiredError, WebSocketError, appSettingsMigrator, configRegistry, fileRepoMigrations, isRequired, isSupabaseAvailable, isWebSocketRequestMessage, processGraph, savedFileMigrator, supabase, useLogger, usePlugins } from "@pipelab/shared";
10
- import { getUiDevServerMissingWarning, uiDevPort, websocketPort } from "@pipelab/constants";
11
- import fs$1, { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, unlink, writeFile } from "node:fs/promises";
12
+ import { SubscriptionRequiredError, WebSocketError, appSettingsMigrator, connectionsMigrator, fileRepoMigrations, isRequired, isSupabaseAvailable, isWebSocketRequestMessage, processGraph, savedFileMigrator, supabase, transformUrl, useLogger, usePlugins } from "@pipelab/shared";
12
13
  import { execa } from "execa";
13
- import tar from "tar";
14
- import yauzl from "yauzl";
15
- import archiver from "archiver";
16
- import { pipeline } from "node:stream/promises";
17
14
  import checkDiskSpace from "check-disk-space";
15
+ import dns from "node:dns/promises";
18
16
  import pacote from "pacote";
19
17
  import semver from "semver";
20
18
  import http$1 from "http";
@@ -53,16 +51,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
53
51
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
54
52
  //#endregion
55
53
  //#region src/context.ts
56
- const _dirname = typeof __dirname !== "undefined" ? __dirname : typeof import.meta !== "undefined" && import.meta.url ? dirname(fileURLToPath(import.meta.url)) : process.cwd();
54
+ const metaUrl = typeof import.meta !== "undefined" ? import.meta.url : void 0;
55
+ const _dirname = typeof __dirname !== "undefined" ? __dirname : metaUrl ? dirname(fileURLToPath(metaUrl)) : process.cwd();
57
56
  const isDev = process.env.NODE_ENV === "development";
58
- const getDefaultUserDataPath = () => {
59
- return join((() => {
57
+ const getDefaultUserDataPath = (env) => {
58
+ const base = (() => {
60
59
  switch (platform()) {
61
60
  case "win32": return process.env.APPDATA || join(homedir(), "AppData", "Roaming");
62
61
  case "darwin": return join(homedir(), "Library", "Application Support");
63
62
  default: return process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
64
63
  }
65
- })(), "@pipelab", isDev ? "app-dev" : "app");
64
+ })();
65
+ const mode = env ?? (isDev ? "dev" : "prod");
66
+ return join(base, "@pipelab", mode === "dev" ? "app-dev" : mode === "beta" ? "app-beta" : "app");
66
67
  };
67
68
  /**
68
69
  * Finds the monorepo root by looking for pnpm-workspace.yaml.
@@ -76,10 +77,17 @@ function findProjectRoot(startDir) {
76
77
  return null;
77
78
  }
78
79
  const projectRoot = findProjectRoot(_dirname);
80
+ const CacheFolder = {
81
+ Actions: "actions",
82
+ Pipelines: "pipelines",
83
+ Pacote: "pacote"
84
+ };
79
85
  var PipelabContext = class {
80
86
  userDataPath;
87
+ releaseTag;
81
88
  constructor(options) {
82
89
  this.userDataPath = options.userDataPath;
90
+ this.releaseTag = options.releaseTag || "latest";
83
91
  }
84
92
  getPackagesPath(...subpaths) {
85
93
  return join(this.userDataPath, "packages", ...subpaths);
@@ -90,6 +98,89 @@ var PipelabContext = class {
90
98
  getConfigPath(...subpaths) {
91
99
  return join(this.userDataPath, "config", ...subpaths);
92
100
  }
101
+ getSettingsPath() {
102
+ return this.getConfigPath("settings.json");
103
+ }
104
+ getConnectionsPath() {
105
+ return this.getConfigPath("connections.json");
106
+ }
107
+ getProjectsPath() {
108
+ return this.getConfigPath("projects.json");
109
+ }
110
+ _cachedSettings = null;
111
+ _cachedSettingsTime = 0;
112
+ getSettings() {
113
+ const settingsPath = this.getSettingsPath();
114
+ if (!existsSync(settingsPath)) return null;
115
+ const now = Date.now();
116
+ if (this._cachedSettings && now - this._cachedSettingsTime < 2e3) return this._cachedSettings;
117
+ try {
118
+ const content = readFileSync(settingsPath, "utf8");
119
+ this._cachedSettings = JSON.parse(content);
120
+ this._cachedSettingsTime = now;
121
+ return this._cachedSettings;
122
+ } catch (e) {
123
+ return this._cachedSettings;
124
+ }
125
+ }
126
+ getTempPath(...subpaths) {
127
+ return join(this.getSettings()?.tempFolder || join(this.userDataPath, "temp"), ...subpaths);
128
+ }
129
+ async createTempFolder(prefix = "pipelab-") {
130
+ const baseDir = this.getTempPath();
131
+ await mkdir(baseDir, { recursive: true });
132
+ return await mkdtemp(join(await realpath(baseDir), prefix));
133
+ }
134
+ getCachePath(folder, ...subpaths) {
135
+ const base = this.getSettings()?.cacheFolder || join(this.userDataPath, "cache");
136
+ if (!folder) return base;
137
+ return join(base, folder, ...subpaths);
138
+ }
139
+ getPnpmPath(...subpaths) {
140
+ return join(this.userDataPath, "pnpm", ...subpaths);
141
+ }
142
+ getBuildHistoryPath(...subpaths) {
143
+ return join(this.userDataPath, "build-history", ...subpaths);
144
+ }
145
+ getNodePath(version = DEFAULT_NODE_VERSION) {
146
+ const isWindows = process.platform === "win32";
147
+ return this.getThirdPartyPath("node", version, isWindows ? "node.exe" : "bin/node");
148
+ }
149
+ getPnpmBinPath(version = DEFAULT_PNPM_VERSION) {
150
+ return this.getPackagesPath("pnpm", version, "bin", "pnpm.cjs");
151
+ }
152
+ getSandboxFolders() {
153
+ const foldersRecord = {
154
+ [SandboxFolder.Packages]: {
155
+ label: "Packages",
156
+ path: this.getPackagesPath()
157
+ },
158
+ [SandboxFolder.ThirdParty]: {
159
+ label: "Third-Party Tools",
160
+ path: this.getThirdPartyPath()
161
+ },
162
+ [SandboxFolder.Config]: {
163
+ label: "Configuration",
164
+ path: this.getConfigPath()
165
+ },
166
+ [SandboxFolder.Temp]: {
167
+ label: "Temporary Files",
168
+ path: this.getTempPath()
169
+ },
170
+ [SandboxFolder.Cache]: {
171
+ label: "Cache",
172
+ path: this.getCachePath()
173
+ },
174
+ [SandboxFolder.Pnpm]: {
175
+ label: "PNPM Home",
176
+ path: this.getPnpmPath()
177
+ }
178
+ };
179
+ return Object.keys(foldersRecord).map((key) => ({
180
+ name: key,
181
+ ...foldersRecord[key]
182
+ }));
183
+ }
93
184
  };
94
185
  //#endregion
95
186
  //#region src/ipc-core.ts
@@ -443,296 +534,363 @@ const registerFsHandlers = (_context) => {
443
534
  });
444
535
  };
445
536
  //#endregion
446
- //#region src/utils/fs-extras.ts
447
- /**
448
- * Ensures a directory exists and a file is created with default content if missing.
449
- */
450
- const ensure = async (filesPath, defaultContent = "{}") => {
451
- await mkdir(dirname(filesPath), { recursive: true });
452
- try {
453
- await access(filesPath);
454
- } catch {
455
- await writeFile(filesPath, defaultContent);
456
- }
457
- };
458
- /**
459
- * Generates a unique temporary folder.
460
- */
461
- const generateTempFolder = async (base) => {
462
- const targetBase = base || tmpdir();
463
- await mkdir(targetBase, { recursive: true });
464
- return await mkdtemp(join(await realpath(targetBase), "pipelab-"));
465
- };
466
- /**
467
- * Extracts a .tar.gz archive.
468
- */
469
- async function extractTarGz(archivePath, destinationDir) {
470
- await mkdir(destinationDir, { recursive: true });
471
- await tar.x({
472
- file: archivePath,
473
- cwd: destinationDir
537
+ //#region src/handlers/config.ts
538
+ const registerConfigHandlers = (context) => {
539
+ const { handle } = useAPI();
540
+ const { logger } = useLogger();
541
+ handle("settings:load", async (_, { send }) => {
542
+ logger().info("settings:load");
543
+ try {
544
+ send({
545
+ type: "end",
546
+ data: {
547
+ type: "success",
548
+ result: await (await setupSettingsConfigFile(context)).getConfig()
549
+ }
550
+ });
551
+ } catch (e) {
552
+ logger().error("settings:load error:", e);
553
+ send({
554
+ type: "end",
555
+ data: {
556
+ type: "error",
557
+ ipcError: e instanceof Error ? e.message : "Unable to load settings"
558
+ }
559
+ });
560
+ }
474
561
  });
475
- }
476
- /**
477
- * Extracts a .zip archive.
478
- */
479
- async function extractZip(archivePath, destinationDir) {
480
- await mkdir(destinationDir, { recursive: true });
481
- return new Promise((resolve, reject) => {
482
- yauzl.open(archivePath, { lazyEntries: true }, (err, zipfile) => {
483
- if (err || !zipfile) return reject(err || /* @__PURE__ */ new Error("Could not open zip file"));
484
- zipfile.on("error", reject);
485
- zipfile.readEntry();
486
- zipfile.on("entry", (entry) => {
487
- const entryPath = join(destinationDir, entry.fileName);
488
- if (/\/$/.test(entry.fileName)) mkdir(entryPath, { recursive: true }).then(() => zipfile.readEntry()).catch(reject);
489
- else mkdir(dirname(entryPath), { recursive: true }).then(() => {
490
- zipfile.openReadStream(entry, (err, readStream) => {
491
- if (err || !readStream) return reject(err || /* @__PURE__ */ new Error("Could not open read stream"));
492
- readStream.on("error", reject);
493
- const writeStream = createWriteStream(entryPath);
494
- writeStream.on("error", reject);
495
- writeStream.on("close", () => zipfile.readEntry());
496
- readStream.pipe(writeStream);
497
- });
498
- }).catch(reject);
562
+ handle("settings:save", async (_, { send, value }) => {
563
+ const { data } = value;
564
+ try {
565
+ const manager = await setupSettingsConfigFile(context);
566
+ const json = typeof data === "string" ? JSON.parse(data) : data;
567
+ await manager.setConfig(json);
568
+ send({
569
+ type: "end",
570
+ data: {
571
+ type: "success",
572
+ result: "ok"
573
+ }
499
574
  });
500
- zipfile.on("end", () => resolve());
501
- });
575
+ } catch (e) {
576
+ logger().error("settings:save error:", e);
577
+ send({
578
+ type: "end",
579
+ data: {
580
+ type: "error",
581
+ ipcError: e instanceof Error ? e.message : "Unable to save settings"
582
+ }
583
+ });
584
+ }
502
585
  });
503
- }
504
- /**
505
- * Zips a folder.
506
- */
507
- const zipFolder = async (from, to, log = console.log) => {
508
- const output = createWriteStream(to);
509
- const archive = archiver("zip", { zlib: { level: 9 } });
510
- return new Promise((resolve, reject) => {
511
- output.on("close", () => {
512
- log(archive.pointer() + " total bytes");
513
- resolve(to);
514
- });
515
- archive.on("error", reject);
516
- archive.pipe(output);
517
- archive.directory(from, false);
518
- archive.finalize();
586
+ handle("settings:reset", async (_, { send, value }) => {
587
+ const { key } = value;
588
+ try {
589
+ const manager = await setupSettingsConfigFile(context);
590
+ const currentConfig = await manager.getConfig();
591
+ const defaultValue = appSettingsMigrator.defaultValue[key];
592
+ await manager.setConfig({
593
+ ...currentConfig ? currentConfig : {},
594
+ [key]: defaultValue
595
+ });
596
+ send({
597
+ type: "end",
598
+ data: {
599
+ type: "success",
600
+ result: "ok"
601
+ }
602
+ });
603
+ } catch (e) {
604
+ logger().error("settings:reset error:", e);
605
+ send({
606
+ type: "end",
607
+ data: {
608
+ type: "error",
609
+ ipcError: e instanceof Error ? e.message : "Unable to reset settings"
610
+ }
611
+ });
612
+ }
519
613
  });
520
- };
521
- const downloadFile = async (url, localPath, hooks, abortSignal) => {
522
- const response = await fetch(url, { signal: abortSignal });
523
- if (!response.ok) throw new Error(`Failed to fetch file: ${response.statusText}`);
524
- const contentLength = response.headers.get("content-length");
525
- if (!contentLength) throw new Error("Content-Length header is missing");
526
- const totalSize = parseInt(contentLength, 10);
527
- let downloadedSize = 0;
528
- const fileStream = createWriteStream(localPath);
529
- const progressStream = new TransformStream({ transform(chunk, controller) {
530
- downloadedSize += chunk.length;
531
- const progress = downloadedSize / totalSize * 100;
532
- hooks?.onProgress?.({
533
- progress,
534
- downloadedSize
535
- });
536
- controller.enqueue(chunk);
537
- } });
538
- const readable = response.body?.pipeThrough(progressStream);
539
- if (!readable) throw new Error("Failed to create a readable stream");
540
- await pipeline(readable, fileStream);
541
- };
542
- const runWithLiveLogs = async (command, args, execaOptions, log, hooks, abortSignal) => {
543
- const subprocess = execa(command, args, {
544
- ...execaOptions,
545
- stdout: "pipe",
546
- stderr: "pipe",
547
- stdin: "pipe",
548
- env: {
549
- ...process.env,
550
- ...execaOptions.env,
551
- TERM: "xterm-256color",
552
- FORCE_STDERR_LOGGING: "1"
553
- },
554
- cancelSignal: abortSignal
614
+ handle("connections:load", async (_, { send }) => {
615
+ logger().info("connections:load");
616
+ try {
617
+ send({
618
+ type: "end",
619
+ data: {
620
+ type: "success",
621
+ result: await (await setupConnectionsConfigFile(context)).getConfig()
622
+ }
623
+ });
624
+ } catch (e) {
625
+ logger().error("connections:load error:", e);
626
+ send({
627
+ type: "end",
628
+ data: {
629
+ type: "error",
630
+ ipcError: e instanceof Error ? e.message : "Unable to load connections"
631
+ }
632
+ });
633
+ }
555
634
  });
556
- hooks?.onCreated?.(subprocess);
557
- subprocess.stdout?.on("data", (data) => {
558
- hooks?.onStdout?.(data.toString(), subprocess);
635
+ handle("connections:save", async (_, { send, value }) => {
636
+ const { data } = value;
637
+ try {
638
+ const manager = await setupConnectionsConfigFile(context);
639
+ const json = typeof data === "string" ? JSON.parse(data) : data;
640
+ await manager.setConfig(json);
641
+ send({
642
+ type: "end",
643
+ data: {
644
+ type: "success",
645
+ result: "ok"
646
+ }
647
+ });
648
+ } catch (e) {
649
+ logger().error("connections:save error:", e);
650
+ send({
651
+ type: "end",
652
+ data: {
653
+ type: "error",
654
+ ipcError: e instanceof Error ? e.message : "Unable to save connections"
655
+ }
656
+ });
657
+ }
559
658
  });
560
- subprocess.stderr?.on("data", (data) => {
561
- hooks?.onStderr?.(data.toString(), subprocess);
659
+ handle("connections:reset", async (_, { send, value }) => {
660
+ const { key } = value;
661
+ try {
662
+ const manager = await setupConnectionsConfigFile(context);
663
+ const currentConfig = await manager.getConfig();
664
+ const defaultValue = connectionsMigrator.defaultValue[key];
665
+ await manager.setConfig({
666
+ ...currentConfig ? currentConfig : {},
667
+ [key]: defaultValue
668
+ });
669
+ send({
670
+ type: "end",
671
+ data: {
672
+ type: "success",
673
+ result: "ok"
674
+ }
675
+ });
676
+ } catch (e) {
677
+ logger().error("connections:reset error:", e);
678
+ send({
679
+ type: "end",
680
+ data: {
681
+ type: "error",
682
+ ipcError: e instanceof Error ? e.message : "Unable to reset connections"
683
+ }
684
+ });
685
+ }
562
686
  });
563
- try {
564
- const { exitCode } = await subprocess;
565
- hooks?.onExit?.(exitCode ?? 0);
566
- } catch (error) {
567
- const code = error.exitCode ?? 1;
568
- hooks?.onExit?.(code);
569
- throw new Error(`Command failed with exit code ${code}: ${error.message}`);
570
- }
571
- };
572
- /**
573
- * Calculates the total size of a directory recursively.
574
- */
575
- async function getFolderSize(dirPath) {
576
- try {
577
- const ArrayOfPromises = (await readdir(dirPath, { withFileTypes: true })).map(async (file) => {
578
- const path = join(dirPath, file.name);
579
- if (file.isDirectory()) try {
580
- return await getFolderSize(path);
581
- } catch {
582
- return 0;
583
- }
584
- try {
585
- const { size } = await stat(path);
586
- return size;
587
- } catch {
588
- return 0;
589
- }
590
- });
591
- return (await Promise.all(ArrayOfPromises)).reduce((acc, size) => acc + size, 0);
592
- } catch {
593
- return 0;
594
- }
595
- }
596
- //#endregion
597
- //#region src/config.ts
598
- const getMigrator = (name) => {
599
- if (configRegistry[name]) return configRegistry[name];
600
- if (name.startsWith("pipeline-")) return configRegistry["pipeline"];
601
- };
602
- const setupConfigFile = async (name, options) => {
603
- const ctx = options.context;
604
- const migrator = options.migrator || getMigrator(name);
605
- if (!migrator) throw new Error(`No migrator found for configuration: ${name}. All managed files must have a migration schema.`);
606
- const filesPath = path.isAbsolute(name) ? name : ctx.getConfigPath(`${name}.json`);
607
- await ensure(filesPath, JSON.stringify(migrator.defaultValue));
608
- return {
609
- setConfig: async (config) => {
610
- const { logger } = useLogger();
611
- try {
612
- await fs$1.writeFile(filesPath, JSON.stringify(config));
613
- return true;
614
- } catch (e) {
615
- logger().error(`Error saving config ${name}:`, e);
616
- return false;
617
- }
618
- },
619
- getConfig: async () => {
620
- const { logger } = useLogger();
621
- let content = void 0;
622
- let originalJson = void 0;
623
- try {
624
- content = await fs$1.readFile(filesPath, "utf8");
625
- if (content !== void 0) originalJson = JSON.parse(content);
626
- } catch (e) {
627
- logger().error(`Error reading or parsing config ${name}:`, e);
628
- }
629
- let json = void 0;
630
- try {
631
- json = await migrator.migrate(originalJson, { debug: false });
632
- } catch (e) {
633
- logger().error(`Error migrating config ${name}:`, e);
634
- json = migrator.defaultValue;
635
- }
636
- const originalVersion = originalJson?.version;
637
- const newVersion = json?.version;
638
- if (originalVersion !== newVersion && content !== void 0) {
639
- const parsedPath = path.parse(filesPath);
640
- const versionSuffix = originalVersion || "unknown";
641
- const backupPath = path.join(parsedPath.dir, `${parsedPath.name}.${versionSuffix}.bak`);
642
- try {
643
- await fs$1.copyFile(filesPath, backupPath);
644
- logger().info(`Backup created for ${name} at ${backupPath} (version ${versionSuffix})`);
645
- } catch (e) {
646
- logger().error(`Failed to create backup for ${name}:`, e);
687
+ handle("projects:load", async (_, { send }) => {
688
+ logger().info("projects:load");
689
+ try {
690
+ send({
691
+ type: "end",
692
+ data: {
693
+ type: "success",
694
+ result: await (await setupProjectsConfigFile(context)).getConfig()
647
695
  }
648
- }
649
- if (originalVersion !== newVersion || content === void 0) try {
650
- await fs$1.writeFile(filesPath, JSON.stringify(json));
651
- } catch (e) {
652
- logger().error(`Error saving migrated config ${name}:`, e);
653
- }
654
- return json;
696
+ });
697
+ } catch (e) {
698
+ logger().error("projects:load error:", e);
699
+ send({
700
+ type: "end",
701
+ data: {
702
+ type: "error",
703
+ ipcError: e instanceof Error ? e.message : "Unable to load projects"
704
+ }
705
+ });
655
706
  }
656
- };
657
- };
658
- //#endregion
659
- //#region src/handlers/config.ts
660
- const registerConfigHandlers = (context) => {
661
- const { handle } = useAPI();
662
- const { logger } = useLogger();
663
- handle("config:load", async (_, { send, value }) => {
664
- const { config: name } = value;
665
- logger().info("config:load", name);
707
+ });
708
+ handle("projects:save", async (_, { send, value }) => {
709
+ const { data } = value;
710
+ try {
711
+ const manager = await setupProjectsConfigFile(context);
712
+ const json = typeof data === "string" ? JSON.parse(data) : data;
713
+ await manager.setConfig(json);
714
+ send({
715
+ type: "end",
716
+ data: {
717
+ type: "success",
718
+ result: "ok"
719
+ }
720
+ });
721
+ } catch (e) {
722
+ logger().error("projects:save error:", e);
723
+ send({
724
+ type: "end",
725
+ data: {
726
+ type: "error",
727
+ ipcError: e instanceof Error ? e.message : "Unable to save projects"
728
+ }
729
+ });
730
+ }
731
+ });
732
+ handle("projects:reset", async (_, { send, value }) => {
733
+ const { key } = value;
734
+ try {
735
+ const manager = await setupProjectsConfigFile(context);
736
+ const currentConfig = await manager.getConfig();
737
+ const defaultValue = fileRepoMigrations.defaultValue[key];
738
+ await manager.setConfig({
739
+ ...currentConfig ? currentConfig : {},
740
+ [key]: defaultValue
741
+ });
742
+ send({
743
+ type: "end",
744
+ data: {
745
+ type: "success",
746
+ result: "ok"
747
+ }
748
+ });
749
+ } catch (e) {
750
+ logger().error("projects:reset error:", e);
751
+ send({
752
+ type: "end",
753
+ data: {
754
+ type: "error",
755
+ ipcError: e instanceof Error ? e.message : "Unable to reset projects"
756
+ }
757
+ });
758
+ }
759
+ });
760
+ handle("pipeline:load-by-name", async (_, { send, value }) => {
761
+ const { name } = value;
762
+ logger().info("pipeline:load-by-name", name);
763
+ try {
764
+ send({
765
+ type: "end",
766
+ data: {
767
+ type: "success",
768
+ result: await (await setupPipelineConfigFileByName(name, context)).getConfig()
769
+ }
770
+ });
771
+ } catch (e) {
772
+ logger().error(`pipeline:load-by-name error for ${name}:`, e);
773
+ send({
774
+ type: "end",
775
+ data: {
776
+ type: "error",
777
+ ipcError: e instanceof Error ? e.message : `Unable to load pipeline ${name}`
778
+ }
779
+ });
780
+ }
781
+ });
782
+ handle("pipeline:load-by-path", async (_, { send, value }) => {
783
+ const { path: absolutePath } = value;
784
+ logger().info("pipeline:load-by-path", absolutePath);
666
785
  try {
667
786
  send({
668
787
  type: "end",
669
788
  data: {
670
789
  type: "success",
671
- result: { result: await (await setupConfigFile(name, { context })).getConfig() }
790
+ result: await (await setupPipelineConfigFileByPath(absolutePath, context)).getConfig()
791
+ }
792
+ });
793
+ } catch (e) {
794
+ logger().error(`pipeline:load-by-path error for ${absolutePath}:`, e);
795
+ send({
796
+ type: "end",
797
+ data: {
798
+ type: "error",
799
+ ipcError: e instanceof Error ? e.message : `Unable to load pipeline ${absolutePath}`
800
+ }
801
+ });
802
+ }
803
+ });
804
+ handle("pipeline:save-by-name", async (_, { send, value }) => {
805
+ const { data, name } = value;
806
+ try {
807
+ const manager = await setupPipelineConfigFileByName(name, context);
808
+ const json = typeof data === "string" ? JSON.parse(data) : data;
809
+ await manager.setConfig(json);
810
+ send({
811
+ type: "end",
812
+ data: {
813
+ type: "success",
814
+ result: "ok"
672
815
  }
673
816
  });
674
817
  } catch (e) {
675
- logger().error(`config:load error for ${name}:`, e);
818
+ logger().error(`pipeline:save-by-name error for ${name}:`, e);
676
819
  send({
677
820
  type: "end",
678
821
  data: {
679
822
  type: "error",
680
- ipcError: e instanceof Error ? e.message : `Unable to load config ${name}`
823
+ ipcError: e instanceof Error ? e.message : `Unable to save pipeline ${name}`
681
824
  }
682
825
  });
683
826
  }
684
827
  });
685
- handle("config:save", async (_, { send, value }) => {
686
- const { data, config: name } = value;
828
+ handle("pipeline:save-by-path", async (_, { send, value }) => {
829
+ const { data, path: absolutePath } = value;
687
830
  try {
688
- const manager = await setupConfigFile(name, { context });
831
+ const manager = await setupPipelineConfigFileByPath(absolutePath, context);
689
832
  const json = typeof data === "string" ? JSON.parse(data) : data;
690
833
  await manager.setConfig(json);
691
834
  send({
692
835
  type: "end",
693
836
  data: {
694
837
  type: "success",
695
- result: { result: "ok" }
838
+ result: "ok"
696
839
  }
697
840
  });
698
841
  } catch (e) {
699
- logger().error(`config:save error for ${name}:`, e);
842
+ logger().error(`pipeline:save-by-path error for ${absolutePath}:`, e);
700
843
  send({
701
844
  type: "end",
702
845
  data: {
703
846
  type: "error",
704
- ipcError: e instanceof Error ? e.message : `Unable to save config ${name}`
847
+ ipcError: e instanceof Error ? e.message : `Unable to save pipeline ${absolutePath}`
705
848
  }
706
849
  });
707
850
  }
708
851
  });
709
- handle("config:reset", async (event, { value, send }) => {
710
- const { config: name, key } = value;
711
- logger().info("config:reset", name, key);
852
+ handle("pipeline:delete-by-name", async (_, { send, value }) => {
853
+ const { name } = value;
854
+ logger().info("pipeline:delete-by-name", name);
712
855
  try {
713
- const manager = await setupConfigFile(name, { context });
714
- const currentConfig = await manager.getConfig();
715
- const migrator = getMigrator(name);
716
- if (!migrator) throw new Error(`No migrator found for configuration: ${name}`);
717
- const defaultValue = migrator.defaultValue[key];
718
- await manager.setConfig({
719
- ...currentConfig ? currentConfig : {},
720
- [key]: defaultValue
856
+ await deletePipelineConfigFileByName(name, context);
857
+ send({
858
+ type: "end",
859
+ data: {
860
+ type: "success",
861
+ result: "ok"
862
+ }
721
863
  });
864
+ } catch (e) {
865
+ logger().error(`pipeline:delete-by-name error for ${name}:`, e);
866
+ send({
867
+ type: "end",
868
+ data: {
869
+ type: "error",
870
+ ipcError: e instanceof Error ? e.message : `Unable to delete pipeline ${name}`
871
+ }
872
+ });
873
+ }
874
+ });
875
+ handle("pipeline:delete-by-path", async (_, { send, value }) => {
876
+ const { path: absolutePath } = value;
877
+ logger().info("pipeline:delete-by-path", absolutePath);
878
+ try {
879
+ await deletePipelineConfigFileByPath(absolutePath, context);
722
880
  send({
723
881
  type: "end",
724
882
  data: {
725
883
  type: "success",
726
- result: { result: "ok" }
884
+ result: "ok"
727
885
  }
728
886
  });
729
887
  } catch (e) {
730
- logger().error(`config:reset error for ${name}:`, e);
888
+ logger().error(`pipeline:delete-by-path error for ${absolutePath}:`, e);
731
889
  send({
732
890
  type: "end",
733
891
  data: {
734
892
  type: "error",
735
- ipcError: e instanceof Error ? e.message : `Unable to reset config ${name}`
893
+ ipcError: e instanceof Error ? e.message : `Unable to delete pipeline ${absolutePath}`
736
894
  }
737
895
  });
738
896
  }
@@ -746,11 +904,10 @@ var BuildHistoryStorage = class {
746
904
  this.context = context;
747
905
  }
748
906
  getStoragePath() {
749
- return join(this.context.userDataPath, "build-history");
907
+ return this.context.getConfigPath("pipelines");
750
908
  }
751
909
  getPipelinePath(pipelineId) {
752
- const sanitizedId = pipelineId.replace(/[/\:*?"<>|]/g, "_").replace(/__/g, "_").replace(/^_+|_+$/g, "");
753
- return join(this.getStoragePath(), `pipeline-${sanitizedId}.json`);
910
+ return join(this.getStoragePath(), `${pipelineId}.history.json`);
754
911
  }
755
912
  async ensureStoragePath() {
756
913
  try {
@@ -777,35 +934,6 @@ var BuildHistoryStorage = class {
777
934
  throw new Error(`Failed to save pipeline history: ${error}`);
778
935
  }
779
936
  }
780
- async applyRetentionPolicy() {
781
- try {
782
- this.logger.logger().info("Applying build history retention policy...");
783
- const policy = (await (await setupConfigFile("settings", { context: this.context })).getConfig())?.buildHistory?.retentionPolicy;
784
- if (!policy || !policy.enabled) {
785
- this.logger.logger().info("Retention policy is disabled. Skipping.");
786
- return;
787
- }
788
- const { maxAge, maxEntries } = policy;
789
- const pipelineFiles = await this.getAllPipelineFiles();
790
- for (const file of pipelineFiles) {
791
- const pipelineId = file.replace("pipeline-", "").replace(".json", "");
792
- let entries = await this.loadPipelineHistory(pipelineId);
793
- const originalCount = entries.length;
794
- if (maxAge > 0) {
795
- const minDate = Date.now() - maxAge * 24 * 60 * 60 * 1e3;
796
- entries = entries.filter((entry) => entry.createdAt >= minDate);
797
- }
798
- if (maxEntries > 0 && entries.length > maxEntries) entries = entries.sort((a, b) => b.createdAt - a.createdAt).slice(0, maxEntries);
799
- if (entries.length < originalCount) {
800
- this.logger.logger().info(`[${pipelineId}] Pruned ${originalCount - entries.length} history entries.`);
801
- await this.savePipelineHistory(pipelineId, entries);
802
- }
803
- }
804
- this.logger.logger().info("Retention policy applied successfully.");
805
- } catch (error) {
806
- this.logger.logger().error("Failed to apply retention policy:", error);
807
- }
808
- }
809
937
  async save(entry) {
810
938
  try {
811
939
  const entries = await this.loadPipelineHistory(entry.pipelineId);
@@ -814,9 +942,6 @@ var BuildHistoryStorage = class {
814
942
  else entries.push(entry);
815
943
  await this.savePipelineHistory(entry.pipelineId, entries);
816
944
  this.logger.logger().info(`Saved build history entry: ${entry.id} for pipeline: ${entry.pipelineId}`);
817
- this.applyRetentionPolicy().catch((err) => {
818
- this.logger.logger().error("Failed to apply retention policy after save:", err);
819
- });
820
945
  } catch (error) {
821
946
  this.logger.logger().error("Failed to save build history entry:", error);
822
947
  throw new Error(`Failed to save build history entry: ${error}`);
@@ -827,7 +952,8 @@ var BuildHistoryStorage = class {
827
952
  if (pipelineId) return (await this.loadPipelineHistory(pipelineId)).find((e) => e.id === id);
828
953
  const files = await this.getAllPipelineFiles();
829
954
  for (const file of files) {
830
- const pId = file.replace("pipeline-", "").replace(".json", "");
955
+ const pId = this.parsePipelineIdFromFilename(file);
956
+ if (!pId) continue;
831
957
  const entry = (await this.loadPipelineHistory(pId)).find((e) => e.id === id);
832
958
  if (entry) return entry;
833
959
  }
@@ -842,7 +968,8 @@ var BuildHistoryStorage = class {
842
968
  const files = await this.getAllPipelineFiles();
843
969
  const allEntries = [];
844
970
  for (const file of files) {
845
- const pipelineId = file.replace("pipeline-", "").replace(".json", "");
971
+ const pipelineId = this.parsePipelineIdFromFilename(file);
972
+ if (!pipelineId) continue;
846
973
  const entries = await this.loadPipelineHistory(pipelineId);
847
974
  allEntries.push(...entries);
848
975
  }
@@ -877,7 +1004,8 @@ var BuildHistoryStorage = class {
877
1004
  } else {
878
1005
  const files = await this.getAllPipelineFiles();
879
1006
  for (const file of files) {
880
- const pId = file.replace("pipeline-", "").replace(".json", "");
1007
+ const pId = this.parsePipelineIdFromFilename(file);
1008
+ if (!pId) continue;
881
1009
  const entries = await this.loadPipelineHistory(pId);
882
1010
  const entryIndex = entries.findIndex((e) => e.id === id);
883
1011
  if (entryIndex >= 0) {
@@ -911,7 +1039,8 @@ var BuildHistoryStorage = class {
911
1039
  } else {
912
1040
  const files = await this.getAllPipelineFiles();
913
1041
  for (const file of files) {
914
- const pId = file.replace("pipeline-", "").replace(".json", "");
1042
+ const pId = this.parsePipelineIdFromFilename(file);
1043
+ if (!pId) continue;
915
1044
  const entries = await this.loadPipelineHistory(pId);
916
1045
  const entryIndex = entries.findIndex((e) => e.id === id);
917
1046
  if (entryIndex >= 0) {
@@ -932,8 +1061,20 @@ var BuildHistoryStorage = class {
932
1061
  try {
933
1062
  await this.ensureStoragePath();
934
1063
  const files = await this.getAllPipelineFiles();
935
- await Promise.all(files.map((file) => unlink(join(this.getStoragePath(), file))));
1064
+ const cachePathsToDelete = /* @__PURE__ */ new Set();
1065
+ for (const file of files) {
1066
+ const pipelineId = this.parsePipelineIdFromFilename(file);
1067
+ if (pipelineId) {
1068
+ const entries = await this.loadPipelineHistory(pipelineId);
1069
+ for (const entry of entries) if (entry.cachePath) cachePathsToDelete.add(entry.cachePath);
1070
+ }
1071
+ await unlink(join(this.getStoragePath(), file));
1072
+ }
936
1073
  this.logger.logger().info("Cleared all build history");
1074
+ for (const cachePath of cachePathsToDelete) await rm(cachePath, {
1075
+ recursive: true,
1076
+ force: true
1077
+ }).catch(() => {});
937
1078
  } catch (error) {
938
1079
  this.logger.logger().error("Failed to clear build history:", error);
939
1080
  throw new Error(`Failed to clear build history: ${error}`);
@@ -941,8 +1082,16 @@ var BuildHistoryStorage = class {
941
1082
  }
942
1083
  async clearByPipeline(pipelineId) {
943
1084
  try {
944
- await unlink(this.getPipelinePath(pipelineId));
1085
+ const pipelinePath = this.getPipelinePath(pipelineId);
1086
+ const entries = await this.loadPipelineHistory(pipelineId);
1087
+ await unlink(pipelinePath);
945
1088
  this.logger.logger().info(`Cleared history for pipeline "${pipelineId}"`);
1089
+ const cachePathsToDelete = /* @__PURE__ */ new Set();
1090
+ for (const entry of entries) if (entry.cachePath) cachePathsToDelete.add(entry.cachePath);
1091
+ for (const cachePath of cachePathsToDelete) await rm(cachePath, {
1092
+ recursive: true,
1093
+ force: true
1094
+ }).catch(() => {});
946
1095
  } catch (error) {
947
1096
  if (error.code === "ENOENT") {
948
1097
  this.logger.logger().warn(`No history file found for pipeline "${pipelineId}". Nothing to clear.`);
@@ -958,25 +1107,25 @@ var BuildHistoryStorage = class {
958
1107
  const files = await this.getAllPipelineFiles();
959
1108
  const diskSpace = await checkDiskSpace(this.context.userDataPath);
960
1109
  const pipelabSize = await getFolderSize(this.context.userDataPath);
961
- const policy = (await (await setupConfigFile("settings", { context: this.context })).getConfig())?.buildHistory?.retentionPolicy || {
962
- enabled: false,
963
- maxEntries: 50,
964
- maxAge: 30
965
- };
1110
+ const folders = [];
1111
+ for (const folder of this.context.getSandboxFolders()) {
1112
+ const size = await getFolderSize(folder.path);
1113
+ folders.push({
1114
+ name: folder.name,
1115
+ label: folder.label,
1116
+ size
1117
+ });
1118
+ }
966
1119
  if (allEntries.length === 0) return {
967
1120
  totalEntries: 0,
968
1121
  totalSize: 0,
969
1122
  numberOfPipelines: files.length,
970
1123
  userDataPath: this.context.userDataPath,
971
- retentionPolicy: {
972
- enabled: policy.enabled,
973
- maxEntries: policy.maxEntries,
974
- maxAge: policy.maxAge
975
- },
976
1124
  disk: {
977
1125
  total: diskSpace.size,
978
1126
  free: diskSpace.free,
979
- pipelab: pipelabSize
1127
+ pipelab: pipelabSize,
1128
+ folders
980
1129
  }
981
1130
  };
982
1131
  let totalSize = 0;
@@ -996,15 +1145,11 @@ var BuildHistoryStorage = class {
996
1145
  newestEntry: sortedEntries[sortedEntries.length - 1]?.createdAt,
997
1146
  numberOfPipelines: files.length,
998
1147
  userDataPath: this.context.userDataPath,
999
- retentionPolicy: {
1000
- enabled: policy.enabled,
1001
- maxEntries: policy.maxEntries,
1002
- maxAge: policy.maxAge
1003
- },
1004
1148
  disk: {
1005
1149
  total: diskSpace.size,
1006
1150
  free: diskSpace.free,
1007
- pipelab: pipelabSize
1151
+ pipelab: pipelabSize,
1152
+ folders
1008
1153
  }
1009
1154
  };
1010
1155
  } catch (error) {
@@ -1015,11 +1160,15 @@ var BuildHistoryStorage = class {
1015
1160
  async getAllPipelineFiles() {
1016
1161
  try {
1017
1162
  await this.ensureStoragePath();
1018
- return (await readdir(this.getStoragePath())).filter((file) => file.startsWith("pipeline-") && file.endsWith(".json"));
1163
+ return (await readdir(this.getStoragePath())).filter((file) => file.endsWith(".history.json"));
1019
1164
  } catch (error) {
1020
1165
  return [];
1021
1166
  }
1022
1167
  }
1168
+ parsePipelineIdFromFilename(filename) {
1169
+ const match = filename.match(/^(.+)\.history\.json$/);
1170
+ return match ? match[1] : null;
1171
+ }
1023
1172
  };
1024
1173
  //#endregion
1025
1174
  //#region src/handlers/history.ts
@@ -1301,40 +1450,6 @@ const registerHistoryHandlers = (context) => {
1301
1450
  });
1302
1451
  }
1303
1452
  });
1304
- handle("build-history:configure", async (_, { send, value }) => {
1305
- try {
1306
- logger().info("Updating build history configuration:", value.config);
1307
- const settings = await setupConfigFile("settings", { context });
1308
- const currentConfig = await settings.getConfig();
1309
- const newConfig = {
1310
- ...currentConfig,
1311
- buildHistory: {
1312
- ...currentConfig?.buildHistory,
1313
- retentionPolicy: {
1314
- ...currentConfig?.buildHistory?.retentionPolicy,
1315
- ...value.config.retentionPolicy
1316
- }
1317
- }
1318
- };
1319
- await settings.saveConfig(newConfig);
1320
- send({
1321
- type: "end",
1322
- data: {
1323
- type: "success",
1324
- result: { result: "ok" }
1325
- }
1326
- });
1327
- } catch (error) {
1328
- logger().error("Failed to configure build history:", error);
1329
- send({
1330
- type: "end",
1331
- data: {
1332
- type: "error",
1333
- ipcError: error instanceof Error ? error.message : "Failed to configure build history"
1334
- }
1335
- });
1336
- }
1337
- });
1338
1453
  };
1339
1454
  //#endregion
1340
1455
  //#region ../../node_modules/serve-handler/src/glob-slash.js
@@ -8324,7 +8439,7 @@ var require_error = /* @__PURE__ */ __commonJSMin(((exports, module) => {
8324
8439
  })();
8325
8440
  }));
8326
8441
  //#endregion
8327
- //#region src/migrations.ts
8442
+ //#region src/server.ts
8328
8443
  var import_src = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
8329
8444
  const { promisify } = __require("util");
8330
8445
  const path$1 = __require("path");
@@ -8778,47 +8893,6 @@ var import_src = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((export
8778
8893
  stream.pipe(response);
8779
8894
  };
8780
8895
  })))(), 1);
8781
- /**
8782
- * Registers migration handlers for the CLI/Standalone server.
8783
- * This overrides the default handlers from @pipelab/core-node to include
8784
- * pipeline and file repo migrations directly in the backend.
8785
- */
8786
- function registerMigrationHandlers(context) {
8787
- const { handle } = useAPI();
8788
- const { logger } = useLogger();
8789
- handle("config:load", async (_, { send, value }) => {
8790
- const { config: name } = value;
8791
- logger().info("[CLI Migration] config:load", name);
8792
- try {
8793
- let migrator = null;
8794
- if (name === "projects") migrator = fileRepoMigrations;
8795
- else if (name === "settings") migrator = appSettingsMigrator;
8796
- else if (name.startsWith("pipeline-") || name.endsWith(".plb")) migrator = savedFileMigrator;
8797
- if (!migrator) throw new Error(`No migrator found for configuration: ${name}. All files loaded via config:load MUST have a migration schema.`);
8798
- send({
8799
- type: "end",
8800
- data: {
8801
- type: "success",
8802
- result: { result: await (await setupConfigFile(name, {
8803
- context,
8804
- migrator
8805
- })).getConfig() }
8806
- }
8807
- });
8808
- } catch (e) {
8809
- logger().error(`[CLI Migration] config:load error for ${name}:`, e);
8810
- send({
8811
- type: "end",
8812
- data: {
8813
- type: "error",
8814
- ipcError: e instanceof Error ? e.message : `Unable to load config ${name}`
8815
- }
8816
- });
8817
- }
8818
- });
8819
- }
8820
- //#endregion
8821
- //#region src/server.ts
8822
8896
  const sendStartupProgress = (message) => {
8823
8897
  console.log(`[Startup Progress] ${message}`);
8824
8898
  webSocketServer.broadcast("startup:progress", {
@@ -8832,10 +8906,43 @@ const sendStartupReady = () => {
8832
8906
  };
8833
8907
  async function serveCommand(options, version, _dirname) {
8834
8908
  if (!options.userData) throw new Error("userDataPath is required for serveCommand");
8835
- const context = new PipelabContext({ userDataPath: options.userData });
8909
+ const releaseTag = version.includes("beta") ? "beta" : "latest";
8910
+ const context = new PipelabContext({
8911
+ userDataPath: options.userData,
8912
+ releaseTag
8913
+ });
8836
8914
  let rawAssetFolder;
8837
- if (!isDev) rawAssetFolder = await fetchPipelabAsset("@pipelab/ui", "latest", { context });
8915
+ if (!isDev) rawAssetFolder = await fetchPipelabAsset("@pipelab/ui", releaseTag, { context });
8838
8916
  const server = http$1.createServer(async (request, response) => {
8917
+ if (request.url?.startsWith("/media-file/")) {
8918
+ const encodedPath = request.url.substring(12);
8919
+ const filePath = decodeURIComponent(encodedPath);
8920
+ const normalizedPath = filePath.startsWith("/") && filePath.match(/^\/[a-zA-Z]:/) ? filePath.substring(1) : filePath;
8921
+ if (existsSync(normalizedPath)) try {
8922
+ const content = await readFile(normalizedPath);
8923
+ let contentType = "application/octet-stream";
8924
+ if (normalizedPath.endsWith(".png")) contentType = "image/png";
8925
+ else if (normalizedPath.endsWith(".jpg") || normalizedPath.endsWith(".jpeg")) contentType = "image/jpeg";
8926
+ else if (normalizedPath.endsWith(".svg")) contentType = "image/svg+xml";
8927
+ else if (normalizedPath.endsWith(".gif")) contentType = "image/gif";
8928
+ else if (normalizedPath.endsWith(".webp")) contentType = "image/webp";
8929
+ response.writeHead(200, {
8930
+ "Content-Type": contentType,
8931
+ "Access-Control-Allow-Origin": "*"
8932
+ });
8933
+ response.end(content);
8934
+ return;
8935
+ } catch (e) {
8936
+ response.writeHead(500, { "Content-Type": "text/plain" });
8937
+ response.end(`Error reading file: ${e}`);
8938
+ return;
8939
+ }
8940
+ else {
8941
+ response.writeHead(404, { "Content-Type": "text/plain" });
8942
+ response.end(`File not found: ${normalizedPath}`);
8943
+ return;
8944
+ }
8945
+ }
8839
8946
  if (isDev) {
8840
8947
  response.writeHead(200, { "Content-Type": "text/html" });
8841
8948
  response.end(`
@@ -8854,7 +8961,13 @@ async function serveCommand(options, version, _dirname) {
8854
8961
  response.end(`Error: UI directory not found at ${rawAssetFolder}.\nPlease run 'pnpm build' in apps/ui to generate the distribution.`);
8855
8962
  return;
8856
8963
  }
8857
- return (0, import_src.default)(request, response, { public: rawAssetFolder });
8964
+ return (0, import_src.default)(request, response, {
8965
+ public: rawAssetFolder,
8966
+ rewrites: [{
8967
+ source: "/**",
8968
+ destination: "/index.html"
8969
+ }]
8970
+ });
8858
8971
  });
8859
8972
  console.log(`Starting Pipelab server on port ${options.port}...`);
8860
8973
  if (isDev) {
@@ -8866,14 +8979,30 @@ async function serveCommand(options, version, _dirname) {
8866
8979
  version,
8867
8980
  context
8868
8981
  });
8869
- registerMigrationHandlers(context);
8870
8982
  sendStartupReady();
8871
8983
  return server;
8872
8984
  }
8873
8985
  //#endregion
8874
8986
  //#region src/utils/remote.ts
8875
- const DEFAULT_NODE_VERSION = "24.14.1";
8876
- const DEFAULT_PNPM_VERSION = "10.12.0";
8987
+ function isPackageComplete(packageDir) {
8988
+ return existsSync(join(packageDir, "package.json"));
8989
+ }
8990
+ function isNodeJSComplete(nodePath) {
8991
+ try {
8992
+ return existsSync(nodePath) && statSync(nodePath).size > 0;
8993
+ } catch {
8994
+ return false;
8995
+ }
8996
+ }
8997
+ function isDependenciesInstalledSync(packageDir) {
8998
+ const nodeModulesPath = join(packageDir, "node_modules");
8999
+ if (!existsSync(nodeModulesPath)) return false;
9000
+ try {
9001
+ return readdirSync(nodeModulesPath).length > 0;
9002
+ } catch {
9003
+ return false;
9004
+ }
9005
+ }
8877
9006
  /**
8878
9007
  * In-memory lock to prevent concurrent operations on the same resource (e.g., downloading Node.js).
8879
9008
  */
@@ -8895,16 +9024,32 @@ async function withLock(key, fn) {
8895
9024
  activeOperations.set(key, promise);
8896
9025
  return promise;
8897
9026
  }
9027
+ let isOnlineCached = null;
9028
+ let lastCheckTime = 0;
9029
+ async function isOnline() {
9030
+ const now = Date.now();
9031
+ if (isOnlineCached !== null && now - lastCheckTime < 1e4) return isOnlineCached;
9032
+ try {
9033
+ await Promise.race([dns.lookup("registry.npmjs.org"), new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("timeout")), 1500))]);
9034
+ isOnlineCached = true;
9035
+ } catch {
9036
+ isOnlineCached = false;
9037
+ }
9038
+ lastCheckTime = now;
9039
+ return isOnlineCached;
9040
+ }
8898
9041
  /**
8899
9042
  * Robust utility to fetch, cache, and resolve an NPM package.
8900
9043
  * Centralized in core-node to avoid circular dependencies.
8901
9044
  */
8902
9045
  async function fetchPackage(packageName, versionOrRange, options) {
9046
+ const start = Date.now();
8903
9047
  if (isDev && projectRoot && process.env.PIPELAB_FORCE_NPM !== "true") {
8904
9048
  if (packageName.startsWith("@pipelab/")) {
9049
+ const localStart = Date.now();
8905
9050
  const local = await tryResolveMonorepoPackage(packageName);
8906
9051
  if (local) {
8907
- console.log(`[Fetcher] ${packageName}: Resolved to local source at ${local.packageDir}`);
9052
+ console.log(`[Fetcher] ${packageName}: Resolved to local source at ${local.packageDir} (${Date.now() - localStart}ms)`);
8908
9053
  return {
8909
9054
  ...local,
8910
9055
  resolvedVersion: "workspace"
@@ -8915,9 +9060,20 @@ async function fetchPackage(packageName, versionOrRange, options) {
8915
9060
  const ctx = options.context;
8916
9061
  const baseDir = ctx.getPackagesPath(packageName);
8917
9062
  let resolvedVersion;
8918
- console.log(`[Fetcher] Resolving ${packageName}@${versionOrRange || "latest"}...`);
8919
- try {
8920
- const cachePath = join(ctx.userDataPath, "cache", "pacote");
9063
+ let resolvedVersionOrRange = versionOrRange;
9064
+ if (resolvedVersionOrRange === "local") resolvedVersionOrRange = "latest";
9065
+ console.log(`[Fetcher] Resolving ${packageName}@${resolvedVersionOrRange || "latest"}...`);
9066
+ const resolveStart = Date.now();
9067
+ if (!await isOnline()) {
9068
+ console.warn(`[Fetcher] ${packageName}: offline mode detected (${Date.now() - resolveStart}ms), trying local fallback...`);
9069
+ const fallbackStart = Date.now();
9070
+ const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, /* @__PURE__ */ new Error("Offline"), baseDir, packageName);
9071
+ if (fallbackVersion) {
9072
+ resolvedVersion = fallbackVersion;
9073
+ console.log(`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`);
9074
+ } else throw new Error(`Offline and no local fallback version available for ${packageName}`);
9075
+ } else try {
9076
+ const cachePath = ctx.getCachePath(CacheFolder.Pacote);
8921
9077
  let packumentPromise = packumentRequests.get(packageName);
8922
9078
  if (!packumentPromise) {
8923
9079
  packumentPromise = pacote.packument(packageName, { cache: cachePath });
@@ -8925,31 +9081,78 @@ async function fetchPackage(packageName, versionOrRange, options) {
8925
9081
  }
8926
9082
  const packument = await packumentPromise;
8927
9083
  const versions = Object.keys(packument.versions);
8928
- const range = versionOrRange || "latest";
8929
- const foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
9084
+ const range = resolvedVersionOrRange || "latest";
9085
+ let foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
9086
+ if (range === "latest" && ctx.releaseTag && ctx.releaseTag !== "latest") {
9087
+ const releaseTagVersion = packument["dist-tags"]?.[ctx.releaseTag];
9088
+ if (releaseTagVersion && semver.valid(releaseTagVersion)) {
9089
+ if (!foundVersion || semver.valid(foundVersion) && semver.gte(releaseTagVersion, foundVersion)) {
9090
+ console.log(`[Fetcher] Using release tag "${ctx.releaseTag}" (${releaseTagVersion}) instead of "latest" (${foundVersion || "none"}) for ${packageName}`);
9091
+ foundVersion = releaseTagVersion;
9092
+ } else if (foundVersion) console.warn(`[Fetcher] Tag "${ctx.releaseTag}" (${releaseTagVersion}) is older than "latest" (${foundVersion}) for ${packageName}, keeping "latest"`);
9093
+ }
9094
+ }
8930
9095
  if (!foundVersion) throw new Error(`Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(packument["dist-tags"] || {}).join(", ")})`);
8931
9096
  resolvedVersion = foundVersion;
8932
- console.log(`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm`);
9097
+ console.log(`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm (${Date.now() - resolveStart}ms)`);
8933
9098
  } catch (error) {
8934
- console.warn(`[Fetcher] ${packageName}: remote resolution failed, trying local fallback...`);
8935
- const fallbackVersion = await tryLocalFallback(versionOrRange, error, baseDir, packageName);
8936
- if (fallbackVersion) resolvedVersion = fallbackVersion;
8937
- else throw error;
8938
- }
8939
- const cachePath = join(ctx.userDataPath, "cache", "pacote");
9099
+ console.warn(`[Fetcher] ${packageName}: remote resolution failed (${Date.now() - resolveStart}ms), trying local fallback...`);
9100
+ const fallbackStart = Date.now();
9101
+ const fallbackVersion = await tryLocalFallback(resolvedVersionOrRange, error, baseDir, packageName);
9102
+ if (fallbackVersion) {
9103
+ resolvedVersion = fallbackVersion;
9104
+ console.log(`[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`);
9105
+ } else throw error;
9106
+ }
9107
+ const cachePath = ctx.getCachePath(CacheFolder.Pacote);
8940
9108
  const packageDir = join(baseDir, resolvedVersion);
8941
- if (existsSync(packageDir) && !options?.installDeps) return {
8942
- packageDir,
8943
- resolvedVersion
8944
- };
9109
+ const checkStart = Date.now();
9110
+ const isInstalled = options?.installDeps ? isPackageComplete(packageDir) && isDependenciesInstalledSync(packageDir) : isPackageComplete(packageDir);
9111
+ const checkDuration = Date.now() - checkStart;
9112
+ if (isInstalled) {
9113
+ console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Already installed (check took ${checkDuration}ms, fetchPackage took ${Date.now() - start}ms)`);
9114
+ return {
9115
+ packageDir,
9116
+ resolvedVersion
9117
+ };
9118
+ }
8945
9119
  return withLock(`package:${packageName}:${resolvedVersion}`, async () => {
8946
- if (!existsSync(packageDir)) {
9120
+ if (!isPackageComplete(packageDir)) {
8947
9121
  console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
8948
- await mkdir(packageDir, { recursive: true });
8949
- await pacote.extract(`${packageName}@${resolvedVersion}`, packageDir, { cache: cachePath });
9122
+ const downloadStart = Date.now();
9123
+ const tempDir = join(baseDir, `.tmp-${resolvedVersion}-${Math.random().toString(36).slice(2)}`);
9124
+ await mkdir(tempDir, { recursive: true });
9125
+ try {
9126
+ await pacote.extract(`${packageName}@${resolvedVersion}`, tempDir, { cache: cachePath });
9127
+ await resolveEntryPoint(tempDir, packageName);
9128
+ if (existsSync(packageDir)) await rm(packageDir, {
9129
+ recursive: true,
9130
+ force: true
9131
+ }).catch(() => {});
9132
+ try {
9133
+ await rename(tempDir, packageDir);
9134
+ } catch (err) {
9135
+ if (isPackageComplete(packageDir)) console.log(`[Fetcher] Destination ${packageDir} already exists and is valid.`);
9136
+ else throw err;
9137
+ }
9138
+ console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloaded and extracted in ${Date.now() - downloadStart}ms`);
9139
+ } catch (err) {
9140
+ await rm(tempDir, {
9141
+ recursive: true,
9142
+ force: true
9143
+ }).catch(() => {});
9144
+ throw err;
9145
+ }
8950
9146
  }
9147
+ const entryStart = Date.now();
8951
9148
  const entryPoint = await resolveEntryPoint(packageDir, packageName);
8952
- if (options?.installDeps) await installDependencies(packageDir, packageName, options);
9149
+ console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Resolved entry point in ${Date.now() - entryStart}ms`);
9150
+ if (options?.installDeps) {
9151
+ const depsStart = Date.now();
9152
+ await installDependencies(packageDir, packageName, options);
9153
+ console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Installed dependencies in ${Date.now() - depsStart}ms`);
9154
+ }
9155
+ console.log(`[Fetcher] ${packageName}@${resolvedVersion}: FetchPackage complete in ${Date.now() - start}ms`);
8953
9156
  return {
8954
9157
  packageDir,
8955
9158
  resolvedVersion,
@@ -8978,7 +9181,7 @@ async function runPnpm(cwd, options) {
8978
9181
  ...process.env,
8979
9182
  NODE_ENV: "production",
8980
9183
  PATH: nodePath ? `${dirname(nodePath)}${delimiter}${process.env.PATH}` : process.env.PATH,
8981
- PNPM_HOME: join(ctx.userDataPath, "pnpm"),
9184
+ PNPM_HOME: ctx.getPnpmPath(),
8982
9185
  PNPM_ONLY_ALLOW_TRUSTED_DEPENDENCIES: "false",
8983
9186
  ...extraEnv
8984
9187
  }
@@ -8988,24 +9191,31 @@ async function runPnpm(cwd, options) {
8988
9191
  * Installs a specific version of Node.js if not already present.
8989
9192
  */
8990
9193
  async function ensureNodeJS(context, version = DEFAULT_NODE_VERSION) {
9194
+ const checkStart = Date.now();
8991
9195
  const isWindows = process.platform === "win32";
8992
9196
  const nodeDir = context.getThirdPartyPath("node", version);
8993
9197
  const finalNodePath = join(nodeDir, isWindows ? "node.exe" : "bin/node");
8994
- if (existsSync(finalNodePath)) return finalNodePath;
9198
+ if (isNodeJSComplete(finalNodePath)) {
9199
+ console.log(`[Environment] Node.js check took ${Date.now() - checkStart}ms (found at ${finalNodePath})`);
9200
+ return finalNodePath;
9201
+ }
8995
9202
  return withLock(`node:${version}`, async () => {
8996
- if (existsSync(finalNodePath)) return finalNodePath;
9203
+ if (isNodeJSComplete(finalNodePath)) return finalNodePath;
8997
9204
  const arch = process.arch === "x64" ? "x64" : process.arch === "arm64" ? "arm64" : "x86";
8998
9205
  const platform = isWindows ? "win" : process.platform === "darwin" ? "osx" : "linux";
8999
9206
  const extension = isWindows ? "zip" : "tar.gz";
9000
9207
  const fileName = `node-v${version}-${platform === "osx" ? "darwin" : platform}-${arch}.${extension}`;
9001
9208
  const downloadUrl = `https://nodejs.org/dist/v${version}/${fileName}`;
9002
- const tempDir = await generateTempFolder(tmpdir());
9209
+ const tempDir = await context.createTempFolder("node-download-");
9003
9210
  const archivePath = join(tempDir, fileName);
9004
9211
  sendStartupProgress(`Downloading Node.js v${version}...`);
9005
9212
  console.log(`Downloading Node.js from ${downloadUrl}...`);
9213
+ const dlStart = Date.now();
9006
9214
  await downloadFile(downloadUrl, archivePath);
9215
+ console.log(`[Environment] Node.js download took ${Date.now() - dlStart}ms`);
9007
9216
  sendStartupProgress(`Extracting Node.js v${version}...`);
9008
9217
  console.log(`Extracting Node.js to ${tempDir}...`);
9218
+ const extStart = Date.now();
9009
9219
  const extractTempDir = join(tempDir, "extracted");
9010
9220
  await mkdir(extractTempDir, { recursive: true });
9011
9221
  if (extension === "zip") await extractZip(archivePath, extractTempDir);
@@ -9013,17 +9223,35 @@ async function ensureNodeJS(context, version = DEFAULT_NODE_VERSION) {
9013
9223
  const nodeSubDir = (await readdir(extractTempDir)).find((entry) => entry.startsWith(`node-v${version}`));
9014
9224
  if (!nodeSubDir) throw new Error(`Could not find extracted Node.js directory`);
9015
9225
  const sourceDir = join(extractTempDir, nodeSubDir);
9016
- await mkdir(dirname(nodeDir), { recursive: true });
9017
- await rm(nodeDir, {
9018
- recursive: true,
9019
- force: true
9020
- });
9021
- await cp(sourceDir, nodeDir, { recursive: true });
9022
- await rm(tempDir, {
9023
- recursive: true,
9024
- force: true
9025
- });
9026
- if (!isWindows) await chmod(finalNodePath, 493).catch(() => {});
9226
+ const parentDir = dirname(nodeDir);
9227
+ await mkdir(parentDir, { recursive: true });
9228
+ const tempNodeDir = join(parentDir, `.tmp-node-${version}-${Math.random().toString(36).slice(2)}`);
9229
+ await mkdir(tempNodeDir, { recursive: true });
9230
+ try {
9231
+ await cp(sourceDir, tempNodeDir, { recursive: true });
9232
+ if (!isWindows) await chmod(join(tempNodeDir, "bin/node"), 493).catch(() => {});
9233
+ if (existsSync(nodeDir)) await rm(nodeDir, {
9234
+ recursive: true,
9235
+ force: true
9236
+ }).catch(() => {});
9237
+ try {
9238
+ await rename(tempNodeDir, nodeDir);
9239
+ } catch (err) {
9240
+ if (isNodeJSComplete(finalNodePath)) console.log(`[Fetcher] Node.js directory already exists and is valid.`);
9241
+ else throw err;
9242
+ }
9243
+ console.log(`[Environment] Node.js extraction took ${Date.now() - extStart}ms`);
9244
+ } finally {
9245
+ await rm(tempNodeDir, {
9246
+ recursive: true,
9247
+ force: true
9248
+ }).catch(() => {});
9249
+ await rm(tempDir, {
9250
+ recursive: true,
9251
+ force: true
9252
+ }).catch(() => {});
9253
+ }
9254
+ console.log(`[Environment] Node.js set up complete in ${Date.now() - checkStart}ms`);
9027
9255
  return finalNodePath;
9028
9256
  });
9029
9257
  }
@@ -9031,36 +9259,56 @@ async function ensureNodeJS(context, version = DEFAULT_NODE_VERSION) {
9031
9259
  * Installs the PNPM package from npm if not already present.
9032
9260
  */
9033
9261
  async function ensurePNPM(context, version = DEFAULT_PNPM_VERSION) {
9262
+ const checkStart = Date.now();
9034
9263
  const pnpmPath = join(context.getPackagesPath("pnpm", version), "bin", "pnpm.cjs");
9035
- if (existsSync(pnpmPath)) return pnpmPath;
9264
+ if (existsSync(pnpmPath)) {
9265
+ console.log(`[Environment] PNPM check took ${Date.now() - checkStart}ms (found at ${pnpmPath})`);
9266
+ return pnpmPath;
9267
+ }
9036
9268
  return withLock(`pnpm:${version}`, async () => {
9037
9269
  if (existsSync(pnpmPath)) return pnpmPath;
9038
9270
  sendStartupProgress(`Checking PNPM v${version}...`);
9039
9271
  const { packageDir } = await fetchPackage("pnpm", version, { context });
9272
+ console.log(`[Environment] PNPM set up complete in ${Date.now() - checkStart}ms`);
9040
9273
  return join(packageDir, "bin", "pnpm.cjs");
9041
9274
  });
9042
9275
  }
9043
9276
  async function installDependencies(packageDir, packageName, options) {
9277
+ const start = Date.now();
9044
9278
  const nodeModulesPath = join(packageDir, "node_modules");
9045
- if (existsSync(nodeModulesPath)) try {
9046
- if ((await readdir(nodeModulesPath)).length === 0) console.warn(`[Fetcher] ${packageName}: node_modules exists but is empty. Re-installing...`);
9047
- else {
9048
- console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
9049
- return;
9050
- }
9051
- } catch (e) {}
9052
- console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
9279
+ if (isDependenciesInstalledSync(packageDir)) {
9280
+ console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
9281
+ return;
9282
+ }
9283
+ const tempDir = `${packageDir}.tmp-deps-${Math.random().toString(36).slice(2)}`;
9284
+ await mkdir(tempDir, { recursive: true });
9053
9285
  try {
9054
- const { all } = await runPnpm(packageDir, {
9286
+ await cp(join(packageDir, "package.json"), join(tempDir, "package.json"));
9287
+ console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
9288
+ const pnpmStart = Date.now();
9289
+ const { all } = await runPnpm(tempDir, {
9055
9290
  signal: options.signal,
9056
9291
  context: options.context
9057
9292
  });
9293
+ console.log(`[Fetcher] ${packageName}: pnpm install command took ${Date.now() - pnpmStart}ms`);
9058
9294
  if (all) console.log(`[Fetcher] ${packageName}: Installation trace:\n${all}`);
9059
- console.log(`[Fetcher] ${packageName}: Dependencies installed successfully.`);
9295
+ const tempNodeModules = join(tempDir, "node_modules");
9296
+ if (existsSync(nodeModulesPath)) await rm(nodeModulesPath, {
9297
+ recursive: true,
9298
+ force: true
9299
+ }).catch(() => {});
9300
+ const renameStart = Date.now();
9301
+ await rename(tempNodeModules, nodeModulesPath);
9302
+ console.log(`[Fetcher] ${packageName}: Dependencies installed successfully (rename took ${Date.now() - renameStart}ms, total installDependencies took ${Date.now() - start}ms).`);
9060
9303
  } catch (err) {
9061
9304
  console.error(`[Fetcher] ${packageName}: CRITICAL ERROR during dependency installation: ${err.message}`);
9062
9305
  if (err.all) console.error(`[Fetcher] ${packageName}: Error details:\n${err.all}`);
9063
9306
  throw new Error(`Failed to install dependencies for ${packageName}. See logs for details.`);
9307
+ } finally {
9308
+ await rm(tempDir, {
9309
+ recursive: true,
9310
+ force: true
9311
+ }).catch(() => {});
9064
9312
  }
9065
9313
  }
9066
9314
  async function fetchPipelabAsset(packageName, versionOrRange, options) {
@@ -9153,22 +9401,27 @@ async function crawlMonorepoPackages() {
9153
9401
  }
9154
9402
  return cache;
9155
9403
  }
9156
- async function findLatestLocalVersion(baseDir) {
9404
+ async function tryLocalFallback(versionOrRange, _error, baseDir, logPrefix) {
9157
9405
  if (!existsSync(baseDir)) return null;
9158
9406
  try {
9159
- return (await readdir(baseDir, { withFileTypes: true })).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name).sort((a, b) => b.localeCompare(a, void 0, {
9160
- numeric: true,
9161
- sensitivity: "base"
9162
- }))[0] || null;
9163
- } catch {
9164
- return null;
9165
- }
9166
- }
9167
- async function tryLocalFallback(_version, _error, baseDir, logPrefix) {
9168
- const latestLocal = await findLatestLocalVersion(baseDir);
9169
- if (latestLocal) {
9170
- console.info(`[Fetcher] ${logPrefix}: Using locally cached version: ${latestLocal}`);
9171
- return latestLocal;
9407
+ const localVersions = (await readdir(baseDir, { withFileTypes: true })).filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name).filter((name) => !!semver.valid(name));
9408
+ if (localVersions.length === 0) return null;
9409
+ const range = versionOrRange || "latest";
9410
+ if (range === "latest") {
9411
+ const latestLocal = localVersions.sort((a, b) => semver.rcompare(a, b))[0] || null;
9412
+ if (latestLocal) {
9413
+ console.info(`[Fetcher] ${logPrefix}: Using locally cached latest version: ${latestLocal}`);
9414
+ return latestLocal;
9415
+ }
9416
+ } else {
9417
+ const matched = semver.maxSatisfying(localVersions, range);
9418
+ if (matched) {
9419
+ console.info(`[Fetcher] ${logPrefix}: Using locally cached matching version: ${matched} for range ${range}`);
9420
+ return matched;
9421
+ }
9422
+ }
9423
+ } catch (e) {
9424
+ console.warn(`[Fetcher] ${logPrefix}: Error during local fallback resolution:`, e);
9172
9425
  }
9173
9426
  return null;
9174
9427
  }
@@ -9202,50 +9455,6 @@ const checkParams = (definitionParams, elementParams) => {
9202
9455
  } else console.warn("Unexpected param \"" + param + "\"");
9203
9456
  for (const param of expected) if (isRequired(definitionParams[param])) throw new Error("Missing param \"" + param + "\"");
9204
9457
  };
9205
- const handleConditionExecute = async (nodeId, pluginId, params, cwd, context) => {
9206
- const ctx = context;
9207
- const { plugins } = usePlugins();
9208
- const { logger } = useLogger();
9209
- const node = plugins.value.find((plugin) => plugin.id === pluginId)?.nodes.find((node) => node.node.id === nodeId);
9210
- if (!node) return {
9211
- type: "error",
9212
- ipcError: "Node not found"
9213
- };
9214
- try {
9215
- if (!(await stat(cwd)).isDirectory()) throw new Error(`Execution directory "${cwd}" exists but is not a directory.`);
9216
- } catch (e) {
9217
- if (e.code === "ENOENT") await mkdir(cwd, { recursive: true });
9218
- else throw e;
9219
- }
9220
- try {
9221
- checkParams(node.node.params, params);
9222
- const resolvedInputs = params;
9223
- return {
9224
- type: "success",
9225
- result: {
9226
- outputs: {},
9227
- value: await node.runner({
9228
- inputs: resolvedInputs,
9229
- log: (...args) => {
9230
- logger().info(`[${node.node.name}]`, ...args);
9231
- },
9232
- meta: { definition: "" },
9233
- setMeta: () => {
9234
- logger().info("set meta defined here");
9235
- },
9236
- cwd,
9237
- context: ctx
9238
- })
9239
- }
9240
- };
9241
- } catch (e) {
9242
- logger().error("Error in condition execution:", e);
9243
- return {
9244
- type: "error",
9245
- ipcError: String(e)
9246
- };
9247
- }
9248
- };
9249
9458
  const handleActionExecute = async (nodeId, pluginId, params, mainWindow, send, abortSignal, cwd, cachePath, context) => {
9250
9459
  const ctx = context;
9251
9460
  const { plugins } = usePlugins();
@@ -9331,26 +9540,55 @@ const handleActionExecute = async (nodeId, pluginId, params, mainWindow, send, a
9331
9540
  };
9332
9541
  //#endregion
9333
9542
  //#region src/plugins-registry.ts
9334
- const DEFAULT_PLUGIN_IDS = [
9335
- "construct",
9336
- "filesystem",
9337
- "system",
9338
- "steam",
9339
- "itch",
9340
- "electron",
9341
- "discord",
9342
- "poki",
9343
- "nvpatch",
9344
- "tauri",
9345
- "minify",
9346
- "netlify"
9347
- ];
9543
+ const enhancePluginDefinition = async (plugin, packageDir, fallbackName, fallbackVersion) => {
9544
+ if (!plugin) return plugin;
9545
+ let packageName = fallbackName;
9546
+ let version = fallbackVersion;
9547
+ let pipelabMeta = null;
9548
+ let pkgDescription = "";
9549
+ try {
9550
+ const pkgJsonPath = join(packageDir, "package.json");
9551
+ if (existsSync(pkgJsonPath)) {
9552
+ const pkgContent = await readFile(pkgJsonPath, "utf8");
9553
+ const pkg = JSON.parse(pkgContent);
9554
+ if (pkg.name) packageName = pkg.name;
9555
+ if (pkg.version) version = pkg.version;
9556
+ if (pkg.description) pkgDescription = pkg.description;
9557
+ if (pkg.pipelab) pipelabMeta = pkg.pipelab;
9558
+ }
9559
+ } catch (e) {
9560
+ console.error(`[Plugins] Failed to read package.json in ${packageDir}:`, e);
9561
+ }
9562
+ plugin.packageName = packageName;
9563
+ plugin.id = packageName;
9564
+ plugin.version = fallbackVersion === "local" ? "local" : version;
9565
+ plugin.isOfficial = packageName.startsWith("@pipelab/");
9566
+ plugin.name = pipelabMeta?.name || packageName;
9567
+ plugin.description = pipelabMeta?.description || pkgDescription || "";
9568
+ if (pipelabMeta?.icon) if (typeof pipelabMeta.icon === "string") {
9569
+ let iconPath = pipelabMeta.icon;
9570
+ if (iconPath.startsWith(".")) iconPath = pathToFileURL(join(packageDir, iconPath)).href;
9571
+ plugin.icon = {
9572
+ type: "image",
9573
+ image: iconPath
9574
+ };
9575
+ } else plugin.icon = pipelabMeta.icon;
9576
+ else plugin.icon = {
9577
+ type: "icon",
9578
+ icon: "pi pi-box"
9579
+ };
9580
+ return plugin;
9581
+ };
9348
9582
  const loadPipelabPlugin = async (id, options) => {
9583
+ const start = Date.now();
9349
9584
  try {
9350
- const { packageDir, entryPoint } = await fetchPipelabPlugin(`@pipelab/plugin-${id}`, "latest", {
9585
+ const packageName = id;
9586
+ const fetchStart = Date.now();
9587
+ const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, options.context.releaseTag, {
9351
9588
  context: options.context,
9352
9589
  installDeps: false
9353
9590
  });
9591
+ const fetchDuration = Date.now() - fetchStart;
9354
9592
  console.log(`[Plugins] [${id}] Attempting to import from: ${entryPoint}`);
9355
9593
  if (!existsSync(entryPoint)) {
9356
9594
  console.error(`[Plugins] [${id}] CRITICAL: Plugin entry point not found at ${entryPoint}`);
@@ -9359,37 +9597,149 @@ const loadPipelabPlugin = async (id, options) => {
9359
9597
  console.log(`[Plugins] [${id}] Directory contents:`, files);
9360
9598
  } catch (e) {}
9361
9599
  }
9600
+ const importStart = Date.now();
9362
9601
  const pluginModule = await import(pathToFileURL(entryPoint).href);
9363
- console.log(`[Plugins] [${id}] Successfully loaded from: ${packageDir}`);
9364
- return pluginModule.default;
9602
+ const importDuration = Date.now() - importStart;
9603
+ const totalDuration = Date.now() - start;
9604
+ console.log(`[Plugins] [${id}] Successfully loaded from: ${packageDir} (fetch: ${fetchDuration}ms, import: ${importDuration}ms, total: ${totalDuration}ms)`);
9605
+ const plugin = pluginModule.default;
9606
+ await enhancePluginDefinition(plugin, packageDir, packageName, options.context.releaseTag);
9607
+ return plugin;
9365
9608
  } catch (e) {
9366
- console.error(`[Plugins] [${id}] CRITICAL: Failed to load:`, e);
9609
+ console.error(`[Plugins] [${id}] CRITICAL: Failed to load after ${Date.now() - start}ms:`, e);
9367
9610
  if (e.code === "ERR_MODULE_NOT_FOUND") console.error(`[Plugins] [${id}] This usually means a dependency is missing in the plugin's node_modules.`);
9368
9611
  return null;
9369
9612
  }
9370
9613
  };
9614
+ const loadCustomPlugin = async (packageName, version, options) => {
9615
+ const start = Date.now();
9616
+ try {
9617
+ const fetchStart = Date.now();
9618
+ const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, version, {
9619
+ context: options.context,
9620
+ installDeps: false
9621
+ });
9622
+ const fetchDuration = Date.now() - fetchStart;
9623
+ console.log(`[Plugins] [${packageName}] Attempting to import custom plugin from: ${entryPoint}`);
9624
+ if (!existsSync(entryPoint)) {
9625
+ console.error(`[Plugins] [${packageName}] CRITICAL: Plugin entry point not found at ${entryPoint}`);
9626
+ return null;
9627
+ }
9628
+ const importStart = Date.now();
9629
+ const pluginModule = await import(pathToFileURL(entryPoint).href);
9630
+ const importDuration = Date.now() - importStart;
9631
+ const totalDuration = Date.now() - start;
9632
+ console.log(`[Plugins] [${packageName}] Successfully loaded custom plugin from: ${packageDir} (fetch: ${fetchDuration}ms, import: ${importDuration}ms, total: ${totalDuration}ms)`);
9633
+ const plugin = pluginModule.default;
9634
+ await enhancePluginDefinition(plugin, packageDir, packageName, version);
9635
+ return plugin;
9636
+ } catch (e) {
9637
+ console.error(`[Plugins] [${packageName}] CRITICAL: Failed to load after ${Date.now() - start}ms:`, e);
9638
+ return null;
9639
+ }
9640
+ };
9641
+ async function findInstalledPlugins(packagesDir) {
9642
+ const installed = [];
9643
+ if (!existsSync(packagesDir)) return installed;
9644
+ async function scan(dir, depth = 0) {
9645
+ if (depth > 4) return;
9646
+ try {
9647
+ const entries = await readdir(dir, { withFileTypes: true });
9648
+ if (entries.some((e) => e.isFile() && e.name === "package.json")) {
9649
+ try {
9650
+ const content = await readFile(join(dir, "package.json"), "utf8");
9651
+ const pkg = JSON.parse(content);
9652
+ if (pkg.name && pkg.name !== "pnpm") installed.push({
9653
+ name: pkg.name,
9654
+ version: pkg.version || "0.0.0",
9655
+ packageDir: dir,
9656
+ description: pkg.description || ""
9657
+ });
9658
+ } catch (e) {}
9659
+ return;
9660
+ }
9661
+ for (const entry of entries) if (entry.isDirectory() && entry.name !== "node_modules" && !entry.name.startsWith(".")) await scan(join(dir, entry.name), depth + 1);
9662
+ } catch (e) {}
9663
+ }
9664
+ await scan(packagesDir);
9665
+ return installed;
9666
+ }
9371
9667
  const builtInPlugins = async (options) => {
9372
9668
  console.log("[Plugins] Starting background plugin loading...");
9373
9669
  sendStartupProgress("Preparing environment...");
9670
+ const envStart = Date.now();
9374
9671
  await Promise.all([ensureNodeJS(options.context), ensurePNPM(options.context)]);
9672
+ console.log(`[Plugins] Environment preparation took ${Date.now() - envStart}ms`);
9375
9673
  const { usePlugins } = await import("@pipelab/shared");
9376
9674
  const { registerPlugins } = usePlugins();
9377
9675
  const { webSocketServer } = await import("./index.mjs");
9378
- Promise.allSettled(DEFAULT_PLUGIN_IDS.map(async (id) => {
9379
- sendStartupProgress(`Loading plugin: ${id}`);
9380
- const plugin = await loadPipelabPlugin(id, options);
9381
- if (plugin) {
9382
- registerPlugins([plugin]);
9383
- webSocketServer.broadcast("plugin:loaded", { plugin });
9676
+ webSocketServer.broadcast("startup:progress", { type: "ready" });
9677
+ (async () => {
9678
+ const totalStart = Date.now();
9679
+ const pluginsToLoad = /* @__PURE__ */ new Map();
9680
+ for (const name of [
9681
+ "@pipelab/plugin-construct",
9682
+ "@pipelab/plugin-filesystem",
9683
+ "@pipelab/plugin-system",
9684
+ "@pipelab/plugin-steam",
9685
+ "@pipelab/plugin-itch",
9686
+ "@pipelab/plugin-electron",
9687
+ "@pipelab/plugin-discord",
9688
+ "@pipelab/plugin-poki",
9689
+ "@pipelab/plugin-nvpatch",
9690
+ "@pipelab/plugin-tauri",
9691
+ "@pipelab/plugin-minify",
9692
+ "@pipelab/plugin-netlify"
9693
+ ]) pluginsToLoad.set(name, "latest");
9694
+ if (isDev && projectRoot) {
9695
+ const pluginsDir = join(projectRoot, "plugins");
9696
+ if (existsSync(pluginsDir)) try {
9697
+ const entries = await readdir(pluginsDir, { withFileTypes: true });
9698
+ for (const entry of entries) if (entry.isDirectory()) {
9699
+ const pkgPath = join(pluginsDir, entry.name, "package.json");
9700
+ if (existsSync(pkgPath)) try {
9701
+ const pkgContent = await readFile(pkgPath, "utf-8");
9702
+ const pkg = JSON.parse(pkgContent);
9703
+ if (pkg.name && pkg.name.startsWith("@pipelab/plugin-") && pkg.name !== "@pipelab/plugin-core") pluginsToLoad.set(pkg.name, "local");
9704
+ } catch (e) {
9705
+ console.error(`[Plugins] Failed to parse package.json for ${entry.name}:`, e);
9706
+ }
9707
+ }
9708
+ } catch (e) {
9709
+ console.error(`[Plugins] Failed to scan local workspace plugins directory:`, e);
9710
+ }
9711
+ }
9712
+ try {
9713
+ const { setupSettingsConfigFile } = await import("./config-Bi0ORcTK.mjs");
9714
+ const settingsPlugins = (await (await setupSettingsConfigFile(options.context)).getConfig())?.plugins || [];
9715
+ for (const plugin of settingsPlugins) if (plugin.name) if (plugin.enabled) {
9716
+ if (!pluginsToLoad.has(plugin.name)) pluginsToLoad.set(plugin.name, "latest");
9717
+ } else pluginsToLoad.delete(plugin.name);
9718
+ } catch (e) {
9719
+ console.error(`[Plugins] Failed to load settings config on startup:`, e);
9384
9720
  }
9385
- })).then(() => {
9386
- console.log("[Plugins] All default plugins loaded.");
9721
+ console.log(`[Plugins] Total plugins to load on startup:`, Array.from(pluginsToLoad.entries()));
9722
+ const loadPromises = Array.from(pluginsToLoad.entries()).map(async ([packageName, version]) => {
9723
+ sendStartupProgress(`Loading plugin: ${packageName}`);
9724
+ const pluginStart = Date.now();
9725
+ try {
9726
+ const plugin = await loadCustomPlugin(packageName, version, options);
9727
+ if (plugin) {
9728
+ registerPlugins([plugin]);
9729
+ webSocketServer.broadcast("plugin:loaded", { plugin });
9730
+ console.log(`[Plugins] Loaded ${packageName}@${version} in ${Date.now() - pluginStart}ms`);
9731
+ }
9732
+ } catch (err) {
9733
+ console.error(`[Plugins] Failed to load plugin ${packageName} at startup:`, err);
9734
+ }
9735
+ });
9736
+ await Promise.all(loadPromises);
9737
+ console.log(`[Plugins] All startup plugins loaded in ${Date.now() - totalStart}ms.`);
9387
9738
  sendStartupProgress("All plugins loaded.");
9388
9739
  setTimeout(() => {
9389
9740
  webSocketServer.broadcast("startup:progress", { type: "done" });
9390
9741
  }, 2e3);
9391
- });
9392
- return [];
9742
+ })();
9393
9743
  };
9394
9744
  //#endregion
9395
9745
  //#region src/utils.ts
@@ -9398,10 +9748,6 @@ const getFinalPlugins = () => {
9398
9748
  const finalPlugins = [];
9399
9749
  for (const plugin of plugins.value) {
9400
9750
  const finalNodes = [];
9401
- const transformUrl = (url) => {
9402
- if (url.startsWith("file://")) return url.replace("file://", "media://");
9403
- return url;
9404
- };
9405
9751
  const finalIcon = plugin.icon?.type === "image" ? {
9406
9752
  ...plugin.icon,
9407
9753
  image: transformUrl(plugin.icon.image)
@@ -9435,6 +9781,7 @@ const executeGraphWithHistory = async ({ graph, variables, projectName, projectP
9435
9781
  projectName,
9436
9782
  projectPath,
9437
9783
  pipelineId,
9784
+ cachePath,
9438
9785
  startTime,
9439
9786
  status: "running",
9440
9787
  logs: [],
@@ -9449,7 +9796,7 @@ const executeGraphWithHistory = async ({ graph, variables, projectName, projectP
9449
9796
  const shouldDisableHistory = process.env.PIPELAB_DISABLE_HISTORY === "true";
9450
9797
  if (!shouldDisableHistory) await buildHistoryStorage.save(initialEntry);
9451
9798
  const logs = [];
9452
- const sandboxPath = await generateTempFolder(cachePath);
9799
+ const sandboxPath = await ctx.createTempFolder("pipeline-sandbox-");
9453
9800
  const { logger } = useLogger();
9454
9801
  let completedSteps = 0;
9455
9802
  const { registerPlugins, plugins: registeredPlugins } = usePlugins();
@@ -9463,7 +9810,7 @@ const executeGraphWithHistory = async ({ graph, variables, projectName, projectP
9463
9810
  } else logger().error(`[Runner] Failed to load or register plugin "${pluginId}"`);
9464
9811
  }
9465
9812
  logger().info(`[Sandbox] Execution sandbox created at: ${sandboxPath}`);
9466
- await (await setupConfigFile("settings", { context: ctx })).getConfig();
9813
+ await (await setupSettingsConfigFile(ctx)).getConfig();
9467
9814
  try {
9468
9815
  const result = await processGraph({
9469
9816
  graph,
@@ -9531,7 +9878,6 @@ const executeGraphWithHistory = async ({ graph, variables, projectName, projectP
9531
9878
  }, pipelineId);
9532
9879
  throw error;
9533
9880
  } finally {
9534
- if (!shouldDisableHistory) buildHistoryStorage.applyRetentionPolicy();
9535
9881
  try {
9536
9882
  await rm(sandboxPath, {
9537
9883
  recursive: true,
@@ -9540,6 +9886,14 @@ const executeGraphWithHistory = async ({ graph, variables, projectName, projectP
9540
9886
  } catch (e) {
9541
9887
  console.warn(`Failed to cleanup sandbox at ${sandboxPath}:`, e);
9542
9888
  }
9889
+ try {
9890
+ await rm(cachePath, {
9891
+ recursive: true,
9892
+ force: true
9893
+ });
9894
+ } catch (e) {
9895
+ console.warn(`Failed to cleanup cache at ${cachePath}:`, e);
9896
+ }
9543
9897
  }
9544
9898
  };
9545
9899
  //#endregion
@@ -9547,7 +9901,7 @@ const executeGraphWithHistory = async ({ graph, variables, projectName, projectP
9547
9901
  const c3toSteamPreset = async () => {
9548
9902
  return {
9549
9903
  data: {
9550
- version: "3.0.0",
9904
+ version: "5.0.0",
9551
9905
  name: "Construct 3 to Steam",
9552
9906
  description: "A basic project to get you started with Construct 3 and Steam",
9553
9907
  variables: [],
@@ -9555,8 +9909,9 @@ const c3toSteamPreset = async () => {
9555
9909
  triggers: [{
9556
9910
  type: "event",
9557
9911
  origin: {
9558
- pluginId: "system",
9559
- nodeId: "manual"
9912
+ pluginId: "@pipelab/plugin-system",
9913
+ nodeId: "manual",
9914
+ version: "latest"
9560
9915
  },
9561
9916
  uid: "manual-start",
9562
9917
  params: {}
@@ -9567,7 +9922,8 @@ const c3toSteamPreset = async () => {
9567
9922
  type: "action",
9568
9923
  origin: {
9569
9924
  nodeId: "export-construct-project",
9570
- pluginId: "construct"
9925
+ pluginId: "@pipelab/plugin-construct",
9926
+ version: "latest"
9571
9927
  },
9572
9928
  params: {
9573
9929
  file: {
@@ -9605,7 +9961,8 @@ const c3toSteamPreset = async () => {
9605
9961
  type: "action",
9606
9962
  origin: {
9607
9963
  nodeId: "unzip-file-node",
9608
- pluginId: "filesystem"
9964
+ pluginId: "@pipelab/plugin-filesystem",
9965
+ version: "latest"
9609
9966
  },
9610
9967
  params: { file: {
9611
9968
  editor: "editor",
@@ -9617,7 +9974,8 @@ const c3toSteamPreset = async () => {
9617
9974
  type: "action",
9618
9975
  origin: {
9619
9976
  nodeId: "electron:package:v2",
9620
- pluginId: "electron"
9977
+ pluginId: "@pipelab/plugin-electron",
9978
+ version: "latest"
9621
9979
  },
9622
9980
  params: {
9623
9981
  arch: {
@@ -9747,7 +10105,8 @@ const c3toSteamPreset = async () => {
9747
10105
  type: "action",
9748
10106
  origin: {
9749
10107
  nodeId: "steam-upload",
9750
- pluginId: "steam"
10108
+ pluginId: "@pipelab/plugin-steam",
10109
+ version: "latest"
9751
10110
  },
9752
10111
  params: {
9753
10112
  sdk: {
@@ -9790,7 +10149,8 @@ const c3toSteamPreset = async () => {
9790
10149
  type: "action",
9791
10150
  origin: {
9792
10151
  nodeId: "fs:open-in-explorer",
9793
- pluginId: "filesystem"
10152
+ pluginId: "@pipelab/plugin-filesystem",
10153
+ version: "latest"
9794
10154
  },
9795
10155
  params: { path: {
9796
10156
  editor: "editor",
@@ -9809,7 +10169,7 @@ const c3toSteamPreset = async () => {
9809
10169
  const newProjectPreset = async () => {
9810
10170
  return {
9811
10171
  data: {
9812
- version: "3.0.0",
10172
+ version: "5.0.0",
9813
10173
  name: "Empty project",
9814
10174
  description: "A default project with no tasks added",
9815
10175
  variables: [],
@@ -9817,8 +10177,9 @@ const newProjectPreset = async () => {
9817
10177
  triggers: [{
9818
10178
  type: "event",
9819
10179
  origin: {
9820
- pluginId: "system",
9821
- nodeId: "manual"
10180
+ pluginId: "@pipelab/plugin-system",
10181
+ nodeId: "manual",
10182
+ version: "latest"
9822
10183
  },
9823
10184
  uid: "manual-start",
9824
10185
  params: {}
@@ -9834,7 +10195,7 @@ const newProjectPreset = async () => {
9834
10195
  const moreToCome = async () => {
9835
10196
  return {
9836
10197
  data: {
9837
- version: "3.0.0",
10198
+ version: "5.0.0",
9838
10199
  name: "More to come!",
9839
10200
  description: "Do not hesitate to suggest templates you would see here",
9840
10201
  variables: [],
@@ -9842,8 +10203,9 @@ const moreToCome = async () => {
9842
10203
  triggers: [{
9843
10204
  type: "event",
9844
10205
  origin: {
9845
- pluginId: "system",
9846
- nodeId: "manual"
10206
+ pluginId: "@pipelab/plugin-system",
10207
+ nodeId: "manual",
10208
+ version: "latest"
9847
10209
  },
9848
10210
  uid: "manual-start",
9849
10211
  params: {}
@@ -9887,10 +10249,6 @@ const registerEngineHandlers = (context) => {
9887
10249
  }
9888
10250
  });
9889
10251
  });
9890
- handle("condition:execute", async (_, { value }) => {
9891
- const { nodeId, params, pluginId } = value;
9892
- await handleConditionExecute(nodeId, pluginId, params, await generateTempFolder(tmpdir()), context);
9893
- });
9894
10252
  let abortControllerGraph = void 0;
9895
10253
  const effectiveActionExecute = async (nodeId, pluginId, params, mainWindow, send, cwd, cachePath) => {
9896
10254
  try {
@@ -9911,23 +10269,32 @@ const registerEngineHandlers = (context) => {
9911
10269
  };
9912
10270
  handle("action:execute", async (event, { send, value }) => {
9913
10271
  const { nodeId, params, pluginId } = value;
9914
- const cachePath = (await (await setupConfigFile("settings", { context })).getConfig())?.cacheFolder || tmpdir();
9915
- const cwd = await generateTempFolder(cachePath);
9916
- const mainWindow = void 0;
9917
- abortControllerGraph = new AbortController();
9918
- const signalPromise = new Promise((resolve, reject) => {
9919
- abortControllerGraph.signal.addEventListener("abort", async () => {
9920
- await send({
9921
- type: "end",
9922
- data: {
9923
- ipcError: "Action aborted",
9924
- type: "error"
9925
- }
10272
+ const cachePath = context.getCachePath(CacheFolder.Actions, pluginId, nodeId);
10273
+ const cwd = await context.createTempFolder("action-execute-");
10274
+ try {
10275
+ const mainWindow = void 0;
10276
+ abortControllerGraph = new AbortController();
10277
+ const signalPromise = new Promise((resolve, reject) => {
10278
+ abortControllerGraph.signal.addEventListener("abort", async () => {
10279
+ await send({
10280
+ type: "end",
10281
+ data: {
10282
+ ipcError: "Action aborted",
10283
+ type: "error"
10284
+ }
10285
+ });
10286
+ return reject(/* @__PURE__ */ new Error("Action interrupted"));
9926
10287
  });
9927
- return reject(/* @__PURE__ */ new Error("Action interrupted"));
9928
10288
  });
9929
- });
9930
- await Promise.race([signalPromise, effectiveActionExecute(nodeId, pluginId, params, mainWindow, send, cwd, cachePath)]);
10289
+ await Promise.race([signalPromise, effectiveActionExecute(nodeId, pluginId, params, mainWindow, send, cwd, cachePath)]);
10290
+ } finally {
10291
+ await rm(cwd, {
10292
+ recursive: true,
10293
+ force: true
10294
+ }).catch((err) => {
10295
+ console.warn(`Failed to cleanup temp folder at ${cwd}:`, err);
10296
+ });
10297
+ }
9931
10298
  });
9932
10299
  handle("constants:get", async (_, { send }) => {
9933
10300
  const userData = context.userDataPath;
@@ -9951,11 +10318,11 @@ const registerEngineHandlers = (context) => {
9951
10318
  });
9952
10319
  handle("graph:execute", async (event, { send, value }) => {
9953
10320
  const { graph, variables, projectName, projectPath, pipelineId } = value;
9954
- const config = await (await setupConfigFile("settings", { context })).getConfig();
10321
+ await (await setupSettingsConfigFile(context)).getConfig();
9955
10322
  const effectiveProjectName = projectName || "Unnamed Project";
9956
10323
  const effectiveProjectPath = projectPath || "";
9957
10324
  const effectivePipelineId = pipelineId || "unknown";
9958
- const effectiveCachePath = config?.cacheFolder || tmpdir();
10325
+ const effectiveCachePath = context.getCachePath(CacheFolder.Pipelines, effectivePipelineId);
9959
10326
  const mainWindow = void 0;
9960
10327
  abortControllerGraph = new AbortController();
9961
10328
  try {
@@ -10063,7 +10430,7 @@ var JsonFileStorage = class {
10063
10430
  filePath;
10064
10431
  logger = useLogger().logger;
10065
10432
  constructor(fileName, context) {
10066
- this.filePath = path.join(context.userDataPath, fileName);
10433
+ this.filePath = context.getConfigPath(fileName);
10067
10434
  const dir = path.dirname(this.filePath);
10068
10435
  if (!fs.existsSync(dir)) try {
10069
10436
  fs.mkdirSync(dir, { recursive: true });
@@ -10262,16 +10629,730 @@ const registerAuthHandlers = (context) => {
10262
10629
  const registerSystemHandlers = (options) => {
10263
10630
  const { handle } = useAPI();
10264
10631
  handle("agent:version:get", async (_, { send }) => {
10632
+ const isStable = options.context.userDataPath.endsWith("app") || !options.context.userDataPath.includes("app-beta");
10633
+ send({
10634
+ type: "end",
10635
+ data: {
10636
+ type: "success",
10637
+ result: {
10638
+ version: isDev ? "workspace" : options.version,
10639
+ channel: isDev ? "dev" : isStable ? "stable" : "beta"
10640
+ }
10641
+ }
10642
+ });
10643
+ });
10644
+ };
10645
+ //#endregion
10646
+ //#region src/handlers/plugins.ts
10647
+ const localPluginsMap = /* @__PURE__ */ new Map();
10648
+ if (isDev && projectRoot) {
10649
+ const pluginsDir = join(projectRoot, "plugins");
10650
+ if (existsSync(pluginsDir)) try {
10651
+ const entries = readdirSync(pluginsDir, { withFileTypes: true });
10652
+ for (const entry of entries) if (entry.isDirectory()) {
10653
+ const pkgPath = join(pluginsDir, entry.name, "package.json");
10654
+ if (existsSync(pkgPath)) try {
10655
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
10656
+ if (pkg.name) localPluginsMap.set(pkg.name, join(pluginsDir, entry.name));
10657
+ } catch (e) {
10658
+ console.error(`[Plugins] Failed to parse package.json for ${entry.name}:`, e);
10659
+ }
10660
+ }
10661
+ } catch (e) {
10662
+ console.error(`[Plugins] Failed to scan local workspace plugins directory:`, e);
10663
+ }
10664
+ }
10665
+ /**
10666
+ * Resolves a requested plugin version.
10667
+ * If the plugin is present in the local workspace map during development,
10668
+ * its version is overridden to "local" so Pipelab loads it from the source files.
10669
+ */
10670
+ function resolvePluginVersion(packageName, requestedVersion) {
10671
+ if (isDev && projectRoot && process.env.PIPELAB_FORCE_NPM !== "true" && localPluginsMap.has(packageName)) return "local";
10672
+ return requestedVersion || "latest";
10673
+ }
10674
+ const registerPluginsHandlers = (context) => {
10675
+ const { handle } = useAPI();
10676
+ handle("plugin:search", async (_, { send, value }) => {
10677
+ try {
10678
+ const query = value.query || "";
10679
+ const text = query ? `${query} keywords:pipelab-plugin` : "keywords:pipelab-plugin";
10680
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(text)}&size=50`;
10681
+ const response = await fetch(url);
10682
+ if (!response.ok) throw new Error(`NPM registry search failed with status ${response.status}`);
10683
+ send({
10684
+ type: "end",
10685
+ data: {
10686
+ type: "success",
10687
+ result: { results: ((await response.json()).objects || []).map((obj) => ({
10688
+ name: obj.package.name,
10689
+ version: obj.package.version,
10690
+ description: obj.package.description,
10691
+ keywords: obj.package.keywords,
10692
+ date: obj.package.date
10693
+ })) }
10694
+ }
10695
+ });
10696
+ } catch (e) {
10697
+ send({
10698
+ type: "end",
10699
+ data: {
10700
+ type: "error",
10701
+ ipcError: e.message || "Failed to search npm registry"
10702
+ }
10703
+ });
10704
+ }
10705
+ });
10706
+ handle("plugin:get-details", async (_, { send, value }) => {
10707
+ try {
10708
+ const { packageName } = value;
10709
+ const packument = await pacote.packument(packageName, { fullMetadata: true });
10710
+ const latestVersion = packument["dist-tags"]?.latest || Object.keys(packument.versions).pop() || "0.0.0";
10711
+ const latestPkg = packument.versions[latestVersion];
10712
+ send({
10713
+ type: "end",
10714
+ data: {
10715
+ type: "success",
10716
+ result: {
10717
+ name: packument.name,
10718
+ latestVersion,
10719
+ versions: Object.keys(packument.versions).reverse(),
10720
+ description: latestPkg?.description
10721
+ }
10722
+ }
10723
+ });
10724
+ } catch (e) {
10725
+ send({
10726
+ type: "end",
10727
+ data: {
10728
+ type: "error",
10729
+ ipcError: e.message || "Failed to get plugin details"
10730
+ }
10731
+ });
10732
+ }
10733
+ });
10734
+ handle("plugin:install", async (_, { send, value }) => {
10735
+ try {
10736
+ const { packageName, version } = value;
10737
+ const mappedVersion = resolvePluginVersion(packageName, version);
10738
+ console.log(`[Plugins] Installing ${packageName}@${mappedVersion}...`);
10739
+ const { packageDir } = await fetchPipelabPlugin(packageName, mappedVersion, {
10740
+ context,
10741
+ installDeps: false
10742
+ });
10743
+ const plugin = await loadCustomPlugin(packageName, mappedVersion, { context });
10744
+ if (!plugin) throw new Error("Failed to load installed plugin module.");
10745
+ const { registerPlugins } = usePlugins();
10746
+ registerPlugins([plugin]);
10747
+ webSocketServer.broadcast("plugin:loaded", { plugin });
10748
+ send({
10749
+ type: "end",
10750
+ data: {
10751
+ type: "success",
10752
+ result: { result: "ok" }
10753
+ }
10754
+ });
10755
+ } catch (e) {
10756
+ console.error(`[Plugins] Installation failed for ${value.packageName}:`, e);
10757
+ send({
10758
+ type: "end",
10759
+ data: {
10760
+ type: "error",
10761
+ ipcError: e.message || "Failed to install plugin"
10762
+ }
10763
+ });
10764
+ }
10765
+ });
10766
+ handle("plugin:uninstall", async (_, { send, value }) => {
10767
+ try {
10768
+ const { packageName } = value;
10769
+ console.log(`[Plugins] Uninstalling plugin ${packageName}...`);
10770
+ const targetDir = context.getPackagesPath(packageName);
10771
+ if (existsSync(targetDir)) await rm(targetDir, {
10772
+ recursive: true,
10773
+ force: true
10774
+ });
10775
+ send({
10776
+ type: "end",
10777
+ data: {
10778
+ type: "success",
10779
+ result: { result: "ok" }
10780
+ }
10781
+ });
10782
+ } catch (e) {
10783
+ send({
10784
+ type: "end",
10785
+ data: {
10786
+ type: "error",
10787
+ ipcError: e.message || "Failed to uninstall plugin"
10788
+ }
10789
+ });
10790
+ }
10791
+ });
10792
+ handle("plugin:list-installed", async (_, { send }) => {
10793
+ try {
10794
+ const rawInstalled = await findInstalledPlugins(context.getPackagesPath());
10795
+ const DEFAULT_PLUGIN_IDS = [
10796
+ "construct",
10797
+ "filesystem",
10798
+ "system",
10799
+ "steam",
10800
+ "itch",
10801
+ "electron",
10802
+ "discord",
10803
+ "poki",
10804
+ "nvpatch",
10805
+ "tauri",
10806
+ "minify",
10807
+ "netlify"
10808
+ ];
10809
+ send({
10810
+ type: "end",
10811
+ data: {
10812
+ type: "success",
10813
+ result: { installed: rawInstalled.filter((item) => {
10814
+ return !DEFAULT_PLUGIN_IDS.some((id) => item.name === `@pipelab/plugin-${id}`);
10815
+ }).map((item) => ({
10816
+ name: item.name,
10817
+ version: item.version,
10818
+ description: item.description
10819
+ })) }
10820
+ }
10821
+ });
10822
+ } catch (e) {
10823
+ send({
10824
+ type: "end",
10825
+ data: {
10826
+ type: "error",
10827
+ ipcError: e.message || "Failed to list installed plugins"
10828
+ }
10829
+ });
10830
+ }
10831
+ });
10832
+ handle("plugin:ensure-loaded", async (_, { send, value }) => {
10833
+ const { plugins } = value;
10834
+ const { plugins: registeredPlugins, registerPlugins } = usePlugins();
10835
+ const loaded = [];
10836
+ const failed = [];
10837
+ const pluginsToEnsure = /* @__PURE__ */ new Set();
10838
+ if (plugins && typeof plugins === "object") {
10839
+ for (const id of Object.keys(plugins)) if (id) pluginsToEnsure.add(id);
10840
+ }
10841
+ for (const packageName of pluginsToEnsure) {
10842
+ const mappedVersion = resolvePluginVersion(packageName);
10843
+ if (registeredPlugins.value.some((p) => {
10844
+ if (p.packageName !== packageName) return false;
10845
+ if (mappedVersion === "latest") return true;
10846
+ return p.version === mappedVersion;
10847
+ })) continue;
10848
+ try {
10849
+ console.log(`[Plugins] JIT loading plugin "${packageName}@${mappedVersion}" for pipeline...`);
10850
+ const plugin = await loadCustomPlugin(packageName, mappedVersion, { context });
10851
+ if (plugin) {
10852
+ registerPlugins([plugin]);
10853
+ webSocketServer.broadcast("plugin:loaded", { plugin });
10854
+ loaded.push(packageName);
10855
+ console.log(`[Plugins] JIT loaded "${packageName}" successfully.`);
10856
+ } else {
10857
+ console.warn(`[Plugins] JIT load for "${packageName}" returned no plugin.`);
10858
+ failed.push(packageName);
10859
+ }
10860
+ } catch (e) {
10861
+ console.error(`[Plugins] JIT load failed for "${packageName}":`, e);
10862
+ failed.push(packageName);
10863
+ }
10864
+ }
10265
10865
  send({
10266
10866
  type: "end",
10267
10867
  data: {
10268
10868
  type: "success",
10269
- result: { version: options.version }
10869
+ result: {
10870
+ loaded,
10871
+ failed
10872
+ }
10270
10873
  }
10271
10874
  });
10272
10875
  });
10273
10876
  };
10274
10877
  //#endregion
10878
+ //#region src/handlers/migration.ts
10879
+ const registerMigrationHandlers = (context) => {
10880
+ const { handle } = useAPI();
10881
+ const { logger } = useLogger();
10882
+ const getFileMetadata = async (filePath) => {
10883
+ const exists = existsSync(filePath);
10884
+ let mtime = Date.now();
10885
+ let version = null;
10886
+ if (exists) {
10887
+ try {
10888
+ const stat = await fs$1.stat(filePath);
10889
+ mtime = stat.mtime ? stat.mtime.getTime() : Date.now();
10890
+ } catch (e) {
10891
+ mtime = Date.now();
10892
+ }
10893
+ try {
10894
+ const content = await fs$1.readFile(filePath, "utf8");
10895
+ const json = JSON.parse(content);
10896
+ version = json && typeof json.version === "string" ? json.version : null;
10897
+ } catch (e) {}
10898
+ } else mtime = Date.now();
10899
+ return {
10900
+ exists,
10901
+ mtime,
10902
+ version
10903
+ };
10904
+ };
10905
+ const isVersionImportable = (sourceVer, targetVer) => {
10906
+ if (!sourceVer) return false;
10907
+ if (!targetVer) return true;
10908
+ try {
10909
+ const coercedSource = semver.coerce(sourceVer);
10910
+ const coercedTarget = semver.coerce(targetVer);
10911
+ if (!coercedSource) return false;
10912
+ if (!coercedTarget) return true;
10913
+ return semver.lte(coercedSource, coercedTarget);
10914
+ } catch {
10915
+ return sourceVer <= targetVer;
10916
+ }
10917
+ };
10918
+ handle("migration:scan-stable", async (_, { send, value }) => {
10919
+ logger().info("[Migration] Scanning other channel database...");
10920
+ try {
10921
+ let sourceEnv = "prod";
10922
+ if (value?.sourceChannel === "stable") sourceEnv = "prod";
10923
+ else if (value?.sourceChannel === "beta") sourceEnv = "beta";
10924
+ else sourceEnv = context.userDataPath.endsWith("app") || !context.userDataPath.includes("app-beta") ? "beta" : "prod";
10925
+ const sourcePath = getDefaultUserDataPath(sourceEnv);
10926
+ if (sourcePath === context.userDataPath) {
10927
+ logger().info("[Migration] Running in same channel. Disabling migration scan.");
10928
+ return send({
10929
+ type: "end",
10930
+ data: {
10931
+ type: "success",
10932
+ result: {
10933
+ sourceChannel: sourceEnv === "prod" ? "Stable" : "Beta",
10934
+ targetChannel: isDev ? "Dev" : context.userDataPath.endsWith("app") || !context.userDataPath.includes("app-beta") ? "Stable" : "Beta",
10935
+ settingsExists: false,
10936
+ settingsVersion: null,
10937
+ settingsVersionTarget: null,
10938
+ settingsMtimeSource: Date.now(),
10939
+ settingsMtimeTarget: Date.now(),
10940
+ settingsImportable: false,
10941
+ connectionsExists: false,
10942
+ connectionsCount: 0,
10943
+ connectionsVersion: null,
10944
+ connectionsVersionTarget: null,
10945
+ connectionsMtimeSource: Date.now(),
10946
+ connectionsMtimeTarget: Date.now(),
10947
+ connectionsImportable: false,
10948
+ projectsExists: false,
10949
+ projectsVersion: null,
10950
+ projectsVersionTarget: null,
10951
+ projectsMtimeSource: Date.now(),
10952
+ projectsMtimeTarget: Date.now(),
10953
+ projectsImportable: false,
10954
+ projects: []
10955
+ }
10956
+ }
10957
+ });
10958
+ }
10959
+ const sourceContext = new PipelabContext({
10960
+ userDataPath: sourcePath,
10961
+ releaseTag: sourceEnv
10962
+ });
10963
+ const sourceSettingsPath = sourceContext.getSettingsPath();
10964
+ const targetSettingsPath = context.getSettingsPath();
10965
+ const sourceConnectionsPath = sourceContext.getConnectionsPath();
10966
+ const targetConnectionsPath = context.getConnectionsPath();
10967
+ const sourceProjectsPath = sourceContext.getProjectsPath();
10968
+ const targetProjectsPath = context.getProjectsPath();
10969
+ const sourceSettingsMeta = await getFileMetadata(sourceSettingsPath);
10970
+ const targetSettingsMeta = await getFileMetadata(targetSettingsPath);
10971
+ const sourceConnectionsMeta = await getFileMetadata(sourceConnectionsPath);
10972
+ const targetConnectionsMeta = await getFileMetadata(targetConnectionsPath);
10973
+ const sourceProjectsMeta = await getFileMetadata(sourceProjectsPath);
10974
+ const targetProjectsMeta = await getFileMetadata(targetProjectsPath);
10975
+ const settingsExists = sourceSettingsMeta.exists;
10976
+ const settingsVersion = sourceSettingsMeta.version;
10977
+ const settingsMtimeSource = sourceSettingsMeta.mtime;
10978
+ const settingsMtimeTarget = targetSettingsMeta.mtime;
10979
+ const settingsImportable = isVersionImportable(settingsVersion, targetSettingsMeta.version);
10980
+ const connectionsExists = sourceConnectionsMeta.exists;
10981
+ const connectionsVersion = sourceConnectionsMeta.version;
10982
+ const connectionsMtimeSource = sourceConnectionsMeta.mtime;
10983
+ const connectionsMtimeTarget = targetConnectionsMeta.mtime;
10984
+ const connectionsImportable = isVersionImportable(connectionsVersion, targetConnectionsMeta.version);
10985
+ const projectsExists = sourceProjectsMeta.exists;
10986
+ const projectsVersion = sourceProjectsMeta.version;
10987
+ const projectsMtimeSource = sourceProjectsMeta.mtime;
10988
+ const projectsMtimeTarget = targetProjectsMeta.mtime;
10989
+ const projectsImportable = isVersionImportable(projectsVersion, targetProjectsMeta.version);
10990
+ let connectionsCount = 0;
10991
+ try {
10992
+ if (sourceConnectionsMeta.exists) {
10993
+ const connContent = await fs$1.readFile(sourceConnectionsPath, "utf8");
10994
+ const connJson = JSON.parse(connContent);
10995
+ if (connJson && Array.isArray(connJson.connections)) connectionsCount = connJson.connections.length;
10996
+ }
10997
+ } catch (err) {
10998
+ logger().error("[Migration] Error parsing source connections.json:", err);
10999
+ }
11000
+ let sourceProjectsList = [];
11001
+ let sourcePipelinesList = [];
11002
+ try {
11003
+ if (sourceProjectsMeta.exists) {
11004
+ const projContent = await fs$1.readFile(sourceProjectsPath, "utf8");
11005
+ const projJson = JSON.parse(projContent);
11006
+ sourceProjectsList = projJson.projects || [];
11007
+ sourcePipelinesList = projJson.pipelines || [];
11008
+ }
11009
+ } catch (err) {
11010
+ logger().error("[Migration] Error parsing source projects.json:", err);
11011
+ }
11012
+ let targetPipelinesList = [];
11013
+ try {
11014
+ if (targetProjectsMeta.exists) {
11015
+ const targetProjContent = await fs$1.readFile(targetProjectsPath, "utf8");
11016
+ targetPipelinesList = JSON.parse(targetProjContent).pipelines || [];
11017
+ }
11018
+ } catch (err) {}
11019
+ const projectsReport = [];
11020
+ for (const proj of sourceProjectsList) {
11021
+ const projPipelines = sourcePipelinesList.filter((p) => p.project === proj.id);
11022
+ const pipelinesReport = [];
11023
+ for (const pipe of projPipelines) {
11024
+ const targetPipe = targetPipelinesList.find((bp) => bp.id === pipe.id);
11025
+ const existsInBeta = !!targetPipe;
11026
+ let pipeName = pipe.id;
11027
+ let pipeDesc = "";
11028
+ if (pipe.type === "internal") {
11029
+ const sourcePipeFile = sourceContext.getConfigPath(`${pipe.configName}.json`);
11030
+ try {
11031
+ if (existsSync(sourcePipeFile)) {
11032
+ const pipeContent = await fs$1.readFile(sourcePipeFile, "utf8");
11033
+ const rawJson = JSON.parse(pipeContent);
11034
+ const pipeJson = await savedFileMigrator.migrate(rawJson);
11035
+ pipeName = pipeJson.name || pipeName;
11036
+ pipeDesc = pipeJson.description || pipeDesc;
11037
+ }
11038
+ } catch (err) {
11039
+ logger().error(`[Migration] Error reading source pipeline file ${sourcePipeFile}:`, err);
11040
+ }
11041
+ } else if (pipe.type === "external") {
11042
+ pipeName = pipe.summary?.name || pipeName;
11043
+ pipeDesc = pipe.summary?.description || pipeDesc;
11044
+ }
11045
+ pipelinesReport.push({
11046
+ id: pipe.id,
11047
+ name: pipeName,
11048
+ description: pipeDesc,
11049
+ type: pipe.type,
11050
+ lastModifiedStable: pipe.type !== "pipelab-cloud" ? pipe.lastModified : void 0,
11051
+ lastModifiedBeta: targetPipe && targetPipe.type !== "pipelab-cloud" ? targetPipe.lastModified : void 0,
11052
+ existsInBeta
11053
+ });
11054
+ }
11055
+ projectsReport.push({
11056
+ id: proj.id,
11057
+ name: proj.name,
11058
+ description: proj.description || "",
11059
+ pipelines: pipelinesReport
11060
+ });
11061
+ }
11062
+ send({
11063
+ type: "end",
11064
+ data: {
11065
+ type: "success",
11066
+ result: {
11067
+ sourceChannel: sourceEnv === "prod" ? "Stable" : "Beta",
11068
+ targetChannel: isDev ? "Dev" : context.userDataPath.endsWith("app") || !context.userDataPath.includes("app-beta") ? "Stable" : "Beta",
11069
+ settingsExists,
11070
+ settingsVersion,
11071
+ settingsVersionTarget: targetSettingsMeta.version,
11072
+ settingsMtimeSource,
11073
+ settingsMtimeTarget,
11074
+ settingsImportable,
11075
+ connectionsExists,
11076
+ connectionsCount,
11077
+ connectionsVersion,
11078
+ connectionsVersionTarget: targetConnectionsMeta.version,
11079
+ connectionsMtimeSource,
11080
+ connectionsMtimeTarget,
11081
+ connectionsImportable,
11082
+ projectsExists,
11083
+ projectsVersion,
11084
+ projectsVersionTarget: targetProjectsMeta.version,
11085
+ projectsMtimeSource,
11086
+ projectsMtimeTarget,
11087
+ projectsImportable,
11088
+ projects: projectsReport
11089
+ }
11090
+ }
11091
+ });
11092
+ } catch (e) {
11093
+ logger().error("[Migration] Error scanning other channel folder:", e);
11094
+ send({
11095
+ type: "end",
11096
+ data: {
11097
+ type: "error",
11098
+ ipcError: e instanceof Error ? e.message : "Failed to scan other channel database"
11099
+ }
11100
+ });
11101
+ }
11102
+ });
11103
+ handle("migration:perform", async (_, { send, value }) => {
11104
+ logger().info("[Migration] Performing migration...");
11105
+ try {
11106
+ const { migrateSettings, migrateConnections, selectedProjects, selectedPipelines, sourceChannel } = value;
11107
+ let sourceEnv = "prod";
11108
+ if (sourceChannel === "stable") sourceEnv = "prod";
11109
+ else if (sourceChannel === "beta") sourceEnv = "beta";
11110
+ else sourceEnv = context.userDataPath.endsWith("app") || !context.userDataPath.includes("app-beta") ? "beta" : "prod";
11111
+ const sourcePath = getDefaultUserDataPath(sourceEnv);
11112
+ const sourceContext = new PipelabContext({
11113
+ userDataPath: sourcePath,
11114
+ releaseTag: sourceEnv
11115
+ });
11116
+ sourceContext.getConfigPath();
11117
+ const targetConfigDir = context.getConfigPath();
11118
+ if (sourcePath === context.userDataPath) throw new Error("Cannot migrate data: Source and target directories are identical.");
11119
+ if (existsSync(targetConfigDir)) {
11120
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11121
+ const backupDir = context.getConfigPath("backups", `backup_${timestamp}`);
11122
+ await fs$1.mkdir(backupDir, { recursive: true });
11123
+ const entries = await fs$1.readdir(targetConfigDir, { withFileTypes: true });
11124
+ for (const entry of entries) {
11125
+ if (entry.name === "backups") continue;
11126
+ const srcPath = join(targetConfigDir, entry.name);
11127
+ const destPath = join(backupDir, entry.name);
11128
+ try {
11129
+ await fs$1.cp(srcPath, destPath, { recursive: true });
11130
+ } catch (copyErr) {
11131
+ logger().error(`[Migration] Failed to backup ${entry.name}:`, copyErr);
11132
+ }
11133
+ }
11134
+ logger().info(`[Migration] Config backup successfully created at ${backupDir}`);
11135
+ }
11136
+ if (migrateSettings) {
11137
+ const sourceSettingsFile = sourceContext.getSettingsPath();
11138
+ const targetSettingsFile = context.getSettingsPath();
11139
+ const sourceMeta = await getFileMetadata(sourceSettingsFile);
11140
+ const targetMeta = await getFileMetadata(targetSettingsFile);
11141
+ if (!isVersionImportable(sourceMeta.version, targetMeta.version)) throw new Error("Cannot migrate settings: Source version is newer than current version.");
11142
+ if (existsSync(sourceSettingsFile)) {
11143
+ let stableSettings = {
11144
+ version: "7.0.0",
11145
+ locale: "en-US",
11146
+ theme: "light",
11147
+ autosave: true,
11148
+ agents: [],
11149
+ plugins: [],
11150
+ tours: {
11151
+ dashboard: {
11152
+ step: 0,
11153
+ completed: false
11154
+ },
11155
+ editor: {
11156
+ step: 0,
11157
+ completed: false
11158
+ }
11159
+ }
11160
+ };
11161
+ try {
11162
+ const stableContent = await fs$1.readFile(sourceSettingsFile, "utf8");
11163
+ stableSettings = JSON.parse(stableContent);
11164
+ } catch (e) {
11165
+ logger().error("[Migration] Failed to read source settings.json:", e);
11166
+ }
11167
+ let betaSettings = {
11168
+ version: "7.0.0",
11169
+ locale: "en-US",
11170
+ theme: "light",
11171
+ autosave: true,
11172
+ agents: [],
11173
+ plugins: [],
11174
+ tours: {
11175
+ dashboard: {
11176
+ step: 0,
11177
+ completed: false
11178
+ },
11179
+ editor: {
11180
+ step: 0,
11181
+ completed: false
11182
+ }
11183
+ }
11184
+ };
11185
+ if (existsSync(targetSettingsFile)) try {
11186
+ const betaContent = await fs$1.readFile(targetSettingsFile, "utf8");
11187
+ betaSettings = JSON.parse(betaContent);
11188
+ } catch (e) {}
11189
+ betaSettings.theme = stableSettings.theme ?? betaSettings.theme;
11190
+ betaSettings.locale = stableSettings.locale ?? betaSettings.locale;
11191
+ betaSettings.autosave = stableSettings.autosave ?? betaSettings.autosave;
11192
+ if (stableSettings.tours) {
11193
+ betaSettings.tours = betaSettings.tours || {
11194
+ dashboard: {
11195
+ step: 0,
11196
+ completed: false
11197
+ },
11198
+ editor: {
11199
+ step: 0,
11200
+ completed: false
11201
+ }
11202
+ };
11203
+ if (stableSettings.tours.dashboard) betaSettings.tours.dashboard = stableSettings.tours.dashboard;
11204
+ if (stableSettings.tours.editor) betaSettings.tours.editor = stableSettings.tours.editor;
11205
+ }
11206
+ const betaAgents = betaSettings.agents || [];
11207
+ const stableAgents = stableSettings.agents || [];
11208
+ for (const sAgent of stableAgents) {
11209
+ const existingIdx = betaAgents.findIndex((a) => a.id === sAgent.id);
11210
+ if (existingIdx >= 0) betaAgents[existingIdx] = { ...sAgent };
11211
+ else betaAgents.push({ ...sAgent });
11212
+ }
11213
+ betaSettings.agents = betaAgents;
11214
+ const betaPlugins = betaSettings.plugins || [];
11215
+ const stablePlugins = stableSettings.plugins || [];
11216
+ for (const sPlugin of stablePlugins) {
11217
+ const existingIdx = betaPlugins.findIndex((p) => p.name === sPlugin.name);
11218
+ if (existingIdx >= 0) betaPlugins[existingIdx] = { ...sPlugin };
11219
+ else betaPlugins.push({ ...sPlugin });
11220
+ }
11221
+ betaSettings.plugins = betaPlugins;
11222
+ await fs$1.mkdir(dirname(targetSettingsFile), { recursive: true });
11223
+ await fs$1.writeFile(targetSettingsFile, JSON.stringify(betaSettings, null, 2));
11224
+ logger().info("[Migration] Settings merged successfully");
11225
+ }
11226
+ }
11227
+ if (migrateConnections) {
11228
+ const sourceConnectionsFile = sourceContext.getConnectionsPath();
11229
+ const targetConnectionsFile = context.getConnectionsPath();
11230
+ const sourceMeta = await getFileMetadata(sourceConnectionsFile);
11231
+ const targetMeta = await getFileMetadata(targetConnectionsFile);
11232
+ if (!isVersionImportable(sourceMeta.version, targetMeta.version)) throw new Error("Cannot migrate connections: Source version is newer than current version.");
11233
+ if (existsSync(sourceConnectionsFile)) {
11234
+ let stableConnections = {
11235
+ version: "1.0.0",
11236
+ connections: []
11237
+ };
11238
+ try {
11239
+ const stableContent = await fs$1.readFile(sourceConnectionsFile, "utf8");
11240
+ stableConnections = JSON.parse(stableContent);
11241
+ } catch (e) {
11242
+ logger().error("[Migration] Failed to read source connections.json:", e);
11243
+ }
11244
+ let betaConnections = {
11245
+ version: "1.0.0",
11246
+ connections: []
11247
+ };
11248
+ if (existsSync(targetConnectionsFile)) try {
11249
+ const betaContent = await fs$1.readFile(targetConnectionsFile, "utf8");
11250
+ betaConnections = JSON.parse(betaContent);
11251
+ } catch (e) {}
11252
+ betaConnections.connections = betaConnections.connections || [];
11253
+ stableConnections.connections = stableConnections.connections || [];
11254
+ for (const sConn of stableConnections.connections) {
11255
+ const existingIdx = betaConnections.connections.findIndex((c) => c.id === sConn.id);
11256
+ if (existingIdx >= 0) betaConnections.connections[existingIdx] = { ...sConn };
11257
+ else betaConnections.connections.push({ ...sConn });
11258
+ }
11259
+ await fs$1.mkdir(dirname(targetConnectionsFile), { recursive: true });
11260
+ await fs$1.writeFile(targetConnectionsFile, JSON.stringify(betaConnections, null, 2));
11261
+ logger().info("[Migration] Connections merged successfully");
11262
+ }
11263
+ }
11264
+ if (selectedPipelines.length > 0 || selectedProjects.length > 0) {
11265
+ const sourceProjectsFile = sourceContext.getProjectsPath();
11266
+ const targetProjectsFile = context.getProjectsPath();
11267
+ const sourceMeta = await getFileMetadata(sourceProjectsFile);
11268
+ const targetMeta = await getFileMetadata(targetProjectsFile);
11269
+ if (!isVersionImportable(sourceMeta.version, targetMeta.version)) throw new Error("Cannot migrate projects: Source version is newer than current version.");
11270
+ let stableFileRepo = {
11271
+ version: "3.0.0",
11272
+ projects: [],
11273
+ pipelines: []
11274
+ };
11275
+ if (existsSync(sourceProjectsFile)) {
11276
+ const stableContent = await fs$1.readFile(sourceProjectsFile, "utf8");
11277
+ stableFileRepo = JSON.parse(stableContent);
11278
+ }
11279
+ let betaFileRepo = {
11280
+ version: "3.0.0",
11281
+ projects: [],
11282
+ pipelines: []
11283
+ };
11284
+ if (existsSync(targetProjectsFile)) try {
11285
+ const betaContent = await fs$1.readFile(targetProjectsFile, "utf8");
11286
+ betaFileRepo = JSON.parse(betaContent);
11287
+ } catch (readErr) {
11288
+ logger().warn("[Migration] Failed to parse existing target projects.json, starting fresh:", readErr);
11289
+ }
11290
+ betaFileRepo.projects = betaFileRepo.projects || [];
11291
+ betaFileRepo.pipelines = betaFileRepo.pipelines || [];
11292
+ for (const projId of selectedProjects) {
11293
+ const stableProj = stableFileRepo.projects.find((p) => p.id === projId);
11294
+ if (stableProj) {
11295
+ const existingProjIdx = betaFileRepo.projects.findIndex((p) => p.id === projId);
11296
+ if (existingProjIdx >= 0) betaFileRepo.projects[existingProjIdx] = { ...stableProj };
11297
+ else betaFileRepo.projects.push({ ...stableProj });
11298
+ }
11299
+ }
11300
+ for (const pipeId of selectedPipelines) {
11301
+ const stablePipe = (stableFileRepo.pipelines || []).find((p) => p.id === pipeId);
11302
+ if (stablePipe) {
11303
+ const parentProjId = stablePipe.project;
11304
+ if (!betaFileRepo.projects.some((p) => p.id === parentProjId)) {
11305
+ const stableProj = stableFileRepo.projects.find((p) => p.id === parentProjId);
11306
+ if (stableProj) betaFileRepo.projects.push({ ...stableProj });
11307
+ }
11308
+ if (stablePipe.type === "internal") {
11309
+ const stablePipeFile = sourceContext.getConfigPath(`${stablePipe.configName}.json`);
11310
+ const betaPipeFile = context.getConfigPath(`${stablePipe.configName}.json`);
11311
+ if (existsSync(stablePipeFile)) {
11312
+ await fs$1.mkdir(dirname(betaPipeFile), { recursive: true });
11313
+ try {
11314
+ const pipeContent = await fs$1.readFile(stablePipeFile, "utf8");
11315
+ const rawJson = JSON.parse(pipeContent);
11316
+ const migratedJson = await savedFileMigrator.migrate(rawJson);
11317
+ await fs$1.writeFile(betaPipeFile, JSON.stringify(migratedJson, null, 2));
11318
+ logger().info(`[Migration] Migrated and copied pipeline file: ${stablePipe.configName}`);
11319
+ } catch (err) {
11320
+ await fs$1.copyFile(stablePipeFile, betaPipeFile);
11321
+ logger().error(`[Migration] Error migrating during copy of ${stablePipe.configName}, fallback to direct copy:`, err);
11322
+ }
11323
+ }
11324
+ }
11325
+ const updatedPipe = { ...stablePipe };
11326
+ if (updatedPipe.type !== "pipelab-cloud") updatedPipe.lastModified = (/* @__PURE__ */ new Date()).toISOString();
11327
+ const existingPipeIdx = betaFileRepo.pipelines.findIndex((p) => p.id === pipeId);
11328
+ if (existingPipeIdx >= 0) betaFileRepo.pipelines[existingPipeIdx] = updatedPipe;
11329
+ else betaFileRepo.pipelines.push(updatedPipe);
11330
+ }
11331
+ }
11332
+ await fs$1.mkdir(dirname(targetProjectsFile), { recursive: true });
11333
+ await fs$1.writeFile(targetProjectsFile, JSON.stringify(betaFileRepo, null, 2));
11334
+ logger().info("[Migration] Merged and saved projects.json successfully");
11335
+ }
11336
+ send({
11337
+ type: "end",
11338
+ data: {
11339
+ type: "success",
11340
+ result: { result: "ok" }
11341
+ }
11342
+ });
11343
+ } catch (e) {
11344
+ logger().error("[Migration] Error performing migration:", e);
11345
+ send({
11346
+ type: "end",
11347
+ data: {
11348
+ type: "error",
11349
+ ipcError: e instanceof Error ? e.message : "Failed to migrate selected data"
11350
+ }
11351
+ });
11352
+ }
11353
+ });
11354
+ };
11355
+ //#endregion
10275
11356
  //#region src/handlers/index.ts
10276
11357
  const registerAllHandlers = async (options) => {
10277
11358
  const context = options.context;
@@ -10283,6 +11364,8 @@ const registerAllHandlers = async (options) => {
10283
11364
  registerAgentsHandlers(context);
10284
11365
  registerAuthHandlers(context);
10285
11366
  registerSystemHandlers(options);
11367
+ registerPluginsHandlers(context);
11368
+ registerMigrationHandlers(context);
10286
11369
  const { registerPlugins } = usePlugins();
10287
11370
  builtInPlugins({ context });
10288
11371
  };
@@ -52555,9 +53638,7 @@ async function runPipelineCommand(file, options, version) {
52555
53638
  version,
52556
53639
  context
52557
53640
  });
52558
- registerMigrationHandlers(context);
52559
- await (await setupConfigFile("settings", { context })).getConfig();
52560
- const cachePath = join(context.userDataPath, "cache");
53641
+ const cachePath = context.getCachePath(CacheFolder.Pipelines, effectivePipelineId);
52561
53642
  await mkdir(cachePath, { recursive: true });
52562
53643
  const abortController = new AbortController();
52563
53644
  let supabase;
@@ -52630,15 +53711,63 @@ async function runPipelineCommand(file, options, version) {
52630
53711
  */
52631
53712
  async function fetchPackageReleases(packageName, options = {}) {
52632
53713
  const { repo = "CynToolkit/pipelab", allowPrerelease = false } = options;
52633
- const url = `https://api.github.com/repos/${repo}/releases`;
52634
- console.log(`[GitHub] Fetching releases for ${packageName} from ${url}...`);
52635
53714
  try {
52636
- const response = await fetch(url, { headers: {
52637
- "User-Agent": "Pipelab-Desktop-Updater",
52638
- Accept: "application/vnd.github.v3+json"
52639
- } });
53715
+ const override = process.env.PIPELAB_OVERRIDE_RELEASE;
53716
+ if (override) {
53717
+ const targetTag = override.includes("@") ? override : `${packageName}@${override}`;
53718
+ console.log(`[GitHub] Fetching specific override release: ${targetTag}`);
53719
+ const response = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(targetTag)}`, {
53720
+ headers: {
53721
+ "User-Agent": "Pipelab-Desktop-Updater",
53722
+ Accept: "application/vnd.github.v3+json"
53723
+ },
53724
+ signal: AbortSignal.timeout(1e4)
53725
+ });
53726
+ if (response.ok) return [await response.json()];
53727
+ console.warn(`[GitHub] Override release tag ${targetTag} not found or error occurred`);
53728
+ }
53729
+ const matchingRefsUrl = `https://api.github.com/repos/${repo}/git/matching-refs/tags/${encodeURIComponent(packageName)}`;
53730
+ console.log(`[GitHub] Querying matching tags from ${matchingRefsUrl}...`);
53731
+ const response = await fetch(matchingRefsUrl, {
53732
+ headers: {
53733
+ "User-Agent": "Pipelab-Desktop-Updater",
53734
+ Accept: "application/vnd.github.v3+json"
53735
+ },
53736
+ signal: AbortSignal.timeout(1e4)
53737
+ });
52640
53738
  if (!response.ok) throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
52641
- return (await response.json()).filter((r) => r.tag_name.startsWith(`${packageName}@`)).filter((r) => allowPrerelease || !r.prerelease);
53739
+ const refs = await response.json();
53740
+ if (!Array.isArray(refs)) return [];
53741
+ const filteredTags = refs.map((r) => r.ref.replace("refs/tags/", "")).filter((tag) => tag.startsWith(`${packageName}@`)).filter((tag) => {
53742
+ const version = tag.split("@").pop();
53743
+ if (!version || !semver.valid(version)) return false;
53744
+ const isPrerelease = semver.prerelease(version) !== null;
53745
+ return allowPrerelease || !isPrerelease;
53746
+ });
53747
+ if (filteredTags.length === 0) return [];
53748
+ filteredTags.sort((a, b) => {
53749
+ const vA = a.split("@").pop() || "0.0.0";
53750
+ const vB = b.split("@").pop() || "0.0.0";
53751
+ return semver.rcompare(vA, vB);
53752
+ });
53753
+ for (const tag of filteredTags) {
53754
+ console.log(`[GitHub] Fetching release details for tag: ${tag}`);
53755
+ const releaseResponse = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`, {
53756
+ headers: {
53757
+ "User-Agent": "Pipelab-Desktop-Updater",
53758
+ Accept: "application/vnd.github.v3+json"
53759
+ },
53760
+ signal: AbortSignal.timeout(1e4)
53761
+ });
53762
+ if (releaseResponse.status === 404) {
53763
+ console.warn(`[GitHub] Tag "${tag}" has no associated Release, skipping.`);
53764
+ continue;
53765
+ }
53766
+ if (!releaseResponse.ok) throw new Error(`GitHub API error: ${releaseResponse.status} ${releaseResponse.statusText}`);
53767
+ return [await releaseResponse.json()];
53768
+ }
53769
+ console.warn(`[GitHub] No published Release found for any matching tag of "${packageName}".`);
53770
+ return [];
52642
53771
  } catch (error) {
52643
53772
  console.error(`[GitHub] Failed to fetch releases for ${packageName}:`, error);
52644
53773
  return [];
@@ -52681,6 +53810,6 @@ async function fetchLatestDesktopRelease(options = {}) {
52681
53810
  return latest;
52682
53811
  }
52683
53812
  //#endregion
52684
- 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 };
53813
+ export { BuildHistoryStorage, CacheFolder, PipelabContext, WebSocketServer, builtInPlugins, deletePipelineConfigFileByName, deletePipelineConfigFileByPath, downloadFile, ensure, ensureNodeJS, ensurePNPM, executeGraphWithHistory, extractTarGz, extractZip, fetchLatestDesktopRelease, fetchLatestPackageRelease, fetchPackage, fetchPackageReleases, fetchPipelabAsset, fetchPipelabCli, fetchPipelabPlugin, findInstalledPlugins, getDefaultUserDataPath, getFinalPlugins, getFolderSize, handleActionExecute, isDev, isOnline, loadCustomPlugin, loadPipelabPlugin, projectRoot, registerAgentsHandlers, registerAllHandlers, registerConfigHandlers, registerEngineHandlers, registerFsHandlers, registerHistoryHandlers, registerShellHandlers, runPipelineCommand, runPnpm, runWithLiveLogs, sendStartupProgress, sendStartupReady, serveCommand, setupConnectionsConfigFile, setupPipelineConfigFileByName, setupPipelineConfigFileByPath, setupProjectsConfigFile, setupSettingsConfigFile, useAPI, usePluginAPI, webSocketServer, zipFolder };
52685
53814
 
52686
53815
  //# sourceMappingURL=index.mjs.map