@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
@@ -0,0 +1,78 @@
1
+ import { PresetFn, SavedFile } from "@pipelab/shared";
2
+
3
+ export const testC3Offline: PresetFn = async () => {
4
+ const packageWithElecton = "electron-package-node";
5
+ const steamUpload = "steam-upload-node";
6
+
7
+ const data: SavedFile = {
8
+ version: "3.0.0",
9
+ variables: [],
10
+ name: "C3 test without export",
11
+ description: "C3 test without export",
12
+ canvas: {
13
+ triggers: [
14
+ {
15
+ type: "event",
16
+ origin: {
17
+ pluginId: "system",
18
+ nodeId: "manual",
19
+ },
20
+ uid: "manual-start",
21
+ params: {},
22
+ },
23
+ ],
24
+ blocks: [
25
+ {
26
+ uid: packageWithElecton,
27
+ type: "action",
28
+ origin: {
29
+ nodeId: "package-to-electron",
30
+ pluginId: "electron",
31
+ },
32
+ params: {
33
+ "input-folder": {
34
+ editor: "editor",
35
+ value: `/home/quentin/Documents/Cyn Assets/app`,
36
+ },
37
+ arch: undefined,
38
+ platform: undefined,
39
+ },
40
+ },
41
+ {
42
+ uid: steamUpload,
43
+ type: "action",
44
+ origin: {
45
+ nodeId: "steam-upload",
46
+ pluginId: "steam",
47
+ },
48
+ params: {
49
+ folder: {
50
+ editor: "editor",
51
+ value: `{{ steps['${packageWithElecton}']['outputs']['output'] }}`,
52
+ },
53
+ appId: {
54
+ editor: "editor",
55
+ value: "3047200",
56
+ },
57
+ depotId: {
58
+ editor: "editor",
59
+ value: "3047201",
60
+ },
61
+ sdk: {
62
+ editor: "editor",
63
+ value: "/home/quentin/Documents/steamworkssdk/sdk",
64
+ },
65
+ username: {
66
+ editor: "editor",
67
+ value: "armaldio",
68
+ },
69
+ },
70
+ },
71
+ ],
72
+ },
73
+ };
74
+
75
+ return {
76
+ data,
77
+ };
78
+ };
@@ -0,0 +1,124 @@
1
+ import { PresetFn, SavedFile } from "@pipelab/shared";
2
+
3
+ export const testC3Unzip: PresetFn = async () => {
4
+ const exportConstructProjectId = "export-construct-project";
5
+ const unzipFileId = "unzip-file-node";
6
+ const packageWithElecton = "electron-package-node";
7
+ const steamUpload = "steam-upload-node";
8
+
9
+ const data: SavedFile = {
10
+ version: "3.0.0",
11
+ name: "From Construct to Steam",
12
+ description: "Export from Construct, package with Electron, then upload to Steam",
13
+ variables: [],
14
+ canvas: {
15
+ triggers: [
16
+ {
17
+ type: "event",
18
+ origin: {
19
+ pluginId: "system",
20
+ nodeId: "manual",
21
+ },
22
+ uid: "manual-start",
23
+ params: {},
24
+ },
25
+ ],
26
+ blocks: [
27
+ {
28
+ uid: exportConstructProjectId,
29
+ type: "action",
30
+ origin: {
31
+ nodeId: "export-construct-project",
32
+ pluginId: "construct",
33
+ },
34
+ params: {
35
+ file: {
36
+ editor: "editor",
37
+ value: `'/home/armaldio/Téléchargements/test.c3p'`,
38
+ },
39
+ username: {
40
+ editor: "editor",
41
+ value: '"a"',
42
+ },
43
+ password: {
44
+ editor: "editor",
45
+ value: '"a"',
46
+ },
47
+ version: {
48
+ editor: "editor",
49
+ value: '"395"',
50
+ },
51
+ headless: {
52
+ editor: "editor",
53
+ value: false,
54
+ },
55
+ },
56
+ },
57
+ {
58
+ uid: unzipFileId,
59
+ type: "action",
60
+ origin: {
61
+ nodeId: "unzip-file-node",
62
+ pluginId: "filesystem",
63
+ },
64
+ params: {
65
+ file: {
66
+ editor: "editor",
67
+ value: `steps['${exportConstructProjectId}']['outputs']['folder']`,
68
+ },
69
+ },
70
+ },
71
+ {
72
+ uid: packageWithElecton,
73
+ type: "action",
74
+ origin: {
75
+ nodeId: "electron:package",
76
+ pluginId: "electron",
77
+ },
78
+ params: {
79
+ "input-folder": {
80
+ editor: "editor",
81
+ value: `steps['${unzipFileId}']['outputs']['output']`,
82
+ },
83
+ arch: undefined,
84
+ platform: undefined,
85
+ },
86
+ },
87
+ {
88
+ uid: steamUpload,
89
+ type: "action",
90
+ origin: {
91
+ nodeId: "steam-upload",
92
+ pluginId: "steam",
93
+ },
94
+ params: {
95
+ folder: {
96
+ editor: "editor",
97
+ value: `steps['${packageWithElecton}']['outputs']['output']`,
98
+ },
99
+ appId: {
100
+ editor: "editor",
101
+ value: "'3047200'",
102
+ },
103
+ depotId: {
104
+ editor: "editor",
105
+ value: "'3047201'",
106
+ },
107
+ sdk: {
108
+ editor: "editor",
109
+ value: "'/home/armaldio/Documents/steamworkssdk/sdk'",
110
+ },
111
+ username: {
112
+ editor: "editor",
113
+ value: "'armaldio'",
114
+ },
115
+ },
116
+ },
117
+ ],
118
+ },
119
+ };
120
+
121
+ return {
122
+ data,
123
+ };
124
+ };
package/src/runner.ts ADDED
@@ -0,0 +1,107 @@
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
+ }
17
+
18
+ export async function runPipelineCommand(file: string, options: RunOptions, version: string) {
19
+ const pipelinePath = isAbsolute(file) ? file : resolve(process.cwd(), file);
20
+
21
+ try {
22
+ await access(pipelinePath);
23
+ } catch (e) {
24
+ throw new Error(`Pipeline file not found at ${pipelinePath}`);
25
+ }
26
+
27
+ const pipelineContent = await readFile(pipelinePath, "utf-8");
28
+ const pipeline = JSON.parse(pipelineContent);
29
+
30
+ let finalPipeline = pipeline;
31
+ if (pipeline.version) {
32
+ finalPipeline = await savedFileMigrator.migrate(pipeline);
33
+ }
34
+
35
+ const {
36
+ graph: rawGraph,
37
+ variables: pipelineVariables,
38
+ projectName,
39
+ projectPath,
40
+ pipelineId,
41
+ canvas,
42
+ name,
43
+ } = finalPipeline;
44
+
45
+ const graph = rawGraph || canvas?.blocks;
46
+
47
+ if (!graph) {
48
+ throw new Error("Pipeline does not contain a valid graph or canvas.blocks");
49
+ }
50
+
51
+ let vars = pipelineVariables || finalPipeline.variables || [];
52
+ if (options.variables) {
53
+ vars = JSON.parse(options.variables);
54
+ }
55
+
56
+ const effectiveProjectName = projectName || name || "CLI Run";
57
+ const effectiveProjectPath = projectPath || process.cwd();
58
+ const effectivePipelineId = pipelineId || "cli-run";
59
+
60
+ console.log(`Executing pipeline: ${effectiveProjectName} (${effectivePipelineId})`);
61
+
62
+ if (!options.userData) throw new Error("userDataPath is required for runPipelineCommand");
63
+ const context = new PipelabContext({
64
+ userDataPath: options.userData,
65
+ });
66
+
67
+ await registerAllHandlers({ version, context });
68
+ registerMigrationHandlers(context);
69
+
70
+ const settings = await setupConfigFile<AppConfig>("settings", { context });
71
+ const config = await settings.getConfig();
72
+ const cachePath = config?.cacheFolder || tmpdir();
73
+
74
+ const abortController = new AbortController();
75
+
76
+ const { result, buildId } = await executeGraphWithHistory({
77
+ graph,
78
+ variables: vars,
79
+ projectName: effectiveProjectName,
80
+ projectPath: effectiveProjectPath,
81
+ pipelineId: effectivePipelineId,
82
+ cachePath: cachePath,
83
+ onNodeEnter: (node) => console.log(`[ENTER] ${node.name} (${node.uid})`),
84
+ onNodeExit: (node) => console.log(`[EXIT] ${node.name} (${node.uid})`),
85
+ onLog: (data) => {
86
+ if (data.type === "log") {
87
+ console.log(`[LOG] ${data.data.message}`);
88
+ }
89
+ },
90
+ abortSignal: abortController.signal,
91
+ context,
92
+ });
93
+
94
+ console.log(`Pipeline execution finished. Build ID: ${buildId}`);
95
+
96
+ if (options.output) {
97
+ const outputPath = isAbsolute(options.output)
98
+ ? options.output
99
+ : resolve(process.cwd(), options.output);
100
+
101
+ await mkdir(dirname(outputPath), { recursive: true });
102
+ await writeFile(outputPath, JSON.stringify(result, null, 2), "utf-8");
103
+ console.log(`Result saved to ${outputPath}`);
104
+ }
105
+
106
+ return result;
107
+ }
package/src/server.ts ADDED
@@ -0,0 +1,80 @@
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 async function serveCommand(options: ServeOptions, version: string, _dirname: string) {
25
+ if (!options.userData) throw new Error("userDataPath is required for serveCommand");
26
+ const context = new PipelabContext({
27
+ userDataPath: options.userData,
28
+ });
29
+
30
+ let rawAssetFolder: string | undefined;
31
+ if (!isDev) {
32
+ rawAssetFolder = await fetchPipelabAsset("@pipelab/ui", "latest", { context });
33
+ }
34
+
35
+ const server = http.createServer(async (request, response) => {
36
+ if (isDev) {
37
+ response.writeHead(200, { "Content-Type": "text/html" });
38
+ response.end(`
39
+ <html>
40
+ <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;">
41
+ <h1 style="color: #38bdf8;">Pipelab Dev Mode</h1>
42
+ <p>The CLI server is running (API/WebSocket), but the UI is not served here in development.</p>
43
+ <p>Please open the UI through its own dev server (usually <a href="http://localhost:5173" style="color: #38bdf8;">http://localhost:5173</a>).</p>
44
+ </body>
45
+ </html>
46
+ `);
47
+ return;
48
+ }
49
+
50
+ if (!rawAssetFolder || !existsSync(rawAssetFolder)) {
51
+ response.writeHead(404, { "Content-Type": "text/plain" });
52
+ response.end(
53
+ `Error: UI directory not found at ${rawAssetFolder}.\n` +
54
+ "Please run 'pnpm build' in apps/ui to generate the distribution.",
55
+ );
56
+ return;
57
+ }
58
+
59
+ return handler(request, response, {
60
+ public: rawAssetFolder,
61
+ });
62
+ });
63
+
64
+ console.log(`Starting Pipelab server on port ${options.port}...`);
65
+ if (isDev) {
66
+ console.info(getUiDevServerMissingWarning());
67
+ console.log(`UI available at http://localhost:${uiDevPort}`);
68
+ } else {
69
+ console.log(`UI available at http://localhost:${options.port}`);
70
+ }
71
+
72
+ await registerAllHandlers({
73
+ version,
74
+ context,
75
+ });
76
+ registerMigrationHandlers(context);
77
+
78
+ await webSocketServer.start(Number(options.port), server);
79
+ return server;
80
+ }
@@ -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,182 @@
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 } 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
+ };