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

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.2",
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.1",
44
+ "@pipelab/shared": "1.0.0-beta.1"
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.1"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsdown",
@@ -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";
@@ -25,7 +25,7 @@ export const loadPipelabPlugin = async (id: string, options: { context: PipelabC
25
25
  const packageName = `@pipelab/plugin-${id}`;
26
26
  const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, "latest", {
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,33 @@ 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(
61
+ const { usePlugins } = await import("@pipelab/shared");
62
+ const { registerPlugins } = usePlugins();
63
+ const { webSocketServer } = await import("./index");
64
+
65
+ // Load plugins asynchronously in the background
66
+ Promise.allSettled(
65
67
  DEFAULT_PLUGIN_IDS.map(async (id) => {
66
68
  sendStartupProgress(`Loading plugin: ${id}`);
67
- return loadPipelabPlugin(id, options);
69
+ const plugin = await loadPipelabPlugin(id, options);
70
+ if (plugin) {
71
+ registerPlugins([plugin]);
72
+ webSocketServer.broadcast("plugin:loaded", { plugin });
73
+ }
68
74
  }),
69
- );
70
-
71
- const plugins = results
72
- .filter((r) => r.status === "fulfilled")
73
- .map((r) => (r as PromiseFulfilledResult<any>).value);
75
+ ).then(() => {
76
+ console.log("[Plugins] All default plugins loaded.");
77
+ sendStartupProgress("All plugins loaded.");
78
+ setTimeout(() => {
79
+ webSocketServer.broadcast("startup:progress", { type: "done" });
80
+ }, 2000);
81
+ });
74
82
 
75
- const filtered = plugins.filter(Boolean).flat();
76
- console.log(`[Plugins] Successfully loaded ${filtered.length} default plugins`);
77
- return filtered;
83
+ return [];
78
84
  };
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
@@ -66,7 +66,7 @@ export async function serveCommand(options: ServeOptions, version: string, _dirn
66
66
  response.writeHead(404, { "Content-Type": "text/plain" });
67
67
  response.end(
68
68
  `Error: UI directory not found at ${rawAssetFolder}.\n` +
69
- "Please run 'pnpm build' in apps/ui to generate the distribution.",
69
+ "Please run 'pnpm build' in apps/ui to generate the distribution.",
70
70
  );
71
71
  return;
72
72
  }
@@ -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
  }
@@ -94,7 +94,9 @@ export async function fetchPackage(
94
94
  const foundVersion = packument["dist-tags"]?.[range] || semver.maxSatisfying(versions, range);
95
95
 
96
96
  if (!foundVersion) {
97
- throw new Error(`Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(packument["dist-tags"] || {}).join(", ")})`);
97
+ throw new Error(
98
+ `Package ${packageName}@${range} not found on npm (available tags: ${Object.keys(packument["dist-tags"] || {}).join(", ")})`,
99
+ );
98
100
  }
99
101
  resolvedVersion = foundVersion;
100
102
  console.log(`[Fetcher] ${packageName}: Resolved to v${resolvedVersion} via npm`);
@@ -150,7 +152,13 @@ export async function runPnpm(
150
152
  },
151
153
  ) {
152
154
  const {
153
- args = ["install", "--prod", "--no-lockfile", "--prefer-offline", "--no-verify-store-integrity"],
155
+ args = [
156
+ "install",
157
+ "--prod",
158
+ "--no-lockfile",
159
+ "--prefer-offline",
160
+ "--no-verify-store-integrity",
161
+ ],
154
162
  extraEnv = {},
155
163
  signal,
156
164
  context: ctx,
@@ -183,10 +191,7 @@ export async function runPnpm(
183
191
  /**
184
192
  * Installs a specific version of Node.js if not already present.
185
193
  */
186
- export async function ensureNodeJS(
187
- context: PipelabContext,
188
- version = DEFAULT_NODE_VERSION,
189
- ) {
194
+ export async function ensureNodeJS(context: PipelabContext, version = DEFAULT_NODE_VERSION) {
190
195
  const isWindows = process.platform === "win32";
191
196
  const nodeDir = context.getThirdPartyPath("node", version);
192
197
  const finalNodePath = join(nodeDir, isWindows ? "node.exe" : "bin/node");
@@ -234,7 +239,7 @@ export async function ensureNodeJS(
234
239
  await cp(sourceDir, nodeDir, { recursive: true });
235
240
  await rm(tempDir, { recursive: true, force: true });
236
241
 
237
- if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => { });
242
+ if (!isWindows) await chmod(finalNodePath, 0o755).catch(() => {});
238
243
  return finalNodePath;
239
244
  });
240
245
  }
@@ -242,10 +247,7 @@ export async function ensureNodeJS(
242
247
  /**
243
248
  * Installs the PNPM package from npm if not already present.
244
249
  */
245
- export async function ensurePNPM(
246
- context: PipelabContext,
247
- version = DEFAULT_PNPM_VERSION,
248
- ) {
250
+ export async function ensurePNPM(context: PipelabContext, version = DEFAULT_PNPM_VERSION) {
249
251
  const pnpmDir = context.getPackagesPath("pnpm", version);
250
252
  const pnpmPath = join(pnpmDir, "bin", "pnpm.cjs");
251
253
 
@@ -271,7 +273,12 @@ async function installDependencies(packageDir: string, packageName: string, opti
271
273
  try {
272
274
  const files = await readdir(nodeModulesPath);
273
275
  if (files.length === 0) {
274
- console.warn(`[Fetcher] ${packageName}: node_modules exists but is empty. Re-installing...`);
276
+ console.warn(
277
+ `[Fetcher] ${packageName}: node_modules exists but is empty. Re-installing...`,
278
+ );
279
+ } else {
280
+ console.log(`[Fetcher] ${packageName}: Dependencies already installed, skipping.`);
281
+ return;
275
282
  }
276
283
  } catch (e) {
277
284
  // Continue to install if readdir fails
@@ -316,17 +323,14 @@ export async function fetchPipelabPlugin(
316
323
  options: FetchOptions,
317
324
  ): Promise<{ packageDir: string; entryPoint: string; isLocal: boolean }> {
318
325
  const { packageDir, isLocal, entryPoint } = await fetchPackage(pluginName, versionOrRange, {
319
- installDeps: true,
326
+ installDeps: false,
320
327
  ...options,
321
328
  });
322
329
 
323
330
  // Default entry point if not provided by fetchPackage
324
331
  let finalEntryPoint = entryPoint;
325
332
  if (!finalEntryPoint) {
326
- const patterns = [
327
- join(packageDir, "dist", "index.mjs"),
328
- join(packageDir, "index.mjs"),
329
- ];
333
+ const patterns = [join(packageDir, "dist", "index.mjs"), join(packageDir, "index.mjs")];
330
334
  finalEntryPoint = patterns.find((p) => existsSync(p)) || patterns[0];
331
335
  }
332
336
 
@@ -346,10 +350,7 @@ export async function fetchPipelabCli(
346
350
  // Default entry point for CLI if not provided
347
351
  let finalEntryPoint = entryPoint;
348
352
  if (!finalEntryPoint) {
349
- const patterns = [
350
- join(packageDir, "dist", "index.mjs"),
351
- join(packageDir, "index.mjs"),
352
- ];
353
+ const patterns = [join(packageDir, "dist", "index.mjs"), join(packageDir, "index.mjs")];
353
354
  finalEntryPoint = patterns.find((p) => existsSync(p)) || patterns[0];
354
355
  }
355
356
 
@@ -387,7 +388,7 @@ async function tryResolveMonorepoPackage(
387
388
  typeof pkg.bin === "string"
388
389
  ? pkg.bin
389
390
  : pkg.bin?.[packageName.replace("@pipelab/", "")] ||
390
- (pkg.bin ? pkg.bin[Object.keys(pkg.bin)[0]] : undefined);
391
+ (pkg.bin ? pkg.bin[Object.keys(pkg.bin)[0]] : undefined);
391
392
 
392
393
  // In dev, we prefer the "main" field if it points to TS, or a hardcoded src/index.ts
393
394
  const tsSource = join(packageDir, "src", "index.ts");
@@ -434,11 +435,11 @@ async function crawlMonorepoPackages(): Promise<Record<string, string>> {
434
435
  if (pkg.name) {
435
436
  cache[pkg.name] = join(fullDir, entry.name);
436
437
  }
437
- } catch (e) { }
438
+ } catch (e) {}
438
439
  }
439
440
  }
440
441
  }
441
- } catch (e) { }
442
+ } catch (e) {}
442
443
  }
443
444
  return cache;
444
445
  }
package/src/utils.ts CHANGED
@@ -256,7 +256,7 @@ export const executeGraphWithHistory = async ({
256
256
  // Don't await, let it run in the background
257
257
  buildHistoryStorage.applyRetentionPolicy();
258
258
  }
259
-
259
+
260
260
  if (shouldCleanup) {
261
261
  try {
262
262
  await rm(sandboxPath, { recursive: true, force: true });