@pipelab/core-node 1.0.0-beta.0

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 (48) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +110 -0
  3. package/README.md +10 -0
  4. package/dist/index.d.mts +416 -0
  5. package/dist/index.d.mts.map +1 -0
  6. package/dist/index.mjs +52612 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +64 -0
  9. package/src/api.ts +115 -0
  10. package/src/config.ts +105 -0
  11. package/src/context.ts +68 -0
  12. package/src/handler-func.ts +234 -0
  13. package/src/handlers/agents.ts +32 -0
  14. package/src/handlers/auth.ts +95 -0
  15. package/src/handlers/build-history.ts +381 -0
  16. package/src/handlers/config.ts +109 -0
  17. package/src/handlers/engine.ts +229 -0
  18. package/src/handlers/fs.ts +97 -0
  19. package/src/handlers/history.ts +329 -0
  20. package/src/handlers/index.ts +41 -0
  21. package/src/handlers/shell.ts +57 -0
  22. package/src/handlers/system.ts +18 -0
  23. package/src/handlers.ts +2 -0
  24. package/src/heavy.ts +4 -0
  25. package/src/index.ts +16 -0
  26. package/src/ipc-core.ts +77 -0
  27. package/src/migrations.ts +72 -0
  28. package/src/paths.ts +1 -0
  29. package/src/plugins-registry.ts +78 -0
  30. package/src/presets/c3toSteam.ts +272 -0
  31. package/src/presets/demo.ts +123 -0
  32. package/src/presets/if.ts +69 -0
  33. package/src/presets/list.ts +30 -0
  34. package/src/presets/loop.ts +65 -0
  35. package/src/presets/moreToCome.ts +32 -0
  36. package/src/presets/newProject.ts +31 -0
  37. package/src/presets/preset.model.ts +0 -0
  38. package/src/presets/test-c3-offline.ts +78 -0
  39. package/src/presets/test-c3-unzip.ts +124 -0
  40. package/src/runner.ts +160 -0
  41. package/src/server.ts +99 -0
  42. package/src/types/runner.ts +101 -0
  43. package/src/utils/fs-extras.ts +211 -0
  44. package/src/utils/remote.ts +497 -0
  45. package/src/utils/storage.ts +99 -0
  46. package/src/utils.ts +268 -0
  47. package/src/websocket-server.ts +288 -0
  48. package/tsconfig.json +19 -0
@@ -0,0 +1,57 @@
1
+ import { useAPI } from "../ipc-core";
2
+ import { useLogger } from "@pipelab/shared";
3
+ import { PipelabContext } from "../context";
4
+ import { homedir } from "node:os";
5
+
6
+ export const registerShellHandlers = (_context: PipelabContext) => {
7
+ const { handle } = useAPI();
8
+ const { logger } = useLogger();
9
+
10
+ handle("dialog:showOpenDialog", async (event, { value, send }) => {
11
+ logger().info("value", value);
12
+ logger().info("dialog:showOpenDialog");
13
+
14
+ // Since we are in a standalone server, we cannot show Electron dialogs.
15
+ // In the future, this could be handled by the UI or a separate GUI process.
16
+ send({
17
+ type: "end",
18
+ data: {
19
+ type: "success",
20
+ result: {
21
+ filePaths: [],
22
+ canceled: true,
23
+ },
24
+ },
25
+ });
26
+ });
27
+
28
+ handle("dialog:showSaveDialog", async (event, { value, send }) => {
29
+ const { logger } = useLogger();
30
+
31
+ logger().info("value", value);
32
+ logger().info("dialog:showSaveDialog");
33
+
34
+ send({
35
+ type: "end",
36
+ data: {
37
+ type: "success",
38
+ result: {
39
+ filePath: undefined,
40
+ canceled: true,
41
+ },
42
+ },
43
+ });
44
+ });
45
+
46
+ handle("fs:getHomeDirectory", async (event, { send }) => {
47
+ send({
48
+ type: "end",
49
+ data: {
50
+ type: "success",
51
+ result: {
52
+ path: homedir(),
53
+ },
54
+ },
55
+ });
56
+ });
57
+ };
@@ -0,0 +1,18 @@
1
+ import { useAPI } from "../ipc-core";
2
+ import { PipelabContext } from "../context";
3
+
4
+ export const registerSystemHandlers = (options: { version: string; context: PipelabContext }) => {
5
+ const { handle } = useAPI();
6
+
7
+ handle("agent:version:get", async (_, { send }) => {
8
+ send({
9
+ type: "end",
10
+ data: {
11
+ type: "success",
12
+ result: {
13
+ version: options.version,
14
+ },
15
+ },
16
+ });
17
+ });
18
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./ipc-core";
2
+ export * from "./handlers/index";
package/src/heavy.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./websocket-server";
2
+ export * from "./handler-func";
3
+ export * from "./utils";
4
+ export * from "./handlers/index";
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export * from "./context";
2
+ export * from "./websocket-server";
3
+ export * from "./ipc-core";
4
+ export * from "./handlers/index";
5
+ export * from "./config";
6
+ export * from "./paths";
7
+ export * from "./api";
8
+ export * from "./heavy";
9
+ export * from "./plugins-registry";
10
+ export * from "./utils/remote";
11
+ export * from "./utils/fs-extras";
12
+ export * from "./types/runner";
13
+ export * from "./runner";
14
+ export * from "./migrations";
15
+ export * from "./server";
16
+ export * from "./utils";
@@ -0,0 +1,77 @@
1
+ import { Channels, IpcMessage } from "@pipelab/shared";
2
+ import { WebSocket as WSWebSocket } from "ws";
3
+ import { useLogger } from "@pipelab/shared";
4
+ import { WebSocketEvent, WebSocketHandler, WebSocketSendFunction } from "@pipelab/shared";
5
+
6
+ export type HandleListenerSendFn<KEY extends Channels> = WebSocketSendFunction<KEY>;
7
+
8
+ export type WsEvent = WebSocketEvent;
9
+
10
+ export type HandleListener<KEY extends Channels> = WebSocketHandler<KEY>;
11
+
12
+ const handlers: Record<string, WebSocketHandler<any>> = {};
13
+
14
+ export const useAPI = () => {
15
+ const { logger } = useLogger();
16
+
17
+ const handle = <KEY extends Channels>(channel: KEY, listener: WebSocketHandler<KEY>) => {
18
+ handlers[channel] = listener;
19
+
20
+ return {
21
+ channel,
22
+ listener,
23
+ };
24
+ };
25
+
26
+ const processWebSocketMessage = (ws: WSWebSocket, channel: string, message: IpcMessage) => {
27
+ const { data, requestId } = message;
28
+
29
+ if (handlers[channel]) {
30
+ logger().debug("Executing handler for channel:", channel, "with data:", JSON.stringify(data));
31
+ const event: WsEvent = {
32
+ sender: ws.url || "websocket-client",
33
+ };
34
+
35
+ const send: HandleListenerSendFn<any> = (events) => {
36
+ const serialized = JSON.stringify(events);
37
+ logger().debug(
38
+ "sending response to",
39
+ requestId,
40
+ ":",
41
+ serialized.length > 500 ? serialized.substring(0, 500) + "..." : serialized,
42
+ );
43
+ const response = {
44
+ type: "response",
45
+ requestId,
46
+ events,
47
+ };
48
+
49
+ if (ws.readyState === WSWebSocket.OPEN) {
50
+ ws.send(JSON.stringify(response), (error) => {
51
+ if (error) {
52
+ logger().error("Failed to send WebSocket response:", error);
53
+ }
54
+ });
55
+ } else {
56
+ logger().debug(
57
+ `Cannot send response to ${requestId}: WebSocket is not open (state: ${ws.readyState})`,
58
+ );
59
+ }
60
+ return Promise.resolve();
61
+ };
62
+
63
+ return handlers[channel](event, {
64
+ send,
65
+ value: data,
66
+ });
67
+ } else {
68
+ logger().warn("No handler found for channel:", channel);
69
+ }
70
+ };
71
+
72
+ return {
73
+ handle,
74
+ processWebSocketMessage,
75
+ handlers,
76
+ };
77
+ };
@@ -0,0 +1,72 @@
1
+ import { useAPI } from "./ipc-core";
2
+ import { PipelabContext } from "./context";
3
+ import { setupConfigFile } from "./config";
4
+ import {
5
+ savedFileMigrator,
6
+ fileRepoMigrations,
7
+ appSettingsMigrator,
8
+ useLogger,
9
+ } from "@pipelab/shared";
10
+
11
+ /**
12
+ * Registers migration handlers for the CLI/Standalone server.
13
+ * This overrides the default handlers from @pipelab/core-node to include
14
+ * pipeline and file repo migrations directly in the backend.
15
+ */
16
+ export function registerMigrationHandlers(context: PipelabContext) {
17
+ const { handle } = useAPI();
18
+ const { logger } = useLogger();
19
+
20
+ handle("config:load", async (_, { send, value }) => {
21
+ const { config: name } = value;
22
+ logger().info("[CLI Migration] config:load", name);
23
+
24
+ try {
25
+ // Determine which migrator to use
26
+ let migrator: any = null;
27
+ if (name === "projects") {
28
+ migrator = fileRepoMigrations;
29
+ } else if (name === "settings") {
30
+ migrator = appSettingsMigrator;
31
+ } else if (name.startsWith("pipeline-") || name.endsWith(".plb")) {
32
+ migrator = savedFileMigrator;
33
+ }
34
+
35
+ if (!migrator) {
36
+ throw new Error(
37
+ `No migrator found for configuration: ${name}. All files loaded via config:load MUST have a migration schema.`,
38
+ );
39
+ }
40
+
41
+ // We use a custom loading logic that applies the migrator
42
+ // Since setupConfigFile in core-node expects the migrator to be in the configRegistry,
43
+ // and we want to keep migrations in the CLI package, we'll manually apply them here.
44
+
45
+ const manager = await setupConfigFile(name, { context, migrator });
46
+ const json = await manager.getConfig();
47
+
48
+ send({
49
+ type: "end",
50
+ data: {
51
+ type: "success",
52
+ result: {
53
+ result: json,
54
+ },
55
+ },
56
+ });
57
+ } catch (e) {
58
+ logger().error(`[CLI Migration] config:load error for ${name}:`, e);
59
+ send({
60
+ type: "end",
61
+ data: {
62
+ type: "error",
63
+ ipcError: e instanceof Error ? e.message : `Unable to load config ${name}`,
64
+ },
65
+ });
66
+ }
67
+ });
68
+
69
+ // We also need to override config:save if we want to ensure consistency,
70
+ // although mostly we just care about migrations on load.
71
+ // The default config:save from core-node will work fine as it uses the same setupConfigFile logic.
72
+ }
package/src/paths.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./context";
@@ -0,0 +1,78 @@
1
+ import { ensureNodeJS, ensurePNPM, fetchPipelabPlugin } from "./utils/remote";
2
+ import { pathToFileURL } from "node:url";
3
+ import { readdir } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ import { PipelabContext } from "./context";
6
+ import { sendStartupProgress } from "./server";
7
+
8
+ const DEFAULT_PLUGIN_IDS = [
9
+ "construct",
10
+ "filesystem",
11
+ "system",
12
+ "steam",
13
+ "itch",
14
+ "electron",
15
+ "discord",
16
+ "poki",
17
+ "nvpatch",
18
+ "tauri",
19
+ "minify",
20
+ "netlify",
21
+ ];
22
+
23
+ export const loadPipelabPlugin = async (id: string, options: { context: PipelabContext }) => {
24
+ try {
25
+ const packageName = `@pipelab/plugin-${id}`;
26
+ const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, "latest", {
27
+ context: options.context,
28
+ installDeps: true,
29
+ });
30
+
31
+ console.log(`[Plugins] [${id}] Attempting to import from: ${entryPoint}`);
32
+ if (!existsSync(entryPoint)) {
33
+ console.error(`[Plugins] [${id}] CRITICAL: Plugin entry point not found at ${entryPoint}`);
34
+ try {
35
+ const files = await readdir(packageDir, { recursive: true });
36
+ console.log(`[Plugins] [${id}] Directory contents:`, files);
37
+ } catch (e) { }
38
+ }
39
+
40
+ const pluginModule = await import(pathToFileURL(entryPoint).href);
41
+ console.log(`[Plugins] [${id}] Successfully loaded from: ${packageDir}`);
42
+ return pluginModule.default;
43
+ } catch (e: any) {
44
+ console.error(`[Plugins] [${id}] CRITICAL: Failed to load:`, e);
45
+ if (e.code === "ERR_MODULE_NOT_FOUND") {
46
+ console.error(
47
+ `[Plugins] [${id}] This usually means a dependency is missing in the plugin's node_modules.`,
48
+ );
49
+ }
50
+ return null;
51
+ }
52
+ };
53
+
54
+ export const builtInPlugins = async (options: { context: PipelabContext }) => {
55
+ console.log("[Plugins] Finalizing default plugins list...");
56
+
57
+ // Pre-ensure Node.js and PNPM once in parallel so plugins don't have to wait for them
58
+ sendStartupProgress("Preparing environment...");
59
+ await Promise.all([
60
+ ensureNodeJS(options.context),
61
+ ensurePNPM(options.context),
62
+ ]);
63
+
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
+ );
70
+
71
+ const plugins = results
72
+ .filter((r) => r.status === "fulfilled")
73
+ .map((r) => (r as PromiseFulfilledResult<any>).value);
74
+
75
+ const filtered = plugins.filter(Boolean).flat();
76
+ console.log(`[Plugins] Successfully loaded ${filtered.length} default plugins`);
77
+ return filtered;
78
+ };
@@ -0,0 +1,272 @@
1
+ import { PresetFn, SavedFile } from "@pipelab/shared";
2
+
3
+ export const c3toSteamPreset: PresetFn = async () => {
4
+ const startId = "manual-start";
5
+
6
+ const data: SavedFile = {
7
+ version: "3.0.0",
8
+ name: "Construct 3 to Steam",
9
+ description: "A basic project to get you started with Construct 3 and Steam",
10
+ variables: [],
11
+ canvas: {
12
+ triggers: [
13
+ {
14
+ type: "event",
15
+ origin: {
16
+ pluginId: "system",
17
+ nodeId: "manual",
18
+ },
19
+ uid: startId,
20
+ params: {},
21
+ },
22
+ ],
23
+ blocks: [
24
+ {
25
+ uid: "3oHbOKmCXo6NSjmrn0tHG",
26
+ type: "action",
27
+ origin: {
28
+ nodeId: "export-construct-project",
29
+ pluginId: "construct",
30
+ },
31
+ params: {
32
+ file: {
33
+ editor: "simple",
34
+ value: "",
35
+ },
36
+ username: {
37
+ editor: "simple",
38
+ value: '""',
39
+ },
40
+ password: {
41
+ editor: "simple",
42
+ value: '""',
43
+ },
44
+ version: {
45
+ editor: "simple",
46
+ value: "",
47
+ },
48
+ headless: {
49
+ editor: "simple",
50
+ value: "true",
51
+ },
52
+ timeout: {
53
+ editor: "simple",
54
+ value: "120",
55
+ },
56
+ customProfile: {
57
+ editor: "simple",
58
+ value: "",
59
+ },
60
+ },
61
+ },
62
+ {
63
+ uid: "6s1uYWDi2XbRriBpvQSBS",
64
+ type: "action",
65
+ origin: {
66
+ nodeId: "unzip-file-node",
67
+ pluginId: "filesystem",
68
+ },
69
+ params: {
70
+ file: {
71
+ editor: "editor",
72
+ value: "steps['3oHbOKmCXo6NSjmrn0tHG']['outputs']['zipFile']",
73
+ },
74
+ },
75
+ },
76
+ {
77
+ uid: "cvxzwBfXPZhH1RbmmxBdG",
78
+ type: "action",
79
+ origin: {
80
+ nodeId: "electron:package:v2",
81
+ pluginId: "electron",
82
+ },
83
+ params: {
84
+ arch: {
85
+ editor: "simple",
86
+ value: "",
87
+ },
88
+ platform: {
89
+ editor: "simple",
90
+ value: "",
91
+ },
92
+ "input-folder": {
93
+ editor: "editor",
94
+ value: "steps['6s1uYWDi2XbRriBpvQSBS']['outputs']['output']",
95
+ },
96
+ name: {
97
+ editor: "simple",
98
+ value: '"Pipelab"',
99
+ },
100
+ appBundleId: {
101
+ editor: "simple",
102
+ value: '"com.pipelab.app"',
103
+ },
104
+ appCopyright: {
105
+ editor: "simple",
106
+ value: '"Copyright © 2024 Pipelab"',
107
+ },
108
+ appVersion: {
109
+ editor: "simple",
110
+ value: '"1.0.0"',
111
+ },
112
+ icon: {
113
+ editor: "simple",
114
+ value: "",
115
+ },
116
+ author: {
117
+ editor: "simple",
118
+ value: '"Pipelab"',
119
+ },
120
+ description: {
121
+ editor: "simple",
122
+ value: '"A sample application"',
123
+ },
124
+ appCategoryType: {
125
+ editor: "simple",
126
+ value: '"public.app-category.developer-tools"',
127
+ },
128
+ width: {
129
+ editor: "simple",
130
+ value: 800,
131
+ },
132
+ height: {
133
+ editor: "simple",
134
+ value: 600,
135
+ },
136
+ fullscreen: {
137
+ editor: "simple",
138
+ value: "false",
139
+ },
140
+ frame: {
141
+ editor: "simple",
142
+ value: "true",
143
+ },
144
+ transparent: {
145
+ editor: "simple",
146
+ value: "false",
147
+ },
148
+ toolbar: {
149
+ editor: "simple",
150
+ value: "true",
151
+ },
152
+ alwaysOnTop: {
153
+ editor: "simple",
154
+ value: "false",
155
+ },
156
+ electronVersion: {
157
+ editor: "simple",
158
+ value: '""',
159
+ },
160
+ customMainCode: {
161
+ editor: "simple",
162
+ value: "",
163
+ },
164
+ disableAsarPackaging: {
165
+ editor: "simple",
166
+ value: "true",
167
+ },
168
+ enableExtraLogging: {
169
+ editor: "simple",
170
+ value: "false",
171
+ },
172
+ clearServiceWorkerOnBoot: {
173
+ editor: "simple",
174
+ value: "false",
175
+ },
176
+ enableInProcessGPU: {
177
+ editor: "simple",
178
+ value: "false",
179
+ },
180
+ enableDisableRendererBackgrounding: {
181
+ editor: "simple",
182
+ value: "false",
183
+ },
184
+ forceHighPerformanceGpu: {
185
+ editor: "simple",
186
+ value: "false",
187
+ },
188
+ websocketApi: {
189
+ editor: "simple",
190
+ value: "[]",
191
+ },
192
+ ignore: {
193
+ editor: "simple",
194
+ value: "[\n // use 'src/app/' as starting point\n]",
195
+ },
196
+ enableSteamSupport: {
197
+ editor: "simple",
198
+ value: "true",
199
+ },
200
+ steamGameId: {
201
+ editor: "simple",
202
+ value: "480",
203
+ },
204
+ },
205
+ },
206
+ {
207
+ uid: "1pJrNARroFnuY9DfiC56r",
208
+ type: "action",
209
+ origin: {
210
+ nodeId: "steam-upload",
211
+ pluginId: "steam",
212
+ },
213
+ params: {
214
+ sdk: {
215
+ editor: "simple",
216
+ value: "",
217
+ },
218
+ username: {
219
+ editor: "simple",
220
+ value: "",
221
+ },
222
+ appId: {
223
+ editor: "simple",
224
+ value: "",
225
+ },
226
+ depotId: {
227
+ editor: "simple",
228
+ value: "",
229
+ },
230
+ description: {
231
+ editor: "simple",
232
+ value: "",
233
+ },
234
+ folder: {
235
+ editor: "editor",
236
+ value: "steps['cvxzwBfXPZhH1RbmmxBdG']['outputs']['output']",
237
+ },
238
+ enableDRM: {
239
+ editor: "simple",
240
+ value: "false",
241
+ },
242
+ binaryToPatch: {
243
+ editor: "simple",
244
+ value: "",
245
+ },
246
+ },
247
+ disabled: false,
248
+ },
249
+ {
250
+ uid: "gnTsA6oO49ySfetxUf-0K",
251
+ type: "action",
252
+ origin: {
253
+ nodeId: "fs:open-in-explorer",
254
+ pluginId: "filesystem",
255
+ },
256
+ params: {
257
+ path: {
258
+ editor: "editor",
259
+ value: "steps['cvxzwBfXPZhH1RbmmxBdG']['outputs']['output']",
260
+ },
261
+ },
262
+ disabled: false,
263
+ },
264
+ ],
265
+ },
266
+ };
267
+
268
+ return {
269
+ data,
270
+ hightlight: true,
271
+ };
272
+ };