@pipelab/core-node 1.0.0-beta.11 → 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 [];
@@ -112,7 +112,12 @@ export async function fetchPackage(
112
112
  const baseDir = ctx.getPackagesPath(packageName);
113
113
  let resolvedVersion: string;
114
114
 
115
- 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"}...`);
116
121
  const resolveStart = Date.now();
117
122
 
118
123
  try {
@@ -126,7 +131,7 @@ export async function fetchPackage(
126
131
 
127
132
  const packument = await packumentPromise;
128
133
  const versions = Object.keys(packument.versions);
129
- const range = versionOrRange || "latest";
134
+ const range = resolvedVersionOrRange || "latest";
130
135
 
131
136
  // Prioritize tags (like 'latest', 'beta', etc.) over semver ranges
132
137
  const foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
@@ -145,7 +150,12 @@ export async function fetchPackage(
145
150
  `[Fetcher] ${packageName}: remote resolution failed (${Date.now() - resolveStart}ms), trying local fallback...`,
146
151
  );
147
152
  const fallbackStart = Date.now();
148
- const fallbackVersion = await tryLocalFallback(versionOrRange, error, baseDir, packageName);
153
+ const fallbackVersion = await tryLocalFallback(
154
+ resolvedVersionOrRange,
155
+ error,
156
+ baseDir,
157
+ packageName,
158
+ );
149
159
  if (fallbackVersion) {
150
160
  resolvedVersion = fallbackVersion;
151
161
  console.log(
@@ -596,30 +606,42 @@ async function crawlMonorepoPackages(): Promise<Record<string, string>> {
596
606
  return cache;
597
607
  }
598
608
 
599
- 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> {
600
615
  if (!existsSync(baseDir)) return null;
601
616
  try {
602
617
  const entries = await readdir(baseDir, { withFileTypes: true });
603
- const versions = entries
618
+ const localVersions = entries
604
619
  .filter((e) => e.isDirectory() || e.isSymbolicLink())
605
620
  .map((e) => e.name)
606
- .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: "base" }));
607
- return versions[0] || null;
608
- } catch {
609
- return null;
610
- }
611
- }
621
+ .filter((name) => !!semver.valid(name));
612
622
 
613
- async function tryLocalFallback(
614
- _version: string | undefined,
615
- _error: unknown,
616
- baseDir: string,
617
- logPrefix: string,
618
- ): Promise<string | null> {
619
- const latestLocal = await findLatestLocalVersion(baseDir);
620
- if (latestLocal) {
621
- console.info(`[Fetcher] ${logPrefix}: Using locally cached version: ${latestLocal}`);
622
- 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);
623
645
  }
624
646
  return null;
625
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
- };
@@ -1,65 +0,0 @@
1
- // @ts-nocheck
2
-
3
- import { PresetFn, SavedFile } from "@pipelab/shared";
4
-
5
- export const loopPreset: PresetFn = async () => {
6
- const forId = "forId";
7
- const arrayId = "arrayId";
8
-
9
- const logStepId = "logStepId";
10
- const logExitId = "logExitId";
11
-
12
- const data: SavedFile = {
13
- version: "1.0.0",
14
- name: "Loop demo",
15
- description: "Loop demo",
16
- variables: [
17
- {
18
- id: arrayId,
19
- description: "An array",
20
- name: "Array",
21
- type: "array",
22
- of: "string",
23
- value: [],
24
- },
25
- ],
26
- canvas: {
27
- blocks: [
28
- {
29
- type: "loop",
30
- origin: {
31
- pluginId: "system",
32
- nodeId: "for",
33
- },
34
-
35
- params: {},
36
- children: [
37
- {
38
- type: "action",
39
- origin: {
40
- pluginId: "system",
41
- nodeId: "log",
42
- },
43
- params: {},
44
- uid: logStepId,
45
- },
46
- ],
47
- uid: forId,
48
- },
49
- {
50
- type: "action",
51
- origin: {
52
- pluginId: "system",
53
- nodeId: "log",
54
- },
55
- params: {},
56
- uid: logExitId,
57
- },
58
- ],
59
- },
60
- };
61
-
62
- return {
63
- data,
64
- };
65
- };