@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
package/src/runner.ts ADDED
@@ -0,0 +1,160 @@
1
+ import { executeGraphWithHistory } from "./utils";
2
+ import { setupConfigFile } from "./config";
3
+ import { isDev, PipelabContext } from "./context";
4
+ import { readFile, access, writeFile, mkdir } from "node:fs/promises";
5
+ import { resolve, isAbsolute, join, dirname } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { savedFileMigrator } from "@pipelab/shared";
8
+ import type { AppConfig } from "@pipelab/shared";
9
+ import { registerMigrationHandlers } from "./migrations";
10
+ import { registerAllHandlers } from "./handlers/index";
11
+
12
+ export interface RunOptions {
13
+ userData?: string;
14
+ variables?: string;
15
+ output?: string;
16
+ cloud?: boolean;
17
+ }
18
+
19
+ export async function runPipelineCommand(file: string, options: RunOptions, version: string) {
20
+ const pipelinePath = isAbsolute(file) ? file : resolve(process.cwd(), file);
21
+
22
+ try {
23
+ await access(pipelinePath);
24
+ } catch (e) {
25
+ throw new Error(`Pipeline file not found at ${pipelinePath}`);
26
+ }
27
+
28
+ const pipelineContent = await readFile(pipelinePath, "utf-8");
29
+ const pipeline = JSON.parse(pipelineContent);
30
+
31
+ let finalPipeline = pipeline;
32
+ if (pipeline.version) {
33
+ finalPipeline = await savedFileMigrator.migrate(pipeline);
34
+ }
35
+
36
+ const {
37
+ graph: rawGraph,
38
+ variables: pipelineVariables,
39
+ projectName,
40
+ projectPath,
41
+ pipelineId,
42
+ canvas,
43
+ name,
44
+ } = finalPipeline;
45
+
46
+ const graph = rawGraph || canvas?.blocks;
47
+
48
+ if (!graph) {
49
+ throw new Error("Pipeline does not contain a valid graph or canvas.blocks");
50
+ }
51
+
52
+ let vars = pipelineVariables || finalPipeline.variables || [];
53
+ if (options.variables) {
54
+ vars = JSON.parse(options.variables);
55
+ }
56
+
57
+ const effectiveProjectName = projectName || name || "CLI Run";
58
+ const effectiveProjectPath = projectPath || process.cwd();
59
+ const effectivePipelineId = pipelineId || "cli-run";
60
+
61
+ console.log(`Executing pipeline: ${effectiveProjectName} (${effectivePipelineId})`);
62
+
63
+ if (!options.userData) throw new Error("userDataPath is required for runPipelineCommand");
64
+ const context = new PipelabContext({
65
+ userDataPath: options.userData,
66
+ });
67
+
68
+ await registerAllHandlers({ version, context });
69
+ registerMigrationHandlers(context);
70
+
71
+ const settings = await setupConfigFile<AppConfig>("settings", { context });
72
+ const config = await settings.getConfig();
73
+ const cachePath = join(context.userDataPath, "cache");
74
+ await mkdir(cachePath, { recursive: true });
75
+
76
+ const abortController = new AbortController();
77
+
78
+ let supabase: any;
79
+ const cloudRunId = process.env.CLOUD_RUN_ID;
80
+ if (options.cloud && cloudRunId) {
81
+ const { createClient } = await import("@supabase/supabase-js");
82
+ supabase = createClient(
83
+ process.env.SUPABASE_URL!,
84
+ process.env.SUPABASE_SERVICE_ROLE_KEY!
85
+ );
86
+ console.log(`Cloud mode enabled. Streaming logs for run: ${cloudRunId}`);
87
+ }
88
+
89
+ try {
90
+ const { result, buildId } = await executeGraphWithHistory({
91
+ graph,
92
+ variables: vars,
93
+ projectName: effectiveProjectName,
94
+ projectPath: effectiveProjectPath,
95
+ pipelineId: effectivePipelineId,
96
+ cachePath: cachePath,
97
+ onNodeEnter: (node) => {
98
+ console.log(`[ENTER] ${node.name} (${node.uid})`);
99
+ 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();
106
+ }
107
+ },
108
+ onNodeExit: (node) => {
109
+ console.log(`[EXIT] ${node.name} (${node.uid})`);
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();
117
+ }
118
+ },
119
+ onLog: (data, node) => {
120
+ if (data.type === "log") {
121
+ const message = data.data.message.join(" ");
122
+ console.log(`[LOG] ${message}`);
123
+ 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();
130
+ }
131
+ }
132
+ },
133
+ abortSignal: abortController.signal,
134
+ context,
135
+ });
136
+
137
+ console.log(`Pipeline execution finished. Build ID: ${buildId}`);
138
+
139
+ if (supabase) {
140
+ await supabase.from("cloud_runs").update({ status: "success" }).eq("id", cloudRunId);
141
+ }
142
+
143
+ if (options.output) {
144
+ const outputPath = isAbsolute(options.output)
145
+ ? options.output
146
+ : resolve(process.cwd(), options.output);
147
+
148
+ await mkdir(dirname(outputPath), { recursive: true });
149
+ await writeFile(outputPath, JSON.stringify(result, null, 2), "utf-8");
150
+ console.log(`Result saved to ${outputPath}`);
151
+ }
152
+
153
+ return result;
154
+ } catch (e) {
155
+ if (supabase) {
156
+ await supabase.from("cloud_runs").update({ status: "failed" }).eq("id", cloudRunId);
157
+ }
158
+ throw e;
159
+ }
160
+ }
package/src/server.ts ADDED
@@ -0,0 +1,99 @@
1
+ import {
2
+ ensurePNPM,
3
+ PipelabContext,
4
+ isDev,
5
+ fetchPipelabAsset,
6
+ registerAllHandlers,
7
+ webSocketServer,
8
+ } from "./index";
9
+ import { getUiDevServerMissingWarning, uiDevPort } from "@pipelab/constants";
10
+ import { existsSync } from "node:fs";
11
+ import { readFile } from "node:fs/promises";
12
+ import { join } from "node:path";
13
+ import http from "http";
14
+ import handler from "serve-handler";
15
+ import { registerMigrationHandlers } from "./migrations";
16
+
17
+ export interface ServeOptions {
18
+ port: string | number;
19
+ userData?: string;
20
+ nodePath?: string;
21
+ pnpmPath?: string;
22
+ }
23
+
24
+ export const sendStartupProgress = (message: string) => {
25
+ console.log(`[Startup Progress] ${message}`);
26
+ webSocketServer.broadcast("startup:progress", {
27
+ type: "progress",
28
+ data: { message },
29
+ });
30
+ };
31
+
32
+ export const sendStartupReady = () => {
33
+ console.log(`[Startup Progress] Ready!`);
34
+ webSocketServer.broadcast("startup:progress", {
35
+ type: "ready",
36
+ });
37
+ };
38
+
39
+ export async function serveCommand(options: ServeOptions, version: string, _dirname: string) {
40
+ if (!options.userData) throw new Error("userDataPath is required for serveCommand");
41
+ const context = new PipelabContext({
42
+ userDataPath: options.userData,
43
+ });
44
+
45
+ let rawAssetFolder: string | undefined;
46
+ if (!isDev) {
47
+ rawAssetFolder = await fetchPipelabAsset("@pipelab/ui", "latest", { context });
48
+ }
49
+
50
+ const server = http.createServer(async (request, response) => {
51
+ if (isDev) {
52
+ response.writeHead(200, { "Content-Type": "text/html" });
53
+ response.end(`
54
+ <html>
55
+ <body style="font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #0f172a; color: #f8fafc;">
56
+ <h1 style="color: #38bdf8;">Pipelab Dev Mode</h1>
57
+ <p>The CLI server is running (API/WebSocket), but the UI is not served here in development.</p>
58
+ <p>Please open the UI through its own dev server (usually <a href="http://localhost:5173" style="color: #38bdf8;">http://localhost:5173</a>).</p>
59
+ </body>
60
+ </html>
61
+ `);
62
+ return;
63
+ }
64
+
65
+ if (!rawAssetFolder || !existsSync(rawAssetFolder)) {
66
+ response.writeHead(404, { "Content-Type": "text/plain" });
67
+ response.end(
68
+ `Error: UI directory not found at ${rawAssetFolder}.\n` +
69
+ "Please run 'pnpm build' in apps/ui to generate the distribution.",
70
+ );
71
+ return;
72
+ }
73
+
74
+ return handler(request, response, {
75
+ public: rawAssetFolder,
76
+ });
77
+ });
78
+
79
+ console.log(`Starting Pipelab server on port ${options.port}...`);
80
+ if (isDev) {
81
+ console.info(getUiDevServerMissingWarning());
82
+ console.log(`UI available at http://localhost:${uiDevPort}`);
83
+ } else {
84
+ console.log(`UI available at http://localhost:${options.port}`);
85
+ }
86
+
87
+ // Start the server EARLY so the UI can connect and receive progress updates
88
+ await webSocketServer.start(Number(options.port), server);
89
+
90
+ await registerAllHandlers({
91
+ version,
92
+ context,
93
+ });
94
+ registerMigrationHandlers(context);
95
+
96
+ sendStartupReady();
97
+
98
+ return server;
99
+ }
@@ -0,0 +1,101 @@
1
+ import type { BrowserWindow } from "electron";
2
+ import type { PipelabContext } from "../context";
3
+ import type {
4
+ Action,
5
+ Condition,
6
+ Loop,
7
+ Expression,
8
+ Event,
9
+ SetOutputActionFn,
10
+ SetOutputLoopFn,
11
+ SetOutputExpressionFn,
12
+ ExtractInputsFromAction,
13
+ ExtractInputsFromCondition,
14
+ ExtractInputsFromLoop,
15
+ ExtractInputsFromEvent,
16
+ ExtractInputsFromExpression,
17
+ } from "@pipelab/shared";
18
+
19
+ export type {
20
+ Action,
21
+ Condition,
22
+ Loop,
23
+ Expression,
24
+ Event,
25
+ SetOutputActionFn,
26
+ SetOutputLoopFn,
27
+ SetOutputExpressionFn,
28
+ ExtractInputsFromAction,
29
+ ExtractInputsFromCondition,
30
+ ExtractInputsFromLoop,
31
+ ExtractInputsFromEvent,
32
+ ExtractInputsFromExpression,
33
+ };
34
+
35
+ export type RunnerCallbackFnArgument = {
36
+ done: () => void;
37
+ id: string;
38
+ log: (...args: Parameters<(typeof console)["log"]>) => void;
39
+ };
40
+
41
+ export type ActionRunnerData<ACTION extends Action> = {
42
+ log: typeof console.log;
43
+ setOutput: SetOutputActionFn<ACTION>;
44
+ inputs: ExtractInputsFromAction<ACTION>;
45
+ setMeta: (callback: (data: ACTION["meta"]) => ACTION["meta"]) => void;
46
+ meta: ACTION["meta"];
47
+ cwd: string;
48
+ paths: {
49
+ cache: string;
50
+ pnpm: string;
51
+ node: string;
52
+ userData: string;
53
+ modules: string;
54
+ thirdparty: string;
55
+ };
56
+ browserWindow: BrowserWindow;
57
+ abortSignal: AbortSignal;
58
+ context: PipelabContext;
59
+ };
60
+
61
+ export type ActionRunner<ACTION extends Action> = (data: ActionRunnerData<ACTION>) => Promise<void>;
62
+
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
+ export type ExpressionRunner<EXPRESSION extends Expression> = (data: {
83
+ log: typeof console.log;
84
+ setOutput: SetOutputExpressionFn<EXPRESSION>;
85
+ inputs: ExtractInputsFromExpression<EXPRESSION>;
86
+ setMeta: (callback: (data: EXPRESSION["meta"]) => EXPRESSION["meta"]) => void;
87
+ meta: EXPRESSION["meta"];
88
+ cwd: string;
89
+ context: PipelabContext;
90
+ }) => Promise<string>;
91
+
92
+ export type EventRunner<EVENT extends Event> = (data: {
93
+ log: typeof console.log;
94
+ inputs: ExtractInputsFromEvent<EVENT>;
95
+ setMeta: (callback: (data: EVENT["meta"]) => EVENT["meta"]) => void;
96
+ meta: EVENT["meta"];
97
+ cwd: string;
98
+ context: PipelabContext;
99
+ }) => Promise<void>;
100
+
101
+ export type Runner = ActionRunner<any> | LoopRunner<any> | EventRunner<any> | ConditionRunner<any>;
@@ -0,0 +1,211 @@
1
+ import { mkdir, createWriteStream, createReadStream } from "node:fs";
2
+ import { execa, Options, Subprocess } from "execa";
3
+ import { mkdir as mkdirP, access, writeFile, realpath, mkdtemp, chmod, stat, readdir } from "node:fs/promises";
4
+ import { join, dirname } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import tar from "tar";
7
+ import yauzl from "yauzl";
8
+ import archiver from "archiver";
9
+ import { pipeline } from "node:stream/promises";
10
+
11
+ /**
12
+ * Ensures a directory exists and a file is created with default content if missing.
13
+ */
14
+ export const ensure = async (filesPath: string, defaultContent = "{}") => {
15
+ await mkdirP(dirname(filesPath), { recursive: true });
16
+ try {
17
+ await access(filesPath);
18
+ } catch {
19
+ await writeFile(filesPath, defaultContent);
20
+ }
21
+ };
22
+
23
+ /**
24
+ * Generates a unique temporary folder.
25
+ */
26
+ export const generateTempFolder = async (base?: string) => {
27
+ const targetBase = base || tmpdir();
28
+ await mkdirP(targetBase, { recursive: true });
29
+ const realPath = await realpath(targetBase);
30
+ const tempFolder = await mkdtemp(join(realPath, "pipelab-"));
31
+ return tempFolder;
32
+ };
33
+
34
+ /**
35
+ * Extracts a .tar.gz archive.
36
+ */
37
+ export async function extractTarGz(archivePath: string, destinationDir: string): Promise<void> {
38
+ await mkdirP(destinationDir, { recursive: true });
39
+ await tar.x({
40
+ file: archivePath,
41
+ cwd: destinationDir,
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Extracts a .zip archive.
47
+ */
48
+ export async function extractZip(archivePath: string, destinationDir: string): Promise<void> {
49
+ await mkdirP(destinationDir, { recursive: true });
50
+ return new Promise((resolve, reject) => {
51
+ yauzl.open(archivePath, { lazyEntries: true }, (err, zipfile) => {
52
+ if (err || !zipfile) return reject(err || new Error("Could not open zip file"));
53
+ zipfile.on("error", reject);
54
+ zipfile.readEntry();
55
+ zipfile.on("entry", (entry) => {
56
+ const entryPath = join(destinationDir, entry.fileName);
57
+ if (/\/$/.test(entry.fileName)) {
58
+ mkdirP(entryPath, { recursive: true })
59
+ .then(() => zipfile.readEntry())
60
+ .catch(reject);
61
+ } else {
62
+ mkdirP(dirname(entryPath), { recursive: true })
63
+ .then(() => {
64
+ zipfile.openReadStream(entry, (err, readStream) => {
65
+ if (err || !readStream)
66
+ return reject(err || new Error("Could not open read stream"));
67
+ readStream.on("error", reject);
68
+ const writeStream = createWriteStream(entryPath);
69
+ writeStream.on("error", reject);
70
+ writeStream.on("close", () => zipfile.readEntry());
71
+ readStream.pipe(writeStream);
72
+ });
73
+ })
74
+ .catch(reject);
75
+ }
76
+ });
77
+ zipfile.on("end", () => resolve());
78
+ });
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Zips a folder.
84
+ */
85
+ export const zipFolder = async (
86
+ from: string,
87
+ to: string,
88
+ log: typeof console.log = console.log,
89
+ ) => {
90
+ const output = createWriteStream(to);
91
+ const archive = archiver("zip", { zlib: { level: 9 } });
92
+ return new Promise<string>((resolve, reject) => {
93
+ output.on("close", () => {
94
+ log(archive.pointer() + " total bytes");
95
+ resolve(to);
96
+ });
97
+ archive.on("error", reject);
98
+ archive.pipe(output);
99
+ archive.directory(from, false);
100
+ archive.finalize();
101
+ });
102
+ };
103
+
104
+ /**
105
+ * Downloads a file with progress tracking.
106
+ */
107
+ export interface DownloadHooks {
108
+ onProgress?: (data: { progress: number; downloadedSize: number }) => void;
109
+ }
110
+
111
+ export const downloadFile = async (
112
+ url: string,
113
+ localPath: string,
114
+ hooks?: DownloadHooks,
115
+ abortSignal?: AbortSignal,
116
+ ): Promise<void> => {
117
+ const response = await fetch(url, { signal: abortSignal });
118
+ if (!response.ok) throw new Error(`Failed to fetch file: ${response.statusText}`);
119
+ const contentLength = response.headers.get("content-length");
120
+ if (!contentLength) throw new Error("Content-Length header is missing");
121
+ const totalSize = parseInt(contentLength, 10);
122
+ let downloadedSize = 0;
123
+ const fileStream = createWriteStream(localPath);
124
+ const progressStream = new TransformStream({
125
+ transform(chunk, controller) {
126
+ downloadedSize += chunk.length;
127
+ const progress = (downloadedSize / totalSize) * 100;
128
+ hooks?.onProgress?.({ progress, downloadedSize });
129
+ controller.enqueue(chunk);
130
+ },
131
+ });
132
+ const readable = response.body?.pipeThrough(progressStream);
133
+ if (!readable) throw new Error("Failed to create a readable stream");
134
+ await pipeline(readable, fileStream);
135
+ };
136
+
137
+ export const runWithLiveLogs = async (
138
+ command: string,
139
+ args: string[],
140
+ execaOptions: Options,
141
+ log: typeof console.log,
142
+ hooks?: {
143
+ onStdout?: (data: string, subprocess: Subprocess) => void;
144
+ onStderr?: (data: string, subprocess: Subprocess) => void;
145
+ onExit?: (code: number) => void;
146
+ onCreated?: (subprocess: Subprocess) => void;
147
+ },
148
+ abortSignal?: AbortSignal,
149
+ ): Promise<void> => {
150
+ const subprocess = execa(command, args, {
151
+ ...execaOptions,
152
+ stdout: "pipe",
153
+ stderr: "pipe",
154
+ stdin: "pipe",
155
+ env: {
156
+ ...process.env,
157
+ ...execaOptions.env,
158
+ TERM: "xterm-256color",
159
+ FORCE_STDERR_LOGGING: "1",
160
+ },
161
+ cancelSignal: abortSignal,
162
+ });
163
+
164
+ hooks?.onCreated?.(subprocess);
165
+
166
+ subprocess.stdout?.on("data", (data: Buffer) => {
167
+ hooks?.onStdout?.(data.toString(), subprocess);
168
+ });
169
+
170
+ subprocess.stderr?.on("data", (data: Buffer) => {
171
+ hooks?.onStderr?.(data.toString(), subprocess);
172
+ });
173
+
174
+ try {
175
+ const { exitCode } = await subprocess;
176
+ hooks?.onExit?.(exitCode ?? 0);
177
+ } catch (error: any) {
178
+ const code = error.exitCode ?? 1;
179
+ hooks?.onExit?.(code);
180
+ throw new Error(`Command failed with exit code ${code}: ${error.message}`);
181
+ }
182
+ };
183
+
184
+ /**
185
+ * Calculates the total size of a directory recursively.
186
+ */
187
+ export async function getFolderSize(dirPath: string): Promise<number> {
188
+ try {
189
+ const files = await readdir(dirPath, { withFileTypes: true });
190
+ const ArrayOfPromises = files.map(async (file) => {
191
+ const path = join(dirPath, file.name);
192
+ if (file.isDirectory()) {
193
+ try {
194
+ return await getFolderSize(path);
195
+ } catch {
196
+ return 0;
197
+ }
198
+ }
199
+ try {
200
+ const { size } = await stat(path);
201
+ return size;
202
+ } catch {
203
+ return 0;
204
+ }
205
+ });
206
+ const results = await Promise.all(ArrayOfPromises);
207
+ return results.reduce((acc, size) => acc + size, 0);
208
+ } catch {
209
+ return 0;
210
+ }
211
+ }