@pipelab/core-node 1.0.1-beta.23

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 (50) hide show
  1. package/.turbo/turbo-build.log +75 -0
  2. package/CHANGELOG.md +291 -0
  3. package/LICENSE +110 -0
  4. package/LICENSE.md +110 -0
  5. package/README.md +10 -0
  6. package/dist/index.d.mts +344 -0
  7. package/dist/index.d.mts.map +1 -0
  8. package/dist/index.mjs +51852 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/package.json +62 -0
  11. package/src/api.ts +115 -0
  12. package/src/config.ts +105 -0
  13. package/src/context.ts +53 -0
  14. package/src/handler-func.ts +234 -0
  15. package/src/handlers/agents.ts +32 -0
  16. package/src/handlers/auth.ts +95 -0
  17. package/src/handlers/build-history.ts +360 -0
  18. package/src/handlers/config.ts +109 -0
  19. package/src/handlers/engine.ts +229 -0
  20. package/src/handlers/fs.ts +97 -0
  21. package/src/handlers/history.ts +299 -0
  22. package/src/handlers/index.ts +41 -0
  23. package/src/handlers/shell.ts +57 -0
  24. package/src/handlers/system.ts +18 -0
  25. package/src/handlers.ts +2 -0
  26. package/src/heavy.ts +4 -0
  27. package/src/index.ts +16 -0
  28. package/src/ipc-core.ts +70 -0
  29. package/src/migrations.ts +72 -0
  30. package/src/paths.ts +1 -0
  31. package/src/plugins-registry.ts +62 -0
  32. package/src/presets/c3toSteam.ts +272 -0
  33. package/src/presets/demo.ts +123 -0
  34. package/src/presets/if.ts +69 -0
  35. package/src/presets/list.ts +30 -0
  36. package/src/presets/loop.ts +65 -0
  37. package/src/presets/moreToCome.ts +32 -0
  38. package/src/presets/newProject.ts +31 -0
  39. package/src/presets/preset.model.ts +0 -0
  40. package/src/presets/test-c3-offline.ts +78 -0
  41. package/src/presets/test-c3-unzip.ts +124 -0
  42. package/src/runner.ts +107 -0
  43. package/src/server.ts +80 -0
  44. package/src/types/runner.ts +101 -0
  45. package/src/utils/fs-extras.ts +182 -0
  46. package/src/utils/remote.ts +381 -0
  47. package/src/utils/storage.ts +99 -0
  48. package/src/utils.ts +258 -0
  49. package/src/websocket-server.ts +288 -0
  50. package/tsconfig.json +19 -0
package/src/utils.ts ADDED
@@ -0,0 +1,258 @@
1
+ import { nanoid } from "nanoid";
2
+ import { usePlugins } from "@pipelab/shared";
3
+ import { RendererPluginDefinition } from "@pipelab/shared";
4
+ import { downloadFile, DownloadHooks } from "./utils/fs-extras";
5
+ import { access, chmod, mkdir, rm, writeFile, readdir, cp } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { isDev, projectRoot, PipelabContext } from "./context";
9
+ import { constants, existsSync } from "node:fs";
10
+ import { processGraph } from "@pipelab/shared";
11
+ import { handleActionExecute } from "./handler-func";
12
+ import { useLogger } from "@pipelab/shared";
13
+ import { BuildHistoryStorage } from "./handlers/build-history";
14
+ import type { BuildHistoryEntry } from "@pipelab/shared";
15
+ import type { Variable } from "@pipelab/shared";
16
+
17
+ import { ensure, generateTempFolder, extractTarGz, extractZip, zipFolder } from "./utils/fs-extras";
18
+ import { fetchPipelabAsset, fetchPipelabPlugin } from "./utils/remote";
19
+ import { setupConfigFile } from "./config";
20
+ import { AppConfig } from "@pipelab/shared";
21
+
22
+ export const getFinalPlugins = () => {
23
+ const { plugins } = usePlugins();
24
+ // console.log('plugins.value', plugins.value)
25
+
26
+ const finalPlugins: RendererPluginDefinition[] = [];
27
+
28
+ for (const plugin of plugins.value) {
29
+ const finalNodes = [];
30
+
31
+ const transformUrl = (url: string) => {
32
+ if (url.startsWith("file://")) {
33
+ return url.replace("file://", "media://");
34
+ }
35
+ return url;
36
+ };
37
+
38
+ const finalIcon =
39
+ plugin.icon?.type === "image"
40
+ ? {
41
+ ...plugin.icon,
42
+ image: transformUrl(plugin.icon.image),
43
+ }
44
+ : plugin.icon;
45
+
46
+ for (const nodeDef of plugin.nodes) {
47
+ const node = nodeDef.node;
48
+ finalNodes.push({
49
+ ...nodeDef,
50
+ node: {
51
+ ...node,
52
+ icon: transformUrl(node.icon),
53
+ },
54
+ });
55
+ }
56
+
57
+ finalPlugins.push({
58
+ ...plugin,
59
+ icon: finalIcon,
60
+ nodes: finalNodes,
61
+ });
62
+ }
63
+
64
+ return finalPlugins;
65
+ };
66
+
67
+ import { ensureNodeJS, ensurePNPM } from "./utils/remote";
68
+
69
+ export const executeGraphWithHistory = async ({
70
+ graph,
71
+ variables,
72
+ projectName,
73
+ projectPath,
74
+ pipelineId,
75
+ onNodeEnter,
76
+ onNodeExit,
77
+ onLog,
78
+ abortSignal,
79
+ mainWindow,
80
+ cachePath,
81
+ context,
82
+ }: {
83
+ graph: any;
84
+ variables: Variable[];
85
+ projectName: string;
86
+ projectPath: string;
87
+ pipelineId: string;
88
+ onNodeEnter?: (node: any) => void;
89
+ onNodeExit?: (node: any) => void;
90
+ onLog?: (data: any, node?: any) => void;
91
+ abortSignal: AbortSignal;
92
+ mainWindow?: any;
93
+ cachePath: string;
94
+ context: PipelabContext;
95
+ }) => {
96
+ const ctx = context;
97
+ const buildHistoryStorage = new BuildHistoryStorage(ctx);
98
+ const buildId = nanoid();
99
+ const startTime = Date.now();
100
+
101
+ // Save initial build history entry
102
+ const now = Date.now();
103
+ const initialEntry: BuildHistoryEntry = {
104
+ id: buildId,
105
+ projectName,
106
+ projectPath,
107
+ pipelineId,
108
+ startTime,
109
+ status: "running",
110
+ logs: [],
111
+ steps: [],
112
+ totalSteps: graph.length,
113
+ completedSteps: 0,
114
+ failedSteps: 0,
115
+ cancelledSteps: 0,
116
+ createdAt: now,
117
+ updatedAt: now,
118
+ };
119
+ const shouldDisableHistory = process.env.PIPELAB_DISABLE_HISTORY === "true";
120
+
121
+ if (!shouldDisableHistory) {
122
+ await buildHistoryStorage.save(initialEntry);
123
+ }
124
+
125
+ const logs: any[] = [];
126
+ const sandboxPath = await generateTempFolder(cachePath);
127
+ const { logger } = useLogger();
128
+
129
+ // Ensure all plugins used in the graph are downloaded and registered
130
+ const { registerPlugins, plugins: registeredPlugins } = usePlugins();
131
+ const pluginIds = new Set(
132
+ graph.map((node: any) => node.origin?.pluginId).filter(Boolean),
133
+ ) as Set<string>;
134
+
135
+ for (const pluginId of pluginIds) {
136
+ const isRegistered = registeredPlugins.value.some((p) => p.id === pluginId);
137
+ if (!isRegistered) {
138
+ logger().info(`[Runner] Plugin "${pluginId}" not found, attempting to load...`);
139
+ try {
140
+ const packageName = `@pipelab/plugin-${pluginId}`;
141
+
142
+ const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, "latest", {
143
+ context: ctx,
144
+ });
145
+ const pluginModule = await import(pathToFileURL(entryPoint).href);
146
+ const pluginDefinition = pluginModule.default;
147
+
148
+ registerPlugins([pluginDefinition]);
149
+ logger().info(`[Runner] Plugin "${pluginId}" loaded and registered successfully`);
150
+ } catch (e) {
151
+ logger().error(`[Runner] Failed to load or register plugin "${pluginId}":`, e);
152
+ }
153
+ }
154
+ }
155
+
156
+ logger().info(`[Sandbox] Execution sandbox created at: ${sandboxPath}`);
157
+ const settingsFile = await setupConfigFile<AppConfig>("settings", { context: ctx });
158
+ const config = await settingsFile.getConfig();
159
+ const shouldCleanup = config?.clearTemporaryFoldersOnPipelineEnd ?? true;
160
+
161
+ try {
162
+ const result = await processGraph({
163
+ graph,
164
+ definitions: getFinalPlugins(),
165
+ variables,
166
+ steps: {},
167
+ context: {},
168
+ onExecuteItem: async (node, params, steps) => {
169
+ if (node.type === "action") {
170
+ return await handleActionExecute(
171
+ node.origin.nodeId,
172
+ node.origin.pluginId,
173
+ params,
174
+ mainWindow,
175
+ async (data) => {
176
+ if (data.type === "log") {
177
+ const logEntry = {
178
+ type: data.type,
179
+ message: data.data.message,
180
+ timestamp: data.data.time,
181
+ nodeUid: node?.uid,
182
+ };
183
+ logs.push(logEntry);
184
+ onLog?.(data, node);
185
+ }
186
+ },
187
+ abortSignal,
188
+ sandboxPath,
189
+ cachePath,
190
+ ctx,
191
+ );
192
+ }
193
+ throw new Error(`Execution of node type ${node.type} not implemented in utils.ts`);
194
+ },
195
+ onNodeEnter: (node) => {
196
+ onNodeEnter?.(node);
197
+ },
198
+ onNodeExit: (node) => {
199
+ onNodeExit?.(node);
200
+ },
201
+ abortSignal,
202
+ });
203
+
204
+ const endTime = Date.now();
205
+ if (!shouldDisableHistory) {
206
+ await buildHistoryStorage.update(
207
+ buildId,
208
+ {
209
+ status: "completed",
210
+ endTime,
211
+ duration: endTime - startTime,
212
+ output: result.steps,
213
+ logs,
214
+ },
215
+ pipelineId,
216
+ );
217
+ }
218
+
219
+ return { result, buildId };
220
+ } catch (error) {
221
+ const endTime = Date.now();
222
+ const isCanceled = error instanceof Error && error.name === "AbortError";
223
+
224
+ if (!shouldDisableHistory) {
225
+ await buildHistoryStorage.update(
226
+ buildId,
227
+ {
228
+ status: isCanceled ? "cancelled" : "failed",
229
+ endTime,
230
+ duration: endTime - startTime,
231
+ error: {
232
+ message: error instanceof Error ? error.message : String(error),
233
+ stack: error instanceof Error ? error.stack : undefined,
234
+ timestamp: endTime,
235
+ },
236
+ logs,
237
+ },
238
+ pipelineId,
239
+ );
240
+ }
241
+
242
+ throw error;
243
+ } finally {
244
+ // Apply retention policy regardless of outcome
245
+ if (!shouldDisableHistory) {
246
+ // Don't await, let it run in the background
247
+ buildHistoryStorage.applyRetentionPolicy();
248
+ }
249
+
250
+ if (shouldCleanup) {
251
+ try {
252
+ await rm(sandboxPath, { recursive: true, force: true });
253
+ } catch (e) {
254
+ console.warn(`Failed to cleanup sandbox at ${sandboxPath}:`, e);
255
+ }
256
+ }
257
+ }
258
+ };
@@ -0,0 +1,288 @@
1
+ import { WebSocketServer as WSWebSocketServer, WebSocket as WSWebSocket } from "ws";
2
+ import http, { IncomingMessage } from "node:http";
3
+ import { nanoid } from "nanoid";
4
+ import { useAPI } from "./ipc-core";
5
+ import { useLogger } from "@pipelab/shared";
6
+ import {
7
+ WebSocketServerConfig,
8
+ WebSocketServerEvents,
9
+ WebSocketConnectionState,
10
+ WebSocketError,
11
+ WebSocketMessage,
12
+ isWebSocketRequestMessage,
13
+ Agent,
14
+ Channels,
15
+ Events,
16
+ } from "@pipelab/shared";
17
+ import { websocketPort } from "@pipelab/constants";
18
+
19
+ export interface ConnectedClient {
20
+ id: string;
21
+ name: string;
22
+ ws: WSWebSocket;
23
+ connectedAt: number;
24
+ }
25
+
26
+ export class WebSocketServer {
27
+ private wss: WSWebSocketServer | null = null;
28
+ private server: import("http").Server | null = null;
29
+ private isReady = false;
30
+ private readyResolve: (() => void) | null = null;
31
+ private connectionState: WebSocketConnectionState = "disconnected";
32
+ private clients: Map<WSWebSocket, ConnectedClient> = new Map();
33
+ private lastBroadcasts: Map<Channels, string> = new Map();
34
+
35
+ async start(port: number = websocketPort, existingServer?: import("http").Server): Promise<void> {
36
+ const { logger } = useLogger();
37
+
38
+ return new Promise((resolve, reject) => {
39
+ try {
40
+ logger().info("Starting WebSocket server on port", port);
41
+
42
+ // Create HTTP server for WebSocket if not provided
43
+ const server = existingServer || http.createServer();
44
+ this.server = server;
45
+
46
+ // Create WebSocket server
47
+ this.wss = new WSWebSocketServer({ server });
48
+
49
+ this.wss.on("connection", (ws: WSWebSocket, request: IncomingMessage) => {
50
+ const clientId = nanoid();
51
+ const clientName = `Agent ${clientId.substring(0, 4)}`;
52
+ const connectedAt = Date.now();
53
+
54
+ const client: ConnectedClient = {
55
+ id: clientId,
56
+ name: clientName,
57
+ ws,
58
+ connectedAt,
59
+ };
60
+
61
+ this.clients.set(ws, client);
62
+ console.log("WebSocket client connected", {
63
+ id: clientId,
64
+ name: clientName,
65
+ url: request.url,
66
+ });
67
+
68
+ // Replay last known state for each channel that has been broadcasted
69
+ for (const messageStr of this.lastBroadcasts.values()) {
70
+ try {
71
+ ws.send(messageStr);
72
+ } catch (error) {
73
+ logger().error("[WebSocket] Failed to replay sticky event to new client:", error);
74
+ }
75
+ }
76
+
77
+ ws.on("message", (data: Buffer) => {
78
+ try {
79
+ const message = JSON.parse(data.toString());
80
+ logger().debug("Received WebSocket message:", message);
81
+ this.handleWebSocketMessage(ws, message);
82
+ } catch (error) {
83
+ logger().error("Failed to parse WebSocket message:", error);
84
+ this.sendError(ws, undefined, "Invalid message format");
85
+ }
86
+ });
87
+
88
+ ws.on("close", (code: number, reason: Buffer) => {
89
+ const client = this.clients.get(ws);
90
+ if (client) {
91
+ logger().info("WebSocket client disconnected", {
92
+ id: client.id,
93
+ name: client.name,
94
+ code,
95
+ reason: reason.toString(),
96
+ });
97
+ this.clients.delete(ws);
98
+ }
99
+ });
100
+
101
+ ws.on("error", (error: Error) => {
102
+ logger().error("WebSocket client error:", error);
103
+ });
104
+ });
105
+
106
+ this.wss.on("error", (error: Error) => {
107
+ logger().error("WebSocket server error:", error);
108
+ });
109
+
110
+ if (!server.listening) {
111
+ server.listen(port, () => {
112
+ this.connectionState = "connected";
113
+ logger().info(`WebSocket server listening on port ${port}`);
114
+ this.isReady = true;
115
+ if (this.readyResolve) {
116
+ this.readyResolve();
117
+ }
118
+ resolve();
119
+ });
120
+ } else {
121
+ this.connectionState = "connected";
122
+ logger().info(`WebSocket server attached to existing listening server`);
123
+ this.isReady = true;
124
+ if (this.readyResolve) {
125
+ this.readyResolve();
126
+ }
127
+ resolve();
128
+ }
129
+
130
+ server.on("error", (error: Error) => {
131
+ this.connectionState = "error";
132
+ logger().error("WebSocket server HTTP error:", error);
133
+ reject(new WebSocketError(`Server error: ${error.message}`));
134
+ });
135
+ } catch (error) {
136
+ this.connectionState = "error";
137
+ logger().error("Failed to start WebSocket server:", error);
138
+ reject(
139
+ new WebSocketError(
140
+ `Failed to start server: ${error instanceof Error ? error.message : "Unknown error"}`,
141
+ ),
142
+ );
143
+ }
144
+ });
145
+ }
146
+
147
+ private async handleWebSocketMessage(ws: WSWebSocket, message: WebSocketMessage) {
148
+ const { logger } = useLogger();
149
+ const { processWebSocketMessage } = useAPI();
150
+
151
+ try {
152
+ if (isWebSocketRequestMessage(message)) {
153
+ logger().debug("Processing WebSocket request message:", {
154
+ channel: message.channel,
155
+ requestId: message.requestId,
156
+ hasData: !!message.data,
157
+ });
158
+
159
+ logger().debug("Routing message to handler:", message.channel);
160
+ await processWebSocketMessage(ws, message.channel, message);
161
+ logger().debug("Handler processed message successfully");
162
+ } else {
163
+ logger().warn("Invalid message format received:", {
164
+ type: message.type,
165
+ requestId: message.requestId,
166
+ hasError: "error" in message,
167
+ });
168
+ this.sendError(
169
+ ws,
170
+ message.requestId,
171
+ "Invalid message format: missing channel or requestId",
172
+ );
173
+ }
174
+ } catch (error) {
175
+ logger().error("Error processing WebSocket message:", error);
176
+ this.sendError(
177
+ ws,
178
+ message.requestId,
179
+ error instanceof Error ? error.message : "Unknown error",
180
+ );
181
+ }
182
+ }
183
+
184
+ private sendError(
185
+ ws: WSWebSocket,
186
+ requestId: string | undefined,
187
+ errorMessage: string,
188
+ code?: string,
189
+ ) {
190
+ try {
191
+ const errorResponse: any = {
192
+ type: "error",
193
+ requestId,
194
+ error: errorMessage,
195
+ ...(code && { code }),
196
+ };
197
+ ws.send(JSON.stringify(errorResponse));
198
+ } catch (error) {
199
+ console.error("Failed to send error response:", error);
200
+ }
201
+ }
202
+
203
+ async stop(): Promise<void> {
204
+ const { logger } = useLogger();
205
+
206
+ return new Promise((resolve) => {
207
+ this.isReady = false;
208
+ this.connectionState = "disconnected";
209
+
210
+ if (this.wss) {
211
+ this.wss.close(() => {
212
+ logger().info("WebSocket server closed");
213
+ resolve();
214
+ });
215
+ } else {
216
+ resolve();
217
+ }
218
+
219
+ if (this.server) {
220
+ this.server.close();
221
+ }
222
+ });
223
+ }
224
+
225
+ waitForReady(): Promise<void> {
226
+ if (this.isReady) {
227
+ return Promise.resolve();
228
+ }
229
+
230
+ return new Promise((resolve) => {
231
+ this.readyResolve = resolve;
232
+ });
233
+ }
234
+
235
+ isServerReady(): boolean {
236
+ return this.isReady;
237
+ }
238
+
239
+ getConnectionState(): WebSocketConnectionState {
240
+ return this.connectionState;
241
+ }
242
+
243
+ getAgents(): Agent[] {
244
+ return Array.from(this.clients.values()).map((client) => ({
245
+ id: client.id,
246
+ name: client.name,
247
+ isSelf: false, // This will be set by the client itself when receiving the list
248
+ connectedAt: client.connectedAt,
249
+ }));
250
+ }
251
+
252
+ getClient(ws: WSWebSocket): ConnectedClient | undefined {
253
+ return this.clients.get(ws);
254
+ }
255
+
256
+ /**
257
+ * Broadcasts a message to all connected clients.
258
+ * Useful for push notifications like auth state changes.
259
+ */
260
+ broadcast<KEY extends Channels>(channel: KEY, data: Events<KEY>): void {
261
+ const { logger } = useLogger();
262
+
263
+ const message = {
264
+ type: "event",
265
+ channel,
266
+ data,
267
+ };
268
+
269
+ const messageStr = JSON.stringify(message);
270
+ this.lastBroadcasts.set(channel, messageStr);
271
+
272
+ if (!this.wss || this.clients.size === 0) return;
273
+ logger().debug(`[WebSocket] Broadcasting to all clients on channel: ${channel}`);
274
+
275
+ for (const client of this.clients.values()) {
276
+ try {
277
+ if (client.ws.readyState === WSWebSocket.OPEN) {
278
+ client.ws.send(messageStr);
279
+ }
280
+ } catch (error) {
281
+ logger().error(`[WebSocket] Failed to broadcast to client ${client.id}:`, error);
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ // Export singleton instance
288
+ export const webSocketServer = new WebSocketServer();
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../tsconfig/node.json",
3
+ "compilerOptions": {
4
+ "moduleResolution": "bundler",
5
+ "outDir": "dist",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "baseUrl": ".",
10
+ "paths": {
11
+ "@pipelab/shared": ["../shared/src"],
12
+ "@pipelab/constants": ["../constants/src/index.ts"],
13
+ "@pipelab/*": ["../shared/src/libs/*"]
14
+ }
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"],
18
+ "references": [{ "path": "../constants" }, { "path": "../shared" }]
19
+ }