@pipelab/core-node 1.0.0-beta.1 → 1.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipelab/core-node",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.10",
4
4
  "private": false,
5
5
  "description": "The Pipelab automation engine for Node.js",
6
6
  "license": "FSL-1.1-MIT",
@@ -26,6 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
+ "@supabase/supabase-js": "2.99.3",
29
30
  "adm-zip": "0.5.16",
30
31
  "archiver": "7.0.1",
31
32
  "check-disk-space": "3.4.0",
@@ -39,9 +40,8 @@
39
40
  "type-fest": "4.39.0",
40
41
  "ws": "8.18.3",
41
42
  "yauzl": "2.10.0",
42
- "@supabase/supabase-js": "2.99.3",
43
- "@pipelab/constants": "1.0.0-beta.0",
44
- "@pipelab/shared": "1.0.0-beta.0"
43
+ "@pipelab/constants": "1.0.0-beta.8",
44
+ "@pipelab/shared": "1.0.0-beta.6"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/adm-zip": "0.5.7",
@@ -54,7 +54,7 @@
54
54
  "@types/yauzl": "2.10.3",
55
55
  "tsdown": "0.21.2",
56
56
  "typescript": "^5.0.0",
57
- "@pipelab/tsconfig": "1.0.0-beta.0"
57
+ "@pipelab/tsconfig": "1.0.0-beta.6"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsdown",
@@ -0,0 +1,118 @@
1
+ import pacote from "pacote";
2
+ import semver from "semver";
3
+ import { fetchLatestDesktopRelease } from "../src/utils/github.js";
4
+
5
+ const PACKAGES = [
6
+ // Apps & Core
7
+ "@pipelab/cli",
8
+ "@pipelab/ui",
9
+ "@pipelab/shared",
10
+ "@pipelab/core-node",
11
+ "@pipelab/constants",
12
+ "@pipelab/migration",
13
+
14
+ // Plugins
15
+ "@pipelab/plugin-construct",
16
+ "@pipelab/plugin-core",
17
+ "@pipelab/plugin-discord",
18
+ "@pipelab/plugin-electron",
19
+ "@pipelab/plugin-filesystem",
20
+ "@pipelab/plugin-itch",
21
+ "@pipelab/plugin-minify",
22
+ "@pipelab/plugin-netlify",
23
+ "@pipelab/plugin-nvpatch",
24
+ "@pipelab/plugin-poki",
25
+ "@pipelab/plugin-steam",
26
+ "@pipelab/plugin-system",
27
+ "@pipelab/plugin-tauri",
28
+
29
+ // Assets
30
+ "@pipelab/asset-discord",
31
+ "@pipelab/asset-electron",
32
+ "@pipelab/asset-netlify",
33
+ "@pipelab/asset-tauri"
34
+ ];
35
+
36
+ async function simulate() {
37
+ console.log("==========================================");
38
+ console.log(" PIPELAB UPDATE FLOW SIMULATOR ");
39
+ console.log("==========================================\n");
40
+
41
+ // 1. Simulate Desktop GitHub Release Update
42
+ console.log("--- 1. Simulating Desktop Application (GitHub Releases) ---");
43
+ try {
44
+ const stableApp = await fetchLatestDesktopRelease({ allowPrerelease: false });
45
+ const betaApp = await fetchLatestDesktopRelease({ allowPrerelease: true });
46
+
47
+ console.log(`[STABLE] Resolved version: ${stableApp ? stableApp.tag_name : "None (Stable users protected)"}`);
48
+ console.log(`[BETA] Resolved version: ${betaApp ? betaApp.tag_name : "None"}`);
49
+ } catch (error) {
50
+ console.error("Failed to fetch desktop releases from GitHub:", error);
51
+ }
52
+
53
+ console.log("\n--- 2. Simulating NPM Packages (CLI, UI, Plugins, Assets) ---");
54
+ console.log("Fetching data from NPM registry. Please wait...\n");
55
+
56
+ const results: Array<{
57
+ package: string;
58
+ stable: string;
59
+ beta: string;
60
+ notes: string;
61
+ }> = [];
62
+
63
+ for (const pkg of PACKAGES) {
64
+ try {
65
+ const packument = await pacote.packument(pkg);
66
+ const stableVersion = packument["dist-tags"]?.latest || "N/A";
67
+ const betaVersion = packument["dist-tags"]?.beta || "N/A";
68
+
69
+ let notes = "";
70
+ if (stableVersion === "N/A" && betaVersion === "N/A") {
71
+ notes = "⚠️ Package not published on NPM";
72
+ } else if (betaVersion !== "N/A" && stableVersion !== "N/A") {
73
+ if (betaVersion === stableVersion) {
74
+ notes = "Stable and Beta are in sync";
75
+ } else {
76
+ // Attempt semver comparison
77
+ const cleanStable = semver.coerce(stableVersion);
78
+ const cleanBeta = semver.coerce(betaVersion);
79
+ if (cleanStable && cleanBeta) {
80
+ const comp = semver.compare(stableVersion, betaVersion);
81
+ if (comp < 0) {
82
+ notes = "🚀 Beta is ahead";
83
+ } else if (comp > 0) {
84
+ notes = "Stable is ahead";
85
+ } else {
86
+ notes = "Stable and Beta are in sync (coerced)";
87
+ }
88
+ } else {
89
+ notes = "Different versions (non-semver)";
90
+ }
91
+ }
92
+ } else if (betaVersion !== "N/A" && stableVersion === "N/A") {
93
+ notes = "Only Beta version exists";
94
+ } else if (stableVersion !== "N/A" && betaVersion === "N/A") {
95
+ notes = "No Beta tag published";
96
+ }
97
+
98
+ results.push({
99
+ package: pkg,
100
+ stable: stableVersion,
101
+ beta: betaVersion,
102
+ notes
103
+ });
104
+ } catch (error: any) {
105
+ results.push({
106
+ package: pkg,
107
+ stable: "ERROR",
108
+ beta: "ERROR",
109
+ notes: `Failed to fetch: ${error.message}`
110
+ });
111
+ }
112
+ }
113
+
114
+ console.table(results);
115
+ console.log("\n==========================================\n");
116
+ }
117
+
118
+ simulate().catch(console.error);
package/src/config.ts CHANGED
@@ -66,6 +66,16 @@ export const setupConfigFile = async <T>(
66
66
  try {
67
67
  json = await migrator.migrate(originalJson, {
68
68
  debug: false,
69
+ onStep: async (state: any, version: string) => {
70
+ const parsedPath = path.parse(filesPath);
71
+ const versionedPath = path.join(parsedPath.dir, `${parsedPath.name}.v${version}.json`);
72
+ try {
73
+ await fs.writeFile(versionedPath, JSON.stringify(state));
74
+ logger().info(`Intermediate backup created for ${name} at ${versionedPath}`);
75
+ } catch (e) {
76
+ logger().error(`Failed to create intermediate backup for ${name} at v${version}:`, e);
77
+ }
78
+ },
69
79
  });
70
80
  } catch (e) {
71
81
  logger().error(`Error migrating config ${name}:`, e);
@@ -75,21 +85,6 @@ export const setupConfigFile = async <T>(
75
85
  const originalVersion = originalJson?.version;
76
86
  const newVersion = json?.version;
77
87
 
78
- // Check if migration actually changed the version
79
- if (originalVersion !== newVersion && content !== undefined) {
80
- // Backup previous file before overwriting
81
- const parsedPath = path.parse(filesPath);
82
- const versionSuffix = originalVersion || "unknown";
83
- const backupPath = path.join(parsedPath.dir, `${parsedPath.name}.${versionSuffix}.bak`);
84
-
85
- try {
86
- await fs.copyFile(filesPath, backupPath);
87
- logger().info(`Backup created for ${name} at ${backupPath} (version ${versionSuffix})`);
88
- } catch (e) {
89
- logger().error(`Failed to create backup for ${name}:`, e);
90
- }
91
- }
92
-
93
88
  // Save back migrated config if changed or if it's a new file
94
89
  if (originalVersion !== newVersion || content === undefined) {
95
90
  try {
package/src/context.ts CHANGED
@@ -12,7 +12,7 @@ const _dirname =
12
12
 
13
13
  export const isDev = process.env.NODE_ENV === "development";
14
14
 
15
- export const getDefaultUserDataPath = () => {
15
+ export const getDefaultUserDataPath = (env?: "dev" | "beta" | "prod") => {
16
16
  const base = (() => {
17
17
  switch (platform()) {
18
18
  case "win32":
@@ -24,7 +24,10 @@ export const getDefaultUserDataPath = () => {
24
24
  }
25
25
  })();
26
26
 
27
- return join(base, "@pipelab", isDev ? "app-dev" : "app");
27
+ const mode = env ?? (isDev ? "dev" : "prod");
28
+ const folder = mode === "dev" ? "app-dev" : mode === "beta" ? "app-beta" : "app";
29
+
30
+ return join(base, "@pipelab", folder);
28
31
  };
29
32
 
30
33
  /**
@@ -45,13 +48,16 @@ export const projectRoot = findProjectRoot(_dirname);
45
48
 
46
49
  export interface PipelabContextOptions {
47
50
  userDataPath: string;
51
+ releaseTag?: string;
48
52
  }
49
53
 
50
54
  export class PipelabContext {
51
55
  public readonly userDataPath: string;
56
+ public readonly releaseTag: string;
52
57
 
53
58
  constructor(options: PipelabContextOptions) {
54
59
  this.userDataPath = options.userDataPath;
60
+ this.releaseTag = options.releaseTag || "latest";
55
61
  }
56
62
 
57
63
  getPackagesPath(...subpaths: string[]) {
@@ -53,9 +53,9 @@ export const handleConditionExecute = async (
53
53
  .find((plugin) => plugin.id === pluginId)
54
54
  ?.nodes.find((node: any) => node.node.id === nodeId) as
55
55
  | {
56
- node: Condition;
57
- runner: ConditionRunner<any>;
58
- }
56
+ node: Condition;
57
+ runner: ConditionRunner<any>;
58
+ }
59
59
  | undefined;
60
60
 
61
61
  if (!node) {
@@ -136,9 +136,9 @@ export const handleActionExecute = async (
136
136
  .find((plugin) => plugin.id === pluginId)
137
137
  ?.nodes.find((node: any) => node.node.id === nodeId) as
138
138
  | {
139
- node: Action;
140
- runner: ActionRunner<any>;
141
- }
139
+ node: Action;
140
+ runner: ActionRunner<any>;
141
+ }
142
142
  | undefined;
143
143
 
144
144
  if (!node) {
@@ -93,17 +93,13 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
93
93
 
94
94
  // 2. Filter by maxEntries (sort by date first to keep the newest)
95
95
  if (maxEntries > 0 && entries.length > maxEntries) {
96
- entries = entries
97
- .sort((a, b) => b.createdAt - a.createdAt)
98
- .slice(0, maxEntries);
96
+ entries = entries.sort((a, b) => b.createdAt - a.createdAt).slice(0, maxEntries);
99
97
  }
100
98
 
101
99
  if (entries.length < originalCount) {
102
100
  this.logger
103
101
  .logger()
104
- .info(
105
- `[${pipelineId}] Pruned ${originalCount - entries.length} history entries.`,
106
- );
102
+ .info(`[${pipelineId}] Pruned ${originalCount - entries.length} history entries.`);
107
103
  await this.savePipelineHistory(pipelineId, entries);
108
104
  }
109
105
  }
@@ -271,8 +267,10 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
271
267
  await unlink(pipelinePath);
272
268
  this.logger.logger().info(`Cleared history for pipeline "${pipelineId}"`);
273
269
  } catch (error: any) {
274
- if (error.code === 'ENOENT') {
275
- this.logger.logger().warn(`No history file found for pipeline "${pipelineId}". Nothing to clear.`);
270
+ if (error.code === "ENOENT") {
271
+ this.logger
272
+ .logger()
273
+ .warn(`No history file found for pipeline "${pipelineId}". Nothing to clear.`);
276
274
  return;
277
275
  }
278
276
  this.logger.logger().error(`Failed to clear history for pipeline "${pipelineId}":`, error);
@@ -127,8 +127,7 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
127
127
  type: "end",
128
128
  data: {
129
129
  type: "error",
130
- ipcError:
131
- error instanceof Error ? error.message : "Failed to get build history entries",
130
+ ipcError: error instanceof Error ? error.message : "Failed to get build history entries",
132
131
  },
133
132
  });
134
133
  }
@@ -158,8 +157,7 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
158
157
  type: "end",
159
158
  data: {
160
159
  type: "error",
161
- ipcError:
162
- error instanceof Error ? error.message : "Failed to update build history entry",
160
+ ipcError: error instanceof Error ? error.message : "Failed to update build history entry",
163
161
  },
164
162
  });
165
163
  }
@@ -189,8 +187,7 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
189
187
  type: "end",
190
188
  data: {
191
189
  type: "error",
192
- ipcError:
193
- error instanceof Error ? error.message : "Failed to delete build history entry",
190
+ ipcError: error instanceof Error ? error.message : "Failed to delete build history entry",
194
191
  },
195
192
  });
196
193
  }
@@ -250,7 +247,8 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
250
247
  type: "end",
251
248
  data: {
252
249
  type: "error",
253
- ipcError: error instanceof Error ? error.message : "Failed to clear build history for pipeline",
250
+ ipcError:
251
+ error instanceof Error ? error.message : "Failed to clear build history for pipeline",
254
252
  },
255
253
  });
256
254
  }
@@ -290,7 +288,7 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
290
288
  handle("build-history:configure", async (_, { send, value }) => {
291
289
  try {
292
290
  logger().info("Updating build history configuration:", value.config);
293
-
291
+
294
292
  const settings = await setupConfigFile<AppConfig>("settings", { context });
295
293
  const currentConfig = await settings.getConfig();
296
294
 
@@ -26,10 +26,10 @@ export const registerAllHandlers = async (options: {
26
26
  registerSystemHandlers(options);
27
27
 
28
28
  const { registerPlugins } = usePlugins();
29
- const plugins = await builtInPlugins({
29
+ // Execute in the background! The plugins will be dynamically registered and broadcasted to the UI.
30
+ builtInPlugins({
30
31
  context,
31
32
  });
32
- registerPlugins(plugins as any);
33
33
  };
34
34
 
35
35
  export { registerShellHandlers } from "./shell";
@@ -23,9 +23,9 @@ const DEFAULT_PLUGIN_IDS = [
23
23
  export const loadPipelabPlugin = async (id: string, options: { context: PipelabContext }) => {
24
24
  try {
25
25
  const packageName = `@pipelab/plugin-${id}`;
26
- const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, "latest", {
26
+ const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, options.context.releaseTag, {
27
27
  context: options.context,
28
- installDeps: true,
28
+ installDeps: false,
29
29
  });
30
30
 
31
31
  console.log(`[Plugins] [${id}] Attempting to import from: ${entryPoint}`);
@@ -34,7 +34,7 @@ export const loadPipelabPlugin = async (id: string, options: { context: PipelabC
34
34
  try {
35
35
  const files = await readdir(packageDir, { recursive: true });
36
36
  console.log(`[Plugins] [${id}] Directory contents:`, files);
37
- } catch (e) { }
37
+ } catch (e) {}
38
38
  }
39
39
 
40
40
  const pluginModule = await import(pathToFileURL(entryPoint).href);
@@ -52,27 +52,32 @@ export const loadPipelabPlugin = async (id: string, options: { context: PipelabC
52
52
  };
53
53
 
54
54
  export const builtInPlugins = async (options: { context: PipelabContext }) => {
55
- console.log("[Plugins] Finalizing default plugins list...");
55
+ console.log("[Plugins] Starting background plugin loading...");
56
56
 
57
57
  // Pre-ensure Node.js and PNPM once in parallel so plugins don't have to wait for them
58
58
  sendStartupProgress("Preparing environment...");
59
- await Promise.all([
60
- ensureNodeJS(options.context),
61
- ensurePNPM(options.context),
62
- ]);
59
+ await Promise.all([ensureNodeJS(options.context), ensurePNPM(options.context)]);
63
60
 
64
- const results = await Promise.allSettled(
65
- DEFAULT_PLUGIN_IDS.map(async (id) => {
66
- sendStartupProgress(`Loading plugin: ${id}`);
67
- return loadPipelabPlugin(id, options);
68
- }),
69
- );
61
+ const { usePlugins } = await import("@pipelab/shared");
62
+ const { registerPlugins } = usePlugins();
63
+ const { webSocketServer } = await import("./index");
70
64
 
71
- const plugins = results
72
- .filter((r) => r.status === "fulfilled")
73
- .map((r) => (r as PromiseFulfilledResult<any>).value);
65
+ // Load plugins asynchronously in the background
66
+ (async () => {
67
+ for (const id of DEFAULT_PLUGIN_IDS) {
68
+ sendStartupProgress(`Loading plugin: ${id}`);
69
+ const plugin = await loadPipelabPlugin(id, options);
70
+ if (plugin) {
71
+ registerPlugins([plugin]);
72
+ webSocketServer.broadcast("plugin:loaded", { plugin });
73
+ }
74
+ }
75
+ console.log("[Plugins] All default plugins loaded.");
76
+ sendStartupProgress("All plugins loaded.");
77
+ setTimeout(() => {
78
+ webSocketServer.broadcast("startup:progress", { type: "ready" });
79
+ }, 2000);
80
+ })();
74
81
 
75
- const filtered = plugins.filter(Boolean).flat();
76
- console.log(`[Plugins] Successfully loaded ${filtered.length} default plugins`);
77
- return filtered;
82
+ return [];
78
83
  };
package/src/runner.ts CHANGED
@@ -79,10 +79,7 @@ export async function runPipelineCommand(file: string, options: RunOptions, vers
79
79
  const cloudRunId = process.env.CLOUD_RUN_ID;
80
80
  if (options.cloud && cloudRunId) {
81
81
  const { createClient } = await import("@supabase/supabase-js");
82
- supabase = createClient(
83
- process.env.SUPABASE_URL!,
84
- process.env.SUPABASE_SERVICE_ROLE_KEY!
85
- );
82
+ supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
86
83
  console.log(`Cloud mode enabled. Streaming logs for run: ${cloudRunId}`);
87
84
  }
88
85
 
@@ -97,23 +94,29 @@ export async function runPipelineCommand(file: string, options: RunOptions, vers
97
94
  onNodeEnter: (node) => {
98
95
  console.log(`[ENTER] ${node.name} (${node.uid})`);
99
96
  if (supabase) {
100
- supabase.from("cloud_run_logs").insert({
101
- run_id: cloudRunId,
102
- message: `[ENTER] ${node.name} (${node.uid})`,
103
- node_uid: node.uid,
104
- type: "node-enter"
105
- }).then();
97
+ supabase
98
+ .from("cloud_run_logs")
99
+ .insert({
100
+ run_id: cloudRunId,
101
+ message: `[ENTER] ${node.name} (${node.uid})`,
102
+ node_uid: node.uid,
103
+ type: "node-enter",
104
+ })
105
+ .then();
106
106
  }
107
107
  },
108
108
  onNodeExit: (node) => {
109
109
  console.log(`[EXIT] ${node.name} (${node.uid})`);
110
110
  if (supabase) {
111
- supabase.from("cloud_run_logs").insert({
112
- run_id: cloudRunId,
113
- message: `[EXIT] ${node.name} (${node.uid})`,
114
- node_uid: node.uid,
115
- type: "node-exit"
116
- }).then();
111
+ supabase
112
+ .from("cloud_run_logs")
113
+ .insert({
114
+ run_id: cloudRunId,
115
+ message: `[EXIT] ${node.name} (${node.uid})`,
116
+ node_uid: node.uid,
117
+ type: "node-exit",
118
+ })
119
+ .then();
117
120
  }
118
121
  },
119
122
  onLog: (data, node) => {
@@ -121,12 +124,15 @@ export async function runPipelineCommand(file: string, options: RunOptions, vers
121
124
  const message = data.data.message.join(" ");
122
125
  console.log(`[LOG] ${message}`);
123
126
  if (supabase) {
124
- supabase.from("cloud_run_logs").insert({
125
- run_id: cloudRunId,
126
- message: message,
127
- node_uid: node?.uid,
128
- type: "log"
129
- }).then();
127
+ supabase
128
+ .from("cloud_run_logs")
129
+ .insert({
130
+ run_id: cloudRunId,
131
+ message: message,
132
+ node_uid: node?.uid,
133
+ type: "log",
134
+ })
135
+ .then();
130
136
  }
131
137
  }
132
138
  },
package/src/server.ts CHANGED
@@ -38,13 +38,15 @@ export const sendStartupReady = () => {
38
38
 
39
39
  export async function serveCommand(options: ServeOptions, version: string, _dirname: string) {
40
40
  if (!options.userData) throw new Error("userDataPath is required for serveCommand");
41
+ const releaseTag = version.includes("beta") ? "beta" : "latest";
41
42
  const context = new PipelabContext({
42
43
  userDataPath: options.userData,
44
+ releaseTag,
43
45
  });
44
46
 
45
47
  let rawAssetFolder: string | undefined;
46
48
  if (!isDev) {
47
- rawAssetFolder = await fetchPipelabAsset("@pipelab/ui", "latest", { context });
49
+ rawAssetFolder = await fetchPipelabAsset("@pipelab/ui", releaseTag, { context });
48
50
  }
49
51
 
50
52
  const server = http.createServer(async (request, response) => {
@@ -66,7 +68,7 @@ export async function serveCommand(options: ServeOptions, version: string, _dirn
66
68
  response.writeHead(404, { "Content-Type": "text/plain" });
67
69
  response.end(
68
70
  `Error: UI directory not found at ${rawAssetFolder}.\n` +
69
- "Please run 'pnpm build' in apps/ui to generate the distribution.",
71
+ "Please run 'pnpm build' in apps/ui to generate the distribution.",
70
72
  );
71
73
  return;
72
74
  }
@@ -1,6 +1,15 @@
1
1
  import { mkdir, createWriteStream, createReadStream } from "node:fs";
2
2
  import { execa, Options, Subprocess } from "execa";
3
- import { mkdir as mkdirP, access, writeFile, realpath, mkdtemp, chmod, stat, readdir } from "node:fs/promises";
3
+ import {
4
+ mkdir as mkdirP,
5
+ access,
6
+ writeFile,
7
+ realpath,
8
+ mkdtemp,
9
+ chmod,
10
+ stat,
11
+ readdir,
12
+ } from "node:fs/promises";
4
13
  import { join, dirname } from "node:path";
5
14
  import { tmpdir } from "node:os";
6
15
  import tar from "tar";
@@ -22,7 +22,7 @@ export interface FetchReleaseOptions {
22
22
  */
23
23
  export async function fetchPackageReleases(
24
24
  packageName: string,
25
- options: FetchReleaseOptions = {}
25
+ options: FetchReleaseOptions = {},
26
26
  ): Promise<GitHubRelease[]> {
27
27
  const { repo = "CynToolkit/pipelab", allowPrerelease = false } = options;
28
28
  const url = `https://api.github.com/repos/${repo}/releases`;
@@ -61,7 +61,7 @@ export async function fetchPackageReleases(
61
61
  */
62
62
  export async function fetchLatestPackageRelease(
63
63
  packageName: string,
64
- options: FetchReleaseOptions = {}
64
+ options: FetchReleaseOptions = {},
65
65
  ): Promise<GitHubRelease | null> {
66
66
  const releases = await fetchPackageReleases(packageName, options);
67
67
 
@@ -75,7 +75,9 @@ export async function fetchLatestPackageRelease(
75
75
  const targetTag = override.includes("@") ? override : `${packageName}@${override}`;
76
76
  const release = releases.find((r) => r.tag_name === targetTag);
77
77
  if (release) {
78
- console.log(`[GitHub] Using release override from PIPELAB_OVERRIDE_RELEASE: ${release.tag_name}`);
78
+ console.log(
79
+ `[GitHub] Using release override from PIPELAB_OVERRIDE_RELEASE: ${release.tag_name}`,
80
+ );
79
81
  return release;
80
82
  }
81
83
  console.warn(`[GitHub] Override release ${override} not found for package ${packageName}`);
@@ -101,12 +103,14 @@ export async function fetchLatestPackageRelease(
101
103
  * This ensures we don't accidentally pick a package release (e.g. @pipelab/shared) as the "latest".
102
104
  */
103
105
  export async function fetchLatestDesktopRelease(
104
- options: FetchReleaseOptions = {}
106
+ options: FetchReleaseOptions = {},
105
107
  ): Promise<GitHubRelease | null> {
106
108
  const latest = await fetchLatestPackageRelease("@pipelab/app", options);
107
-
109
+
108
110
  if (latest) {
109
- console.log(`[GitHub] Found latest desktop release: ${latest.tag_name} (${latest.prerelease ? "pre-release" : "stable"})`);
111
+ console.log(
112
+ `[GitHub] Found latest desktop release: ${latest.tag_name} (${latest.prerelease ? "pre-release" : "stable"})`,
113
+ );
110
114
  } else {
111
115
  console.warn(`[GitHub] No desktop releases found`);
112
116
  }