@pipelab/core-node 0.0.1
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/.oxfmtrc.json +3 -0
- package/.turbo/turbo-build.log +56 -0
- package/.turbo/turbo-format.log +6 -0
- package/.turbo/turbo-lint.log +618 -0
- package/CHANGELOG.md +62 -0
- package/LICENSE +110 -0
- package/README.md +10 -0
- package/dist/index.d.mts +449 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +52693 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +64 -0
- package/scratch/simulate-updates.ts +118 -0
- package/src/api.ts +115 -0
- package/src/config.ts +100 -0
- package/src/context.ts +74 -0
- package/src/handler-func.ts +234 -0
- package/src/handlers/agents.ts +32 -0
- package/src/handlers/auth.ts +95 -0
- package/src/handlers/build-history.ts +379 -0
- package/src/handlers/config.ts +109 -0
- package/src/handlers/engine.ts +229 -0
- package/src/handlers/fs.ts +97 -0
- package/src/handlers/history.ts +327 -0
- package/src/handlers/index.ts +41 -0
- package/src/handlers/shell.ts +57 -0
- package/src/handlers/system.ts +18 -0
- package/src/handlers.ts +2 -0
- package/src/heavy.ts +4 -0
- package/src/index.ts +17 -0
- package/src/ipc-core.ts +77 -0
- package/src/migrations.ts +72 -0
- package/src/paths.ts +1 -0
- package/src/plugins-registry.ts +84 -0
- package/src/presets/c3toSteam.ts +272 -0
- package/src/presets/demo.ts +123 -0
- package/src/presets/if.ts +69 -0
- package/src/presets/list.ts +30 -0
- package/src/presets/loop.ts +65 -0
- package/src/presets/moreToCome.ts +32 -0
- package/src/presets/newProject.ts +31 -0
- package/src/presets/preset.model.ts +0 -0
- package/src/presets/test-c3-offline.ts +78 -0
- package/src/presets/test-c3-unzip.ts +124 -0
- package/src/runner.ts +166 -0
- package/src/server.ts +101 -0
- package/src/types/runner.ts +101 -0
- package/src/utils/fs-extras.ts +220 -0
- package/src/utils/github.ts +119 -0
- package/src/utils/remote.ts +498 -0
- package/src/utils/storage.ts +99 -0
- package/src/utils.ts +268 -0
- package/src/websocket-server.ts +288 -0
- package/tsconfig.json +19 -0
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pipelab/core-node",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "The Pipelab automation engine for Node.js",
|
|
6
|
+
"license": "FSL-1.1-MIT",
|
|
7
|
+
"author": "CynToolkit",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/CynToolkit/pipelab.git",
|
|
11
|
+
"directory": "packages/core-node"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.mjs",
|
|
16
|
+
"types": "./dist/index.d.mts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"import": "./dist/index.mjs",
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"default": "./dist/index.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@supabase/supabase-js": "2.99.3",
|
|
30
|
+
"adm-zip": "0.5.16",
|
|
31
|
+
"archiver": "7.0.1",
|
|
32
|
+
"check-disk-space": "3.4.0",
|
|
33
|
+
"es-toolkit": "1.25.2",
|
|
34
|
+
"execa": "9.5.2",
|
|
35
|
+
"nanoid": "5.0.7",
|
|
36
|
+
"pacote": "21.5.0",
|
|
37
|
+
"semver": "^7.7.4",
|
|
38
|
+
"slash": "3.0.0",
|
|
39
|
+
"tar": "6.2.1",
|
|
40
|
+
"type-fest": "4.39.0",
|
|
41
|
+
"ws": "8.18.3",
|
|
42
|
+
"yauzl": "2.10.0",
|
|
43
|
+
"@pipelab/constants": "0.0.1",
|
|
44
|
+
"@pipelab/shared": "0.0.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/adm-zip": "0.5.7",
|
|
48
|
+
"@types/archiver": "7.0.0",
|
|
49
|
+
"@types/node": "24.12.2",
|
|
50
|
+
"@types/pacote": "11.1.8",
|
|
51
|
+
"@types/semver": "^7.7.1",
|
|
52
|
+
"@types/tar": "7.0.87",
|
|
53
|
+
"@types/ws": "8.18.1",
|
|
54
|
+
"@types/yauzl": "2.10.3",
|
|
55
|
+
"tsdown": "0.21.2",
|
|
56
|
+
"typescript": "^5.0.0",
|
|
57
|
+
"@pipelab/tsconfig": "0.0.1"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsdown",
|
|
61
|
+
"format": "oxfmt .",
|
|
62
|
+
"lint": "oxlint ."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import pacote from "pacote";
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
import { fetchLatestDesktopRelease } from "../src/utils/github.js";
|
|
4
|
+
|
|
5
|
+
const PACKAGES = [
|
|
6
|
+
// Apps & Core
|
|
7
|
+
"@pipelab/cli",
|
|
8
|
+
"@pipelab/ui",
|
|
9
|
+
"@pipelab/shared",
|
|
10
|
+
"@pipelab/core-node",
|
|
11
|
+
"@pipelab/constants",
|
|
12
|
+
"@pipelab/migration",
|
|
13
|
+
|
|
14
|
+
// Plugins
|
|
15
|
+
"@pipelab/plugin-construct",
|
|
16
|
+
"@pipelab/plugin-core",
|
|
17
|
+
"@pipelab/plugin-discord",
|
|
18
|
+
"@pipelab/plugin-electron",
|
|
19
|
+
"@pipelab/plugin-filesystem",
|
|
20
|
+
"@pipelab/plugin-itch",
|
|
21
|
+
"@pipelab/plugin-minify",
|
|
22
|
+
"@pipelab/plugin-netlify",
|
|
23
|
+
"@pipelab/plugin-nvpatch",
|
|
24
|
+
"@pipelab/plugin-poki",
|
|
25
|
+
"@pipelab/plugin-steam",
|
|
26
|
+
"@pipelab/plugin-system",
|
|
27
|
+
"@pipelab/plugin-tauri",
|
|
28
|
+
|
|
29
|
+
// Assets
|
|
30
|
+
"@pipelab/asset-discord",
|
|
31
|
+
"@pipelab/asset-electron",
|
|
32
|
+
"@pipelab/asset-netlify",
|
|
33
|
+
"@pipelab/asset-tauri"
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
async function simulate() {
|
|
37
|
+
console.log("==========================================");
|
|
38
|
+
console.log(" PIPELAB UPDATE FLOW SIMULATOR ");
|
|
39
|
+
console.log("==========================================\n");
|
|
40
|
+
|
|
41
|
+
// 1. Simulate Desktop GitHub Release Update
|
|
42
|
+
console.log("--- 1. Simulating Desktop Application (GitHub Releases) ---");
|
|
43
|
+
try {
|
|
44
|
+
const stableApp = await fetchLatestDesktopRelease({ allowPrerelease: false });
|
|
45
|
+
const betaApp = await fetchLatestDesktopRelease({ allowPrerelease: true });
|
|
46
|
+
|
|
47
|
+
console.log(`[STABLE] Resolved version: ${stableApp ? stableApp.tag_name : "None (Stable users protected)"}`);
|
|
48
|
+
console.log(`[BETA] Resolved version: ${betaApp ? betaApp.tag_name : "None"}`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("Failed to fetch desktop releases from GitHub:", error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log("\n--- 2. Simulating NPM Packages (CLI, UI, Plugins, Assets) ---");
|
|
54
|
+
console.log("Fetching data from NPM registry. Please wait...\n");
|
|
55
|
+
|
|
56
|
+
const results: Array<{
|
|
57
|
+
package: string;
|
|
58
|
+
stable: string;
|
|
59
|
+
beta: string;
|
|
60
|
+
notes: string;
|
|
61
|
+
}> = [];
|
|
62
|
+
|
|
63
|
+
for (const pkg of PACKAGES) {
|
|
64
|
+
try {
|
|
65
|
+
const packument = await pacote.packument(pkg);
|
|
66
|
+
const stableVersion = packument["dist-tags"]?.latest || "N/A";
|
|
67
|
+
const betaVersion = packument["dist-tags"]?.beta || "N/A";
|
|
68
|
+
|
|
69
|
+
let notes = "";
|
|
70
|
+
if (stableVersion === "N/A" && betaVersion === "N/A") {
|
|
71
|
+
notes = "⚠️ Package not published on NPM";
|
|
72
|
+
} else if (betaVersion !== "N/A" && stableVersion !== "N/A") {
|
|
73
|
+
if (betaVersion === stableVersion) {
|
|
74
|
+
notes = "Stable and Beta are in sync";
|
|
75
|
+
} else {
|
|
76
|
+
// Attempt semver comparison
|
|
77
|
+
const cleanStable = semver.coerce(stableVersion);
|
|
78
|
+
const cleanBeta = semver.coerce(betaVersion);
|
|
79
|
+
if (cleanStable && cleanBeta) {
|
|
80
|
+
const comp = semver.compare(stableVersion, betaVersion);
|
|
81
|
+
if (comp < 0) {
|
|
82
|
+
notes = "🚀 Beta is ahead";
|
|
83
|
+
} else if (comp > 0) {
|
|
84
|
+
notes = "Stable is ahead";
|
|
85
|
+
} else {
|
|
86
|
+
notes = "Stable and Beta are in sync (coerced)";
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
notes = "Different versions (non-semver)";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else if (betaVersion !== "N/A" && stableVersion === "N/A") {
|
|
93
|
+
notes = "Only Beta version exists";
|
|
94
|
+
} else if (stableVersion !== "N/A" && betaVersion === "N/A") {
|
|
95
|
+
notes = "No Beta tag published";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
results.push({
|
|
99
|
+
package: pkg,
|
|
100
|
+
stable: stableVersion,
|
|
101
|
+
beta: betaVersion,
|
|
102
|
+
notes
|
|
103
|
+
});
|
|
104
|
+
} catch (error: any) {
|
|
105
|
+
results.push({
|
|
106
|
+
package: pkg,
|
|
107
|
+
stable: "ERROR",
|
|
108
|
+
beta: "ERROR",
|
|
109
|
+
notes: `Failed to fetch: ${error.message}`
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.table(results);
|
|
115
|
+
console.log("\n==========================================\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
simulate().catch(console.error);
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
import { klona } from "klona";
|
|
3
|
+
import { toRaw } from "vue";
|
|
4
|
+
|
|
5
|
+
import type { Tagged } from "type-fest";
|
|
6
|
+
import { ILogObjMeta } from "tslog";
|
|
7
|
+
import { useLogger } from "@pipelab/shared";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
UpdateStatus,
|
|
11
|
+
RendererChannels,
|
|
12
|
+
RendererData,
|
|
13
|
+
RendererEvents,
|
|
14
|
+
RendererEnd,
|
|
15
|
+
RendererMessage,
|
|
16
|
+
RequestId,
|
|
17
|
+
HandleListenerRendererSendFn,
|
|
18
|
+
HandleListenerRenderer as BaseHandleListenerRenderer,
|
|
19
|
+
} from "@pipelab/shared";
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
UpdateStatus,
|
|
23
|
+
RendererChannels,
|
|
24
|
+
RendererData,
|
|
25
|
+
RendererEvents,
|
|
26
|
+
RendererEnd,
|
|
27
|
+
RendererMessage,
|
|
28
|
+
RequestId,
|
|
29
|
+
HandleListenerRendererSendFn,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type HandleListenerRenderer<KEY extends RendererChannels> = (
|
|
33
|
+
event: Electron.IpcMainInvokeEvent,
|
|
34
|
+
data: { value: RendererData<KEY>; send: HandleListenerRendererSendFn<KEY> },
|
|
35
|
+
) => Promise<void>;
|
|
36
|
+
|
|
37
|
+
export type ListenerMain<KEY extends RendererChannels> = (
|
|
38
|
+
event: Electron.IpcMainEvent,
|
|
39
|
+
data: RendererEvents<KEY>,
|
|
40
|
+
) => Promise<void>;
|
|
41
|
+
|
|
42
|
+
export const usePluginAPI = (browserWindow: any) => {
|
|
43
|
+
const { logger } = useLogger();
|
|
44
|
+
/**
|
|
45
|
+
* Send an order
|
|
46
|
+
*/
|
|
47
|
+
const send = <KEY extends RendererChannels>(channel: KEY, args?: RendererData<KEY>) => {
|
|
48
|
+
if (!browserWindow || browserWindow.isDestroyed()) return;
|
|
49
|
+
browserWindow.webContents.send(channel, args);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const on = <KEY extends RendererChannels>(
|
|
53
|
+
channel: KEY | string,
|
|
54
|
+
listener: (event: any, data: RendererEvents<KEY>) => void,
|
|
55
|
+
) => {
|
|
56
|
+
if (!browserWindow || browserWindow.isDestroyed()) return () => {};
|
|
57
|
+
const ipcMain = browserWindow.webContents.ipc.on(channel, listener);
|
|
58
|
+
|
|
59
|
+
const cancel = () => {
|
|
60
|
+
if (browserWindow.isDestroyed()) return;
|
|
61
|
+
ipcMain.removeListener(channel, listener);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return cancel;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Send an order and wait for it's execution
|
|
69
|
+
*/
|
|
70
|
+
const execute = async <KEY extends RendererChannels>(
|
|
71
|
+
channel: KEY,
|
|
72
|
+
data?: RendererData<KEY>,
|
|
73
|
+
listener?: ListenerMain<KEY>,
|
|
74
|
+
) => {
|
|
75
|
+
const newId = nanoid() as RequestId;
|
|
76
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
77
|
+
return new Promise<RendererEnd<KEY>>(async (resolve, reject) => {
|
|
78
|
+
const message: RendererMessage = {
|
|
79
|
+
requestId: newId,
|
|
80
|
+
data: toRaw(klona(data)),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (!browserWindow || browserWindow.isDestroyed()) {
|
|
84
|
+
return reject(new Error("Browser window is destroyed"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cancel = on(newId, async (event, data) => {
|
|
88
|
+
// console.log('receiving event', event, data)
|
|
89
|
+
if (data.type === "end") {
|
|
90
|
+
cancel();
|
|
91
|
+
return resolve(data.data);
|
|
92
|
+
} else {
|
|
93
|
+
await listener?.(event, data);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// send the message
|
|
98
|
+
try {
|
|
99
|
+
browserWindow.webContents.send(channel, message);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
logger().error(e);
|
|
102
|
+
logger().error(channel, message);
|
|
103
|
+
reject(e);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
send,
|
|
110
|
+
on,
|
|
111
|
+
execute,
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type UseMainAPI = ReturnType<typeof usePluginAPI>;
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { PipelabContext } from "./context";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensure } from "./utils/fs-extras";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import { useLogger } from "@pipelab/shared";
|
|
6
|
+
import { configRegistry, Migrator } from "@pipelab/shared";
|
|
7
|
+
|
|
8
|
+
export const getMigrator = <T>(name: string) => {
|
|
9
|
+
if (configRegistry[name]) {
|
|
10
|
+
return configRegistry[name] as Migrator<T>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (name.startsWith("pipeline-")) {
|
|
14
|
+
return configRegistry["pipeline"] as Migrator<T>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const setupConfigFile = async <T>(
|
|
21
|
+
name: string,
|
|
22
|
+
options: { context: PipelabContext; migrator?: Migrator<T> },
|
|
23
|
+
) => {
|
|
24
|
+
const ctx = options.context;
|
|
25
|
+
const migrator = options.migrator || getMigrator<T>(name);
|
|
26
|
+
|
|
27
|
+
if (!migrator) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`No migrator found for configuration: ${name}. All managed files must have a migration schema.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isAbsolutePath = path.isAbsolute(name);
|
|
34
|
+
const filesPath = isAbsolutePath ? name : ctx.getConfigPath(`${name}.json`);
|
|
35
|
+
|
|
36
|
+
await ensure(filesPath, JSON.stringify(migrator.defaultValue));
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
setConfig: async (config: T) => {
|
|
40
|
+
const { logger } = useLogger();
|
|
41
|
+
try {
|
|
42
|
+
await fs.writeFile(filesPath, JSON.stringify(config));
|
|
43
|
+
return true;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
logger().error(`Error saving config ${name}:`, e);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
getConfig: async () => {
|
|
50
|
+
const { logger } = useLogger();
|
|
51
|
+
let content = undefined;
|
|
52
|
+
let originalJson: any = undefined;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
content = await fs.readFile(filesPath, "utf8");
|
|
56
|
+
if (content !== undefined) {
|
|
57
|
+
originalJson = JSON.parse(content);
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
logger().error(`Error reading or parsing config ${name}:`, e);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// console.log("content", content);
|
|
64
|
+
|
|
65
|
+
let json: any = undefined;
|
|
66
|
+
try {
|
|
67
|
+
json = await migrator.migrate(originalJson, {
|
|
68
|
+
debug: false,
|
|
69
|
+
onStep: async (state: any, version: string) => {
|
|
70
|
+
const parsedPath = path.parse(filesPath);
|
|
71
|
+
const versionedPath = path.join(parsedPath.dir, `${parsedPath.name}.v${version}.json`);
|
|
72
|
+
try {
|
|
73
|
+
await fs.writeFile(versionedPath, JSON.stringify(state));
|
|
74
|
+
logger().info(`Intermediate backup created for ${name} at ${versionedPath}`);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
logger().error(`Failed to create intermediate backup for ${name} at v${version}:`, e);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
} catch (e) {
|
|
81
|
+
logger().error(`Error migrating config ${name}:`, e);
|
|
82
|
+
json = migrator.defaultValue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const originalVersion = originalJson?.version;
|
|
86
|
+
const newVersion = json?.version;
|
|
87
|
+
|
|
88
|
+
// Save back migrated config if changed or if it's a new file
|
|
89
|
+
if (originalVersion !== newVersion || content === undefined) {
|
|
90
|
+
try {
|
|
91
|
+
await fs.writeFile(filesPath, JSON.stringify(json));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
logger().error(`Error saving migrated config ${name}:`, e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return json as T;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
};
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { join, dirname, resolve } from "node:path";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
const _dirname =
|
|
7
|
+
typeof __dirname !== "undefined"
|
|
8
|
+
? __dirname
|
|
9
|
+
: typeof import.meta !== "undefined" && import.meta.url
|
|
10
|
+
? dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
: process.cwd();
|
|
12
|
+
|
|
13
|
+
export const isDev = process.env.NODE_ENV === "development";
|
|
14
|
+
|
|
15
|
+
export const getDefaultUserDataPath = (env?: "dev" | "beta" | "prod") => {
|
|
16
|
+
const base = (() => {
|
|
17
|
+
switch (platform()) {
|
|
18
|
+
case "win32":
|
|
19
|
+
return process.env.APPDATA || join(homedir(), "AppData", "Roaming");
|
|
20
|
+
case "darwin":
|
|
21
|
+
return join(homedir(), "Library", "Application Support");
|
|
22
|
+
default:
|
|
23
|
+
return process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
const mode = env ?? (isDev ? "dev" : "prod");
|
|
28
|
+
const folder = mode === "dev" ? "app-dev" : mode === "beta" ? "app-beta" : "app";
|
|
29
|
+
|
|
30
|
+
return join(base, "@pipelab", folder);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Finds the monorepo root by looking for pnpm-workspace.yaml.
|
|
35
|
+
*/
|
|
36
|
+
function findProjectRoot(startDir: string): string | null {
|
|
37
|
+
let curr = startDir;
|
|
38
|
+
while (curr !== dirname(curr)) {
|
|
39
|
+
if (existsSync(join(curr, "pnpm-workspace.yaml"))) {
|
|
40
|
+
return curr;
|
|
41
|
+
}
|
|
42
|
+
curr = dirname(curr);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const projectRoot = findProjectRoot(_dirname);
|
|
48
|
+
|
|
49
|
+
export interface PipelabContextOptions {
|
|
50
|
+
userDataPath: string;
|
|
51
|
+
releaseTag?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class PipelabContext {
|
|
55
|
+
public readonly userDataPath: string;
|
|
56
|
+
public readonly releaseTag: string;
|
|
57
|
+
|
|
58
|
+
constructor(options: PipelabContextOptions) {
|
|
59
|
+
this.userDataPath = options.userDataPath;
|
|
60
|
+
this.releaseTag = options.releaseTag || "latest";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getPackagesPath(...subpaths: string[]) {
|
|
64
|
+
return join(this.userDataPath, "packages", ...subpaths);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getThirdPartyPath(...subpaths: string[]) {
|
|
68
|
+
return join(this.userDataPath, "thirdparty", ...subpaths);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getConfigPath(...subpaths: string[]) {
|
|
72
|
+
return join(this.userDataPath, "config", ...subpaths);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { End } from "@pipelab/shared";
|
|
2
|
+
import { ensureNodeJS, ensurePNPM } from "./utils/remote";
|
|
3
|
+
import { Action, ActionRunner, Condition, ConditionRunner } from "./types/runner";
|
|
4
|
+
import { InputsDefinition } from "@pipelab/shared";
|
|
5
|
+
import { usePlugins } from "@pipelab/shared";
|
|
6
|
+
import { isRequired } from "@pipelab/shared";
|
|
7
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
8
|
+
import { PipelabContext } from "./context";
|
|
9
|
+
import { useLogger } from "@pipelab/shared";
|
|
10
|
+
import { BlockCondition } from "@pipelab/shared";
|
|
11
|
+
import { HandleListenerSendFn } from "./handlers";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
const { join } = path;
|
|
14
|
+
|
|
15
|
+
const checkParams = (definitionParams: InputsDefinition, elementParams: Record<string, string>) => {
|
|
16
|
+
// get a list of all required params
|
|
17
|
+
let expected = Object.keys(definitionParams);
|
|
18
|
+
|
|
19
|
+
const found: string[] = [];
|
|
20
|
+
// for each param in elementParams
|
|
21
|
+
for (const param of Object.keys(elementParams)) {
|
|
22
|
+
// if the param is in the expected list
|
|
23
|
+
if (expected.includes(param)) {
|
|
24
|
+
// add to found
|
|
25
|
+
found.push(param);
|
|
26
|
+
// remove from expected
|
|
27
|
+
expected = expected.filter((x) => x !== param);
|
|
28
|
+
} else {
|
|
29
|
+
// throw new Error('Unexpected param "' + param + '"')
|
|
30
|
+
console.warn('Unexpected param "' + param + '"');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const param of expected) {
|
|
35
|
+
if (isRequired(definitionParams[param])) {
|
|
36
|
+
throw new Error('Missing param "' + param + '"');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const handleConditionExecute = async (
|
|
42
|
+
nodeId: string,
|
|
43
|
+
pluginId: string,
|
|
44
|
+
params: BlockCondition["params"],
|
|
45
|
+
cwd: string,
|
|
46
|
+
context: PipelabContext,
|
|
47
|
+
): Promise<End<"condition:execute">> => {
|
|
48
|
+
const ctx = context;
|
|
49
|
+
const { plugins } = usePlugins();
|
|
50
|
+
const { logger } = useLogger();
|
|
51
|
+
|
|
52
|
+
const node = plugins.value
|
|
53
|
+
.find((plugin) => plugin.id === pluginId)
|
|
54
|
+
?.nodes.find((node: any) => node.node.id === nodeId) as
|
|
55
|
+
| {
|
|
56
|
+
node: Condition;
|
|
57
|
+
runner: ConditionRunner<any>;
|
|
58
|
+
}
|
|
59
|
+
| undefined;
|
|
60
|
+
|
|
61
|
+
if (!node) {
|
|
62
|
+
return {
|
|
63
|
+
type: "error",
|
|
64
|
+
ipcError: "Node not found",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const s = await stat(cwd);
|
|
70
|
+
if (!s.isDirectory()) {
|
|
71
|
+
throw new Error(`Execution directory "${cwd}" exists but is not a directory.`);
|
|
72
|
+
}
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
if (e.code === "ENOENT") {
|
|
75
|
+
await mkdir(cwd, { recursive: true });
|
|
76
|
+
} else {
|
|
77
|
+
throw e;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
checkParams(node.node.params, params);
|
|
83
|
+
const resolvedInputs = params; // await resolveConditionInputs(params, node.node, steps)
|
|
84
|
+
|
|
85
|
+
const result = await node.runner({
|
|
86
|
+
inputs: resolvedInputs,
|
|
87
|
+
log: (...args) => {
|
|
88
|
+
logger().info(`[${node.node.name}]`, ...args);
|
|
89
|
+
},
|
|
90
|
+
meta: {
|
|
91
|
+
definition: "",
|
|
92
|
+
},
|
|
93
|
+
setMeta: () => {
|
|
94
|
+
logger().info("set meta defined here");
|
|
95
|
+
},
|
|
96
|
+
cwd,
|
|
97
|
+
context: ctx,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
type: "success",
|
|
102
|
+
result: {
|
|
103
|
+
outputs: {},
|
|
104
|
+
value: result,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
} catch (e) {
|
|
108
|
+
logger().error("Error in condition execution:", e);
|
|
109
|
+
return {
|
|
110
|
+
type: "error",
|
|
111
|
+
ipcError: String(e),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const handleActionExecute = async (
|
|
117
|
+
nodeId: string,
|
|
118
|
+
pluginId: string,
|
|
119
|
+
params: Record<string, string>,
|
|
120
|
+
mainWindow: any | undefined,
|
|
121
|
+
send: HandleListenerSendFn<"action:execute">,
|
|
122
|
+
abortSignal: AbortSignal,
|
|
123
|
+
cwd: string,
|
|
124
|
+
cachePath: string,
|
|
125
|
+
context: PipelabContext,
|
|
126
|
+
): Promise<End<"action:execute">> => {
|
|
127
|
+
const ctx = context;
|
|
128
|
+
const { plugins } = usePlugins();
|
|
129
|
+
const { logger } = useLogger();
|
|
130
|
+
|
|
131
|
+
mainWindow?.setProgressBar(1, {
|
|
132
|
+
mode: "indeterminate",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const node = plugins.value
|
|
136
|
+
.find((plugin) => plugin.id === pluginId)
|
|
137
|
+
?.nodes.find((node: any) => node.node.id === nodeId) as
|
|
138
|
+
| {
|
|
139
|
+
node: Action;
|
|
140
|
+
runner: ActionRunner<any>;
|
|
141
|
+
}
|
|
142
|
+
| undefined;
|
|
143
|
+
|
|
144
|
+
if (!node) {
|
|
145
|
+
mainWindow?.setProgressBar(1, { mode: "normal" });
|
|
146
|
+
return {
|
|
147
|
+
type: "error",
|
|
148
|
+
ipcError: "Node not found",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nodePath = await ensureNodeJS(ctx);
|
|
153
|
+
const pnpm = await ensurePNPM(ctx);
|
|
154
|
+
|
|
155
|
+
const outputs: Record<string, unknown> = {};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
try {
|
|
159
|
+
const s = await stat(cwd);
|
|
160
|
+
if (!s.isDirectory()) {
|
|
161
|
+
throw new Error(`Execution directory "${cwd}" exists but is not a directory.`);
|
|
162
|
+
}
|
|
163
|
+
} catch (e: any) {
|
|
164
|
+
if (e.code === "ENOENT") {
|
|
165
|
+
await mkdir(cwd, { recursive: true });
|
|
166
|
+
} else {
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
checkParams(node.node.params, params);
|
|
171
|
+
const resolvedInputs = params; // await resolveActionInputs(params, node.node, steps)
|
|
172
|
+
logger().info("resolvedInputs", resolvedInputs);
|
|
173
|
+
|
|
174
|
+
await node.runner({
|
|
175
|
+
inputs: resolvedInputs,
|
|
176
|
+
log: (...args) => {
|
|
177
|
+
const decorator = `[${node.node.name}]`;
|
|
178
|
+
const logArgs = [decorator, ...args];
|
|
179
|
+
logger().info(...logArgs);
|
|
180
|
+
send({
|
|
181
|
+
type: "log",
|
|
182
|
+
data: {
|
|
183
|
+
decorator,
|
|
184
|
+
time: Date.now(),
|
|
185
|
+
message: args,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
setOutput: (key: any, value: unknown) => {
|
|
190
|
+
outputs[key] = value;
|
|
191
|
+
},
|
|
192
|
+
meta: {
|
|
193
|
+
definition: "",
|
|
194
|
+
},
|
|
195
|
+
setMeta: () => {
|
|
196
|
+
logger().info("set meta defined here");
|
|
197
|
+
},
|
|
198
|
+
cwd: cwd,
|
|
199
|
+
paths: {
|
|
200
|
+
cache: cachePath,
|
|
201
|
+
node: nodePath,
|
|
202
|
+
pnpm,
|
|
203
|
+
modules: "",
|
|
204
|
+
userData: ctx.userDataPath,
|
|
205
|
+
thirdparty: ctx.getThirdPartyPath(),
|
|
206
|
+
},
|
|
207
|
+
browserWindow: mainWindow,
|
|
208
|
+
abortSignal,
|
|
209
|
+
context: ctx,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
mainWindow?.setProgressBar(1, { mode: "normal" });
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
type: "success",
|
|
216
|
+
result: { outputs, tmp: cwd },
|
|
217
|
+
};
|
|
218
|
+
} catch (e) {
|
|
219
|
+
logger().error("Error in action execution:", e);
|
|
220
|
+
mainWindow?.setProgressBar(1, { mode: "normal" });
|
|
221
|
+
|
|
222
|
+
// If aborted, throw AbortError to distinguish from actual errors
|
|
223
|
+
if (abortSignal.aborted) {
|
|
224
|
+
const abortError = new Error("Aborted");
|
|
225
|
+
abortError.name = "AbortError";
|
|
226
|
+
throw abortError;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
type: "error",
|
|
231
|
+
ipcError: String(e),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
};
|