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

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/src/server.ts CHANGED
@@ -50,6 +50,46 @@ export async function serveCommand(options: ServeOptions, version: string, _dirn
50
50
  }
51
51
 
52
52
  const server = http.createServer(async (request, response) => {
53
+ // Serve local media files securely via HTTP
54
+ if (request.url?.startsWith("/media-file/")) {
55
+ const prefix = "/media-file/";
56
+ const encodedPath = request.url.substring(prefix.length);
57
+ const filePath = decodeURIComponent(encodedPath);
58
+ // Strip leading slash on Windows if followed by a drive letter (e.g. /C:/...)
59
+ const normalizedPath =
60
+ filePath.startsWith("/") && filePath.match(/^\/[a-zA-Z]:/)
61
+ ? filePath.substring(1)
62
+ : filePath;
63
+
64
+ if (existsSync(normalizedPath)) {
65
+ try {
66
+ const content = await readFile(normalizedPath);
67
+ let contentType = "application/octet-stream";
68
+ if (normalizedPath.endsWith(".png")) contentType = "image/png";
69
+ else if (normalizedPath.endsWith(".jpg") || normalizedPath.endsWith(".jpeg"))
70
+ contentType = "image/jpeg";
71
+ else if (normalizedPath.endsWith(".svg")) contentType = "image/svg+xml";
72
+ else if (normalizedPath.endsWith(".gif")) contentType = "image/gif";
73
+ else if (normalizedPath.endsWith(".webp")) contentType = "image/webp";
74
+
75
+ response.writeHead(200, {
76
+ "Content-Type": contentType,
77
+ "Access-Control-Allow-Origin": "*",
78
+ });
79
+ response.end(content);
80
+ return;
81
+ } catch (e) {
82
+ response.writeHead(500, { "Content-Type": "text/plain" });
83
+ response.end(`Error reading file: ${e}`);
84
+ return;
85
+ }
86
+ } else {
87
+ response.writeHead(404, { "Content-Type": "text/plain" });
88
+ response.end(`File not found: ${normalizedPath}`);
89
+ return;
90
+ }
91
+ }
92
+
53
93
  if (isDev) {
54
94
  response.writeHead(200, { "Content-Type": "text/html" });
55
95
  response.end(`
@@ -2,32 +2,22 @@ import type { BrowserWindow } from "electron";
2
2
  import type { PipelabContext } from "../context";
3
3
  import type {
4
4
  Action,
5
- Condition,
6
- Loop,
7
5
  Expression,
8
6
  Event,
9
7
  SetOutputActionFn,
10
- SetOutputLoopFn,
11
8
  SetOutputExpressionFn,
12
9
  ExtractInputsFromAction,
13
- ExtractInputsFromCondition,
14
- ExtractInputsFromLoop,
15
10
  ExtractInputsFromEvent,
16
11
  ExtractInputsFromExpression,
17
12
  } from "@pipelab/shared";
18
13
 
19
14
  export type {
20
15
  Action,
21
- Condition,
22
- Loop,
23
16
  Expression,
24
17
  Event,
25
18
  SetOutputActionFn,
26
- SetOutputLoopFn,
27
19
  SetOutputExpressionFn,
28
20
  ExtractInputsFromAction,
29
- ExtractInputsFromCondition,
30
- ExtractInputsFromLoop,
31
21
  ExtractInputsFromEvent,
32
22
  ExtractInputsFromExpression,
33
23
  };
@@ -60,25 +50,6 @@ export type ActionRunnerData<ACTION extends Action> = {
60
50
 
61
51
  export type ActionRunner<ACTION extends Action> = (data: ActionRunnerData<ACTION>) => Promise<void>;
62
52
 
63
- export type ConditionRunner<CONDITION extends Condition> = (data: {
64
- log: typeof console.log;
65
- inputs: ExtractInputsFromCondition<CONDITION>;
66
- setMeta: (callback: (data: CONDITION["meta"]) => CONDITION["meta"]) => void;
67
- meta: CONDITION["meta"];
68
- cwd: string;
69
- context: PipelabContext;
70
- }) => Promise<boolean>;
71
-
72
- export type LoopRunner<LOOP extends Loop> = (data: {
73
- log: typeof console.log;
74
- setOutput: SetOutputLoopFn<LOOP>;
75
- inputs: ExtractInputsFromLoop<LOOP>;
76
- setMeta: (callback: (data: LOOP["meta"]) => LOOP["meta"]) => void;
77
- meta: LOOP["meta"];
78
- cwd: string;
79
- context: PipelabContext;
80
- }) => Promise<"step" | "exit">;
81
-
82
53
  export type ExpressionRunner<EXPRESSION extends Expression> = (data: {
83
54
  log: typeof console.log;
84
55
  setOutput: SetOutputExpressionFn<EXPRESSION>;
@@ -98,4 +69,4 @@ export type EventRunner<EVENT extends Event> = (data: {
98
69
  context: PipelabContext;
99
70
  }) => Promise<void>;
100
71
 
101
- export type Runner = ActionRunner<any> | LoopRunner<any> | EventRunner<any> | ConditionRunner<any>;
72
+ export type Runner = ActionRunner<any> | EventRunner<any>;
@@ -23,7 +23,10 @@ import { pipeline } from "node:stream/promises";
23
23
  export const ensure = async (filesPath: string, defaultContent = "{}") => {
24
24
  await mkdirP(dirname(filesPath), { recursive: true });
25
25
  try {
26
- await access(filesPath);
26
+ const s = await stat(filesPath);
27
+ if (s.size === 0) {
28
+ await writeFile(filesPath, defaultContent);
29
+ }
27
30
  } catch {
28
31
  await writeFile(filesPath, defaultContent);
29
32
  }
@@ -25,12 +25,32 @@ export async function fetchPackageReleases(
25
25
  options: FetchReleaseOptions = {},
26
26
  ): Promise<GitHubRelease[]> {
27
27
  const { repo = "CynToolkit/pipelab", allowPrerelease = false } = options;
28
- const url = `https://api.github.com/repos/${repo}/releases`;
29
-
30
- console.log(`[GitHub] Fetching releases for ${packageName} from ${url}...`);
31
28
 
32
29
  try {
33
- const response = await fetch(url, {
30
+ const override = process.env.PIPELAB_OVERRIDE_RELEASE;
31
+ if (override) {
32
+ const targetTag = override.includes("@") ? override : `${packageName}@${override}`;
33
+ console.log(`[GitHub] Fetching specific override release: ${targetTag}`);
34
+ const response = await fetch(
35
+ `https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(targetTag)}`,
36
+ {
37
+ headers: {
38
+ "User-Agent": "Pipelab-Desktop-Updater",
39
+ Accept: "application/vnd.github.v3+json",
40
+ },
41
+ },
42
+ );
43
+ if (response.ok) {
44
+ const release: GitHubRelease = await response.json();
45
+ return [release];
46
+ }
47
+ console.warn(`[GitHub] Override release tag ${targetTag} not found or error occurred`);
48
+ }
49
+
50
+ const matchingRefsUrl = `https://api.github.com/repos/${repo}/git/matching-refs/tags/${encodeURIComponent(packageName)}`;
51
+ console.log(`[GitHub] Querying matching tags from ${matchingRefsUrl}...`);
52
+
53
+ const response = await fetch(matchingRefsUrl, {
34
54
  headers: {
35
55
  "User-Agent": "Pipelab-Desktop-Updater",
36
56
  Accept: "application/vnd.github.v3+json",
@@ -41,14 +61,71 @@ export async function fetchPackageReleases(
41
61
  throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
42
62
  }
43
63
 
44
- const releases: GitHubRelease[] = await response.json();
64
+ const refs = await response.json();
65
+ if (!Array.isArray(refs)) {
66
+ return [];
67
+ }
45
68
 
46
- // Filter for releases that follow the {packageName}@X.Y.Z tag pattern
47
- const packageReleases = releases
48
- .filter((r) => r.tag_name.startsWith(`${packageName}@`))
49
- .filter((r) => allowPrerelease || !r.prerelease);
69
+ // Extract tags matching the packageName@ pattern
70
+ const matchingTags = refs
71
+ .map((r: any) => r.ref.replace("refs/tags/", ""))
72
+ .filter((tag: string) => tag.startsWith(`${packageName}@`));
73
+
74
+ // Filter by semver and prerelease options
75
+ const filteredTags = matchingTags.filter((tag: string) => {
76
+ const version = tag.split("@").pop();
77
+ if (!version || !semver.valid(version)) {
78
+ return false;
79
+ }
80
+ const isPrerelease = semver.prerelease(version) !== null;
81
+ return allowPrerelease || !isPrerelease;
82
+ });
50
83
 
51
- return packageReleases;
84
+ if (filteredTags.length === 0) {
85
+ return [];
86
+ }
87
+
88
+ // Sort by version (newest/highest first)
89
+ filteredTags.sort((a, b) => {
90
+ const vA = a.split("@").pop() || "0.0.0";
91
+ const vB = b.split("@").pop() || "0.0.0";
92
+ return semver.rcompare(vA, vB);
93
+ });
94
+
95
+ // Walk tags from newest to oldest, skipping any that have no GitHub Release attached.
96
+ // A bare git tag (no release) returns 404 from the releases/tags endpoint.
97
+ for (const tag of filteredTags) {
98
+ console.log(`[GitHub] Fetching release details for tag: ${tag}`);
99
+
100
+ const releaseResponse = await fetch(
101
+ `https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`,
102
+ {
103
+ headers: {
104
+ "User-Agent": "Pipelab-Desktop-Updater",
105
+ Accept: "application/vnd.github.v3+json",
106
+ },
107
+ },
108
+ );
109
+
110
+ if (releaseResponse.status === 404) {
111
+ // Tag exists but no Release was published for it — skip and try older tag
112
+ console.warn(`[GitHub] Tag "${tag}" has no associated Release, skipping.`);
113
+ continue;
114
+ }
115
+
116
+ if (!releaseResponse.ok) {
117
+ throw new Error(
118
+ `GitHub API error: ${releaseResponse.status} ${releaseResponse.statusText}`,
119
+ );
120
+ }
121
+
122
+ const release: GitHubRelease = await releaseResponse.json();
123
+ return [release];
124
+ }
125
+
126
+ // All tags were bare (no Release found)
127
+ console.warn(`[GitHub] No published Release found for any matching tag of "${packageName}".`);
128
+ return [];
52
129
  } catch (error) {
53
130
  console.error(`[GitHub] Failed to fetch releases for ${packageName}:`, error);
54
131
  return [];
@@ -1,6 +1,16 @@
1
1
  import { dirname, delimiter, join } from "node:path";
2
2
  import { tmpdir } from "node:os";
3
- import { mkdir, readdir, readFile, writeFile, access, chmod, rm, cp, rename } from "node:fs/promises";
3
+ import {
4
+ mkdir,
5
+ readdir,
6
+ readFile,
7
+ writeFile,
8
+ access,
9
+ chmod,
10
+ rm,
11
+ cp,
12
+ rename,
13
+ } from "node:fs/promises";
4
14
  import { existsSync, constants, statSync, readdirSync } from "node:fs";
5
15
  import pacote from "pacote";
6
16
  import semver from "semver";
@@ -80,12 +90,16 @@ export async function fetchPackage(
80
90
  isLocal?: boolean;
81
91
  entryPoint?: string;
82
92
  }> {
93
+ const start = Date.now();
83
94
  // 0. Check for local monorepo package in development
84
95
  if (isDev && projectRoot && process.env.PIPELAB_FORCE_NPM !== "true") {
85
96
  if (packageName.startsWith("@pipelab/")) {
97
+ const localStart = Date.now();
86
98
  const local = await tryResolveMonorepoPackage(packageName);
87
99
  if (local) {
88
- console.log(`[Fetcher] ${packageName}: Resolved to local source at ${local.packageDir}`);
100
+ console.log(
101
+ `[Fetcher] ${packageName}: Resolved to local source at ${local.packageDir} (${Date.now() - localStart}ms)`,
102
+ );
89
103
  return {
90
104
  ...local,
91
105
  resolvedVersion: "workspace",
@@ -98,7 +112,13 @@ export async function fetchPackage(
98
112
  const baseDir = ctx.getPackagesPath(packageName);
99
113
  let resolvedVersion: string;
100
114
 
101
- console.log(`[Fetcher] Resolving ${packageName}@${versionOrRange || "latest"}...`);
115
+ let resolvedVersionOrRange = versionOrRange;
116
+ if (resolvedVersionOrRange === "local") {
117
+ resolvedVersionOrRange = "latest";
118
+ }
119
+
120
+ console.log(`[Fetcher] Resolving ${packageName}@${resolvedVersionOrRange || "latest"}...`);
121
+ const resolveStart = Date.now();
102
122
 
103
123
  try {
104
124
  // 1. Resolve version/range using npm with session-wide memoization and disk cache
@@ -111,7 +131,7 @@ export async function fetchPackage(
111
131
 
112
132
  const packument = await packumentPromise;
113
133
  const versions = Object.keys(packument.versions);
114
- const range = versionOrRange || "latest";
134
+ const range = resolvedVersionOrRange || "latest";
115
135
 
116
136
  // Prioritize tags (like 'latest', 'beta', etc.) over semver ranges
117
137
  const foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
@@ -122,12 +142,25 @@ export async function fetchPackage(
122
142
  );
123
143
  }
124
144
  resolvedVersion = foundVersion;
125
- console.log(`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm`);
145
+ console.log(
146
+ `[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm (${Date.now() - resolveStart}ms)`,
147
+ );
126
148
  } catch (error) {
127
- console.warn(`[Fetcher] ${packageName}: remote resolution failed, trying local fallback...`);
128
- const fallbackVersion = await tryLocalFallback(versionOrRange, error, baseDir, packageName);
149
+ console.warn(
150
+ `[Fetcher] ${packageName}: remote resolution failed (${Date.now() - resolveStart}ms), trying local fallback...`,
151
+ );
152
+ const fallbackStart = Date.now();
153
+ const fallbackVersion = await tryLocalFallback(
154
+ resolvedVersionOrRange,
155
+ error,
156
+ baseDir,
157
+ packageName,
158
+ );
129
159
  if (fallbackVersion) {
130
160
  resolvedVersion = fallbackVersion;
161
+ console.log(
162
+ `[Fetcher] ${packageName}: Resolved to local fallback ${resolvedVersion} (${Date.now() - fallbackStart}ms)`,
163
+ );
131
164
  } else {
132
165
  throw error;
133
166
  }
@@ -136,12 +169,17 @@ export async function fetchPackage(
136
169
  const cachePath = join(ctx.userDataPath, "cache", "pacote");
137
170
  const packageDir = join(baseDir, resolvedVersion);
138
171
 
172
+ const checkStart = Date.now();
139
173
  // If the package already exists and we don't need to install dependencies (or they are already installed), return immediately
140
174
  const isInstalled = options?.installDeps
141
- ? (isPackageComplete(packageDir) && isDependenciesInstalledSync(packageDir))
175
+ ? isPackageComplete(packageDir) && isDependenciesInstalledSync(packageDir)
142
176
  : isPackageComplete(packageDir);
177
+ const checkDuration = Date.now() - checkStart;
143
178
 
144
179
  if (isInstalled) {
180
+ console.log(
181
+ `[Fetcher] ${packageName}@${resolvedVersion}: Already installed (check took ${checkDuration}ms, fetchPackage took ${Date.now() - start}ms)`,
182
+ );
145
183
  return { packageDir, resolvedVersion };
146
184
  }
147
185
 
@@ -149,8 +187,12 @@ export async function fetchPackage(
149
187
  return withLock(lockKey, async () => {
150
188
  if (!isPackageComplete(packageDir)) {
151
189
  console.log(`[Fetcher] ${packageName}@${resolvedVersion}: Downloading to ${packageDir}...`);
152
-
153
- const tempDir = join(baseDir, `.tmp-${resolvedVersion}-${Math.random().toString(36).slice(2)}`);
190
+ const downloadStart = Date.now();
191
+
192
+ const tempDir = join(
193
+ baseDir,
194
+ `.tmp-${resolvedVersion}-${Math.random().toString(36).slice(2)}`,
195
+ );
154
196
  await mkdir(tempDir, { recursive: true });
155
197
  try {
156
198
  await pacote.extract(`${packageName}@${resolvedVersion}`, tempDir, {
@@ -175,6 +217,9 @@ export async function fetchPackage(
175
217
  throw err;
176
218
  }
177
219
  }
220
+ console.log(
221
+ `[Fetcher] ${packageName}@${resolvedVersion}: Downloaded and extracted in ${Date.now() - downloadStart}ms`,
222
+ );
178
223
  } catch (err) {
179
224
  await rm(tempDir, { recursive: true, force: true }).catch(() => {});
180
225
  throw err;
@@ -182,12 +227,23 @@ export async function fetchPackage(
182
227
  }
183
228
 
184
229
  // 2. Resolve entry point from package.json for downloaded package
230
+ const entryStart = Date.now();
185
231
  const entryPoint = await resolveEntryPoint(packageDir, packageName);
232
+ console.log(
233
+ `[Fetcher] ${packageName}@${resolvedVersion}: Resolved entry point in ${Date.now() - entryStart}ms`,
234
+ );
186
235
 
187
236
  if (options?.installDeps) {
237
+ const depsStart = Date.now();
188
238
  await installDependencies(packageDir, packageName, options);
239
+ console.log(
240
+ `[Fetcher] ${packageName}@${resolvedVersion}: Installed dependencies in ${Date.now() - depsStart}ms`,
241
+ );
189
242
  }
190
243
 
244
+ console.log(
245
+ `[Fetcher] ${packageName}@${resolvedVersion}: FetchPackage complete in ${Date.now() - start}ms`,
246
+ );
191
247
  return { packageDir, resolvedVersion, entryPoint };
192
248
  });
193
249
  }
@@ -245,11 +301,15 @@ export async function runPnpm(
245
301
  * Installs a specific version of Node.js if not already present.
246
302
  */
247
303
  export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NODE_VERSION) {
304
+ const checkStart = Date.now();
248
305
  const isWindows = process.platform === "win32";
249
306
  const nodeDir = context.getThirdPartyPath("node", version);
250
307
  const finalNodePath = join(nodeDir, isWindows ? "node.exe" : "bin/node");
251
308
 
252
309
  if (isNodeJSComplete(finalNodePath)) {
310
+ console.log(
311
+ `[Environment] Node.js check took ${Date.now() - checkStart}ms (found at ${finalNodePath})`,
312
+ );
253
313
  return finalNodePath;
254
314
  }
255
315
 
@@ -269,10 +329,13 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
269
329
 
270
330
  sendStartupProgress(`Downloading Node.js v${version}...`);
271
331
  console.log(`Downloading Node.js from ${downloadUrl}...`);
332
+ const dlStart = Date.now();
272
333
  await downloadFile(downloadUrl, archivePath);
334
+ console.log(`[Environment] Node.js download took ${Date.now() - dlStart}ms`);
273
335
 
274
336
  sendStartupProgress(`Extracting Node.js v${version}...`);
275
337
  console.log(`Extracting Node.js to ${tempDir}...`);
338
+ const extStart = Date.now();
276
339
  const extractTempDir = join(tempDir, "extracted");
277
340
  await mkdir(extractTempDir, { recursive: true });
278
341
 
@@ -290,7 +353,10 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
290
353
  const parentDir = dirname(nodeDir);
291
354
  await mkdir(parentDir, { recursive: true });
292
355
 
293
- const tempNodeDir = join(parentDir, `.tmp-node-${version}-${Math.random().toString(36).slice(2)}`);
356
+ const tempNodeDir = join(
357
+ parentDir,
358
+ `.tmp-node-${version}-${Math.random().toString(36).slice(2)}`,
359
+ );
294
360
  await mkdir(tempNodeDir, { recursive: true });
295
361
 
296
362
  try {
@@ -313,11 +379,13 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
313
379
  throw err;
314
380
  }
315
381
  }
382
+ console.log(`[Environment] Node.js extraction took ${Date.now() - extStart}ms`);
316
383
  } finally {
317
384
  await rm(tempNodeDir, { recursive: true, force: true }).catch(() => {});
318
385
  await rm(tempDir, { recursive: true, force: true }).catch(() => {});
319
386
  }
320
387
 
388
+ console.log(`[Environment] Node.js set up complete in ${Date.now() - checkStart}ms`);
321
389
  return finalNodePath;
322
390
  });
323
391
  }
@@ -326,10 +394,14 @@ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NO
326
394
  * Installs the PNPM package from npm if not already present.
327
395
  */
328
396
  export async function ensurePNPM(context: PipelabContext, version = DEFAULT_PNPM_VERSION) {
397
+ const checkStart = Date.now();
329
398
  const pnpmDir = context.getPackagesPath("pnpm", version);
330
399
  const pnpmPath = join(pnpmDir, "bin", "pnpm.cjs");
331
400
 
332
401
  if (existsSync(pnpmPath)) {
402
+ console.log(
403
+ `[Environment] PNPM check took ${Date.now() - checkStart}ms (found at ${pnpmPath})`,
404
+ );
333
405
  return pnpmPath;
334
406
  }
335
407
 
@@ -340,11 +412,13 @@ export async function ensurePNPM(context: PipelabContext, version = DEFAULT_PNPM
340
412
  const { packageDir } = await fetchPackage("pnpm", version, {
341
413
  context,
342
414
  });
415
+ console.log(`[Environment] PNPM set up complete in ${Date.now() - checkStart}ms`);
343
416
  return join(packageDir, "bin", "pnpm.cjs");
344
417
  });
345
418
  }
346
419
 
347
420
  async function installDependencies(packageDir: string, packageName: string, options: FetchOptions) {
421
+ const start = Date.now();
348
422
  const nodeModulesPath = join(packageDir, "node_modules");
349
423
 
350
424
  if (isDependenciesInstalledSync(packageDir)) {
@@ -359,12 +433,14 @@ async function installDependencies(packageDir: string, packageName: string, opti
359
433
  try {
360
434
  // Copy package.json to tempDir so pnpm can install dependencies
361
435
  await cp(join(packageDir, "package.json"), join(tempDir, "package.json"));
362
-
436
+
363
437
  console.log(`[Fetcher] ${packageName}: Ensuring dependencies are installed...`);
438
+ const pnpmStart = Date.now();
364
439
  const { all } = await runPnpm(tempDir, {
365
440
  signal: options.signal,
366
441
  context: options.context,
367
442
  });
443
+ console.log(`[Fetcher] ${packageName}: pnpm install command took ${Date.now() - pnpmStart}ms`);
368
444
 
369
445
  if (all) console.log(`[Fetcher] ${packageName}: Installation trace:\n${all}`);
370
446
 
@@ -373,8 +449,11 @@ async function installDependencies(packageDir: string, packageName: string, opti
373
449
  if (existsSync(nodeModulesPath)) {
374
450
  await rm(nodeModulesPath, { recursive: true, force: true }).catch(() => {});
375
451
  }
452
+ const renameStart = Date.now();
376
453
  await rename(tempNodeModules, nodeModulesPath);
377
- console.log(`[Fetcher] ${packageName}: Dependencies installed successfully.`);
454
+ console.log(
455
+ `[Fetcher] ${packageName}: Dependencies installed successfully (rename took ${Date.now() - renameStart}ms, total installDependencies took ${Date.now() - start}ms).`,
456
+ );
378
457
  } catch (err: any) {
379
458
  console.error(
380
459
  `[Fetcher] ${packageName}: CRITICAL ERROR during dependency installation: ${err.message}`,
@@ -527,30 +606,42 @@ async function crawlMonorepoPackages(): Promise<Record<string, string>> {
527
606
  return cache;
528
607
  }
529
608
 
530
- async function findLatestLocalVersion(baseDir: string): Promise<string | null> {
609
+ async function tryLocalFallback(
610
+ versionOrRange: string | undefined,
611
+ _error: unknown,
612
+ baseDir: string,
613
+ logPrefix: string,
614
+ ): Promise<string | null> {
531
615
  if (!existsSync(baseDir)) return null;
532
616
  try {
533
617
  const entries = await readdir(baseDir, { withFileTypes: true });
534
- const versions = entries
618
+ const localVersions = entries
535
619
  .filter((e) => e.isDirectory() || e.isSymbolicLink())
536
620
  .map((e) => e.name)
537
- .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
538
- return versions[0] || null;
539
- } catch {
540
- return null;
541
- }
542
- }
621
+ .filter((name) => !!semver.valid(name));
543
622
 
544
- async function tryLocalFallback(
545
- _version: string | undefined,
546
- _error: unknown,
547
- baseDir: string,
548
- logPrefix: string,
549
- ): Promise<string | null> {
550
- const latestLocal = await findLatestLocalVersion(baseDir);
551
- if (latestLocal) {
552
- console.info(`[Fetcher] ${logPrefix}: Using locally cached version: ${latestLocal}`);
553
- return latestLocal;
623
+ if (localVersions.length === 0) return null;
624
+
625
+ const range = versionOrRange || "latest";
626
+
627
+ if (range === "latest") {
628
+ const sorted = localVersions.sort((a, b) => semver.rcompare(a, b));
629
+ const latestLocal = sorted[0] || null;
630
+ if (latestLocal) {
631
+ console.info(`[Fetcher] ${logPrefix}: Using locally cached latest version: ${latestLocal}`);
632
+ return latestLocal;
633
+ }
634
+ } else {
635
+ const matched = semver.maxSatisfying(localVersions, range);
636
+ if (matched) {
637
+ console.info(
638
+ `[Fetcher] ${logPrefix}: Using locally cached matching version: ${matched} for range ${range}`,
639
+ );
640
+ return matched;
641
+ }
642
+ }
643
+ } catch (e) {
644
+ console.warn(`[Fetcher] ${logPrefix}: Error during local fallback resolution:`, e);
554
645
  }
555
646
  return null;
556
647
  }
package/src/utils.ts CHANGED
@@ -1,24 +1,27 @@
1
1
  import { nanoid } from "nanoid";
2
- import { usePlugins } from "@pipelab/shared";
3
- import { RendererPluginDefinition } from "@pipelab/shared";
2
+ import {
3
+ usePlugins,
4
+ RendererPluginDefinition,
5
+ processGraph,
6
+ useLogger,
7
+ BuildHistoryEntry,
8
+ Variable,
9
+ AppConfig,
10
+ transformUrl,
11
+ } from "@pipelab/shared";
4
12
  import { downloadFile, DownloadHooks } from "./utils/fs-extras";
5
13
  import { access, chmod, mkdir, rm, writeFile, readdir, cp } from "node:fs/promises";
6
14
  import { dirname, join } from "node:path";
7
15
  import { pathToFileURL } from "node:url";
8
16
  import { isDev, projectRoot, PipelabContext } from "./context";
9
17
  import { constants, existsSync } from "node:fs";
10
- import { processGraph } from "@pipelab/shared";
11
18
  import { handleActionExecute } from "./handler-func";
12
- import { useLogger } from "@pipelab/shared";
13
19
  import { BuildHistoryStorage } from "./handlers/build-history";
14
- import type { BuildHistoryEntry } from "@pipelab/shared";
15
- import type { Variable } from "@pipelab/shared";
16
20
 
17
21
  import { ensure, generateTempFolder, extractTarGz, extractZip, zipFolder } from "./utils/fs-extras";
18
22
  import { fetchPipelabAsset } from "./utils/remote";
19
23
  import { loadPipelabPlugin } from "./plugins-registry";
20
24
  import { setupConfigFile } from "./config";
21
- import { AppConfig } from "@pipelab/shared";
22
25
 
23
26
  export const getFinalPlugins = () => {
24
27
  const { plugins } = usePlugins();
@@ -29,13 +32,6 @@ export const getFinalPlugins = () => {
29
32
  for (const plugin of plugins.value) {
30
33
  const finalNodes = [];
31
34
 
32
- const transformUrl = (url: string) => {
33
- if (url.startsWith("file://")) {
34
- return url.replace("file://", "media://");
35
- }
36
- return url;
37
- };
38
-
39
35
  const finalIcon =
40
36
  plugin.icon?.type === "image"
41
37
  ? {
package/src/presets/if.ts DELETED
@@ -1,69 +0,0 @@
1
- // @ts-nocheck
2
- import { PresetFn, SavedFile } from "@pipelab/shared";
3
-
4
- export const ifPreset: PresetFn = async () => {
5
- const branchId = "branchId";
6
- const logOkId = "logOkId";
7
- const logKoId = "logKoId";
8
- const booleanId = "booleanId";
9
-
10
- const data: SavedFile = {
11
- version: "1.0.0",
12
- name: "Condition demo",
13
- description: "Condition demo",
14
- variables: [
15
- {
16
- type: "boolean",
17
- id: booleanId,
18
- description: "The value of the conditon",
19
- name: "Value",
20
- value: true,
21
- },
22
- ],
23
- canvas: {
24
- blocks: [
25
- {
26
- type: "condition",
27
- origin: {
28
- pluginId: "system",
29
- nodeId: "branch",
30
- },
31
- uid: branchId,
32
- params: {
33
- condition: "",
34
- },
35
- branchTrue: [
36
- {
37
- type: "action",
38
- origin: {
39
- pluginId: "system",
40
- nodeId: "log",
41
- },
42
- uid: logOkId,
43
- params: {
44
- text: "OK",
45
- },
46
- },
47
- ],
48
- branchFalse: [
49
- {
50
- type: "action",
51
- origin: {
52
- pluginId: "system",
53
- nodeId: "log",
54
- },
55
- uid: logKoId,
56
- params: {
57
- text: "KO",
58
- },
59
- },
60
- ],
61
- },
62
- ],
63
- },
64
- };
65
-
66
- return {
67
- data,
68
- };
69
- };