@pipelab/core-node 1.0.0-beta.0 → 1.0.0-beta.10
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/CHANGELOG.md +80 -0
- package/dist/index.d.mts +36 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +204 -56
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/scratch/simulate-updates.ts +118 -0
- package/src/config.ts +10 -15
- package/src/context.ts +8 -2
- package/src/handler-func.ts +6 -6
- package/src/handlers/build-history.ts +6 -8
- package/src/handlers/history.ts +6 -8
- package/src/handlers/index.ts +2 -2
- package/src/index.ts +1 -0
- package/src/plugins-registry.ts +25 -20
- package/src/runner.ts +28 -22
- package/src/server.ts +4 -2
- package/src/utils/fs-extras.ts +10 -1
- package/src/utils/github.ts +119 -0
- package/src/utils/remote.ts +133 -49
- package/src/utils.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelab/core-node",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The Pipelab automation engine for Node.js",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@supabase/supabase-js": "2.99.3",
|
|
29
30
|
"adm-zip": "0.5.16",
|
|
30
31
|
"archiver": "7.0.1",
|
|
31
32
|
"check-disk-space": "3.4.0",
|
|
@@ -39,9 +40,8 @@
|
|
|
39
40
|
"type-fest": "4.39.0",
|
|
40
41
|
"ws": "8.18.3",
|
|
41
42
|
"yauzl": "2.10.0",
|
|
42
|
-
"@
|
|
43
|
-
"@pipelab/
|
|
44
|
-
"@pipelab/shared": "1.0.0-beta.0"
|
|
43
|
+
"@pipelab/constants": "1.0.0-beta.8",
|
|
44
|
+
"@pipelab/shared": "1.0.0-beta.6"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/adm-zip": "0.5.7",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@types/yauzl": "2.10.3",
|
|
55
55
|
"tsdown": "0.21.2",
|
|
56
56
|
"typescript": "^5.0.0",
|
|
57
|
-
"@pipelab/tsconfig": "1.0.0-beta.
|
|
57
|
+
"@pipelab/tsconfig": "1.0.0-beta.6"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"build": "tsdown",
|
|
@@ -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/config.ts
CHANGED
|
@@ -66,6 +66,16 @@ export const setupConfigFile = async <T>(
|
|
|
66
66
|
try {
|
|
67
67
|
json = await migrator.migrate(originalJson, {
|
|
68
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
|
+
},
|
|
69
79
|
});
|
|
70
80
|
} catch (e) {
|
|
71
81
|
logger().error(`Error migrating config ${name}:`, e);
|
|
@@ -75,21 +85,6 @@ export const setupConfigFile = async <T>(
|
|
|
75
85
|
const originalVersion = originalJson?.version;
|
|
76
86
|
const newVersion = json?.version;
|
|
77
87
|
|
|
78
|
-
// Check if migration actually changed the version
|
|
79
|
-
if (originalVersion !== newVersion && content !== undefined) {
|
|
80
|
-
// Backup previous file before overwriting
|
|
81
|
-
const parsedPath = path.parse(filesPath);
|
|
82
|
-
const versionSuffix = originalVersion || "unknown";
|
|
83
|
-
const backupPath = path.join(parsedPath.dir, `${parsedPath.name}.${versionSuffix}.bak`);
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await fs.copyFile(filesPath, backupPath);
|
|
87
|
-
logger().info(`Backup created for ${name} at ${backupPath} (version ${versionSuffix})`);
|
|
88
|
-
} catch (e) {
|
|
89
|
-
logger().error(`Failed to create backup for ${name}:`, e);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
88
|
// Save back migrated config if changed or if it's a new file
|
|
94
89
|
if (originalVersion !== newVersion || content === undefined) {
|
|
95
90
|
try {
|
package/src/context.ts
CHANGED
|
@@ -12,7 +12,7 @@ const _dirname =
|
|
|
12
12
|
|
|
13
13
|
export const isDev = process.env.NODE_ENV === "development";
|
|
14
14
|
|
|
15
|
-
export const getDefaultUserDataPath = () => {
|
|
15
|
+
export const getDefaultUserDataPath = (env?: "dev" | "beta" | "prod") => {
|
|
16
16
|
const base = (() => {
|
|
17
17
|
switch (platform()) {
|
|
18
18
|
case "win32":
|
|
@@ -24,7 +24,10 @@ export const getDefaultUserDataPath = () => {
|
|
|
24
24
|
}
|
|
25
25
|
})();
|
|
26
26
|
|
|
27
|
-
|
|
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);
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -45,13 +48,16 @@ export const projectRoot = findProjectRoot(_dirname);
|
|
|
45
48
|
|
|
46
49
|
export interface PipelabContextOptions {
|
|
47
50
|
userDataPath: string;
|
|
51
|
+
releaseTag?: string;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
export class PipelabContext {
|
|
51
55
|
public readonly userDataPath: string;
|
|
56
|
+
public readonly releaseTag: string;
|
|
52
57
|
|
|
53
58
|
constructor(options: PipelabContextOptions) {
|
|
54
59
|
this.userDataPath = options.userDataPath;
|
|
60
|
+
this.releaseTag = options.releaseTag || "latest";
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
getPackagesPath(...subpaths: string[]) {
|
package/src/handler-func.ts
CHANGED
|
@@ -53,9 +53,9 @@ export const handleConditionExecute = async (
|
|
|
53
53
|
.find((plugin) => plugin.id === pluginId)
|
|
54
54
|
?.nodes.find((node: any) => node.node.id === nodeId) as
|
|
55
55
|
| {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
node: Condition;
|
|
57
|
+
runner: ConditionRunner<any>;
|
|
58
|
+
}
|
|
59
59
|
| undefined;
|
|
60
60
|
|
|
61
61
|
if (!node) {
|
|
@@ -136,9 +136,9 @@ export const handleActionExecute = async (
|
|
|
136
136
|
.find((plugin) => plugin.id === pluginId)
|
|
137
137
|
?.nodes.find((node: any) => node.node.id === nodeId) as
|
|
138
138
|
| {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
node: Action;
|
|
140
|
+
runner: ActionRunner<any>;
|
|
141
|
+
}
|
|
142
142
|
| undefined;
|
|
143
143
|
|
|
144
144
|
if (!node) {
|
|
@@ -93,17 +93,13 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
93
93
|
|
|
94
94
|
// 2. Filter by maxEntries (sort by date first to keep the newest)
|
|
95
95
|
if (maxEntries > 0 && entries.length > maxEntries) {
|
|
96
|
-
entries = entries
|
|
97
|
-
.sort((a, b) => b.createdAt - a.createdAt)
|
|
98
|
-
.slice(0, maxEntries);
|
|
96
|
+
entries = entries.sort((a, b) => b.createdAt - a.createdAt).slice(0, maxEntries);
|
|
99
97
|
}
|
|
100
98
|
|
|
101
99
|
if (entries.length < originalCount) {
|
|
102
100
|
this.logger
|
|
103
101
|
.logger()
|
|
104
|
-
.info(
|
|
105
|
-
`[${pipelineId}] Pruned ${originalCount - entries.length} history entries.`,
|
|
106
|
-
);
|
|
102
|
+
.info(`[${pipelineId}] Pruned ${originalCount - entries.length} history entries.`);
|
|
107
103
|
await this.savePipelineHistory(pipelineId, entries);
|
|
108
104
|
}
|
|
109
105
|
}
|
|
@@ -271,8 +267,10 @@ export class BuildHistoryStorage implements IBuildHistoryStorage {
|
|
|
271
267
|
await unlink(pipelinePath);
|
|
272
268
|
this.logger.logger().info(`Cleared history for pipeline "${pipelineId}"`);
|
|
273
269
|
} catch (error: any) {
|
|
274
|
-
if (error.code ===
|
|
275
|
-
this.logger
|
|
270
|
+
if (error.code === "ENOENT") {
|
|
271
|
+
this.logger
|
|
272
|
+
.logger()
|
|
273
|
+
.warn(`No history file found for pipeline "${pipelineId}". Nothing to clear.`);
|
|
276
274
|
return;
|
|
277
275
|
}
|
|
278
276
|
this.logger.logger().error(`Failed to clear history for pipeline "${pipelineId}":`, error);
|
package/src/handlers/history.ts
CHANGED
|
@@ -127,8 +127,7 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
|
|
|
127
127
|
type: "end",
|
|
128
128
|
data: {
|
|
129
129
|
type: "error",
|
|
130
|
-
ipcError:
|
|
131
|
-
error instanceof Error ? error.message : "Failed to get build history entries",
|
|
130
|
+
ipcError: error instanceof Error ? error.message : "Failed to get build history entries",
|
|
132
131
|
},
|
|
133
132
|
});
|
|
134
133
|
}
|
|
@@ -158,8 +157,7 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
|
|
|
158
157
|
type: "end",
|
|
159
158
|
data: {
|
|
160
159
|
type: "error",
|
|
161
|
-
ipcError:
|
|
162
|
-
error instanceof Error ? error.message : "Failed to update build history entry",
|
|
160
|
+
ipcError: error instanceof Error ? error.message : "Failed to update build history entry",
|
|
163
161
|
},
|
|
164
162
|
});
|
|
165
163
|
}
|
|
@@ -189,8 +187,7 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
|
|
|
189
187
|
type: "end",
|
|
190
188
|
data: {
|
|
191
189
|
type: "error",
|
|
192
|
-
ipcError:
|
|
193
|
-
error instanceof Error ? error.message : "Failed to delete build history entry",
|
|
190
|
+
ipcError: error instanceof Error ? error.message : "Failed to delete build history entry",
|
|
194
191
|
},
|
|
195
192
|
});
|
|
196
193
|
}
|
|
@@ -250,7 +247,8 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
|
|
|
250
247
|
type: "end",
|
|
251
248
|
data: {
|
|
252
249
|
type: "error",
|
|
253
|
-
ipcError:
|
|
250
|
+
ipcError:
|
|
251
|
+
error instanceof Error ? error.message : "Failed to clear build history for pipeline",
|
|
254
252
|
},
|
|
255
253
|
});
|
|
256
254
|
}
|
|
@@ -290,7 +288,7 @@ export const registerHistoryHandlers = (context: PipelabContext) => {
|
|
|
290
288
|
handle("build-history:configure", async (_, { send, value }) => {
|
|
291
289
|
try {
|
|
292
290
|
logger().info("Updating build history configuration:", value.config);
|
|
293
|
-
|
|
291
|
+
|
|
294
292
|
const settings = await setupConfigFile<AppConfig>("settings", { context });
|
|
295
293
|
const currentConfig = await settings.getConfig();
|
|
296
294
|
|
package/src/handlers/index.ts
CHANGED
|
@@ -26,10 +26,10 @@ export const registerAllHandlers = async (options: {
|
|
|
26
26
|
registerSystemHandlers(options);
|
|
27
27
|
|
|
28
28
|
const { registerPlugins } = usePlugins();
|
|
29
|
-
|
|
29
|
+
// Execute in the background! The plugins will be dynamically registered and broadcasted to the UI.
|
|
30
|
+
builtInPlugins({
|
|
30
31
|
context,
|
|
31
32
|
});
|
|
32
|
-
registerPlugins(plugins as any);
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
export { registerShellHandlers } from "./shell";
|
package/src/index.ts
CHANGED
package/src/plugins-registry.ts
CHANGED
|
@@ -23,9 +23,9 @@ const DEFAULT_PLUGIN_IDS = [
|
|
|
23
23
|
export const loadPipelabPlugin = async (id: string, options: { context: PipelabContext }) => {
|
|
24
24
|
try {
|
|
25
25
|
const packageName = `@pipelab/plugin-${id}`;
|
|
26
|
-
const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName,
|
|
26
|
+
const { packageDir, entryPoint } = await fetchPipelabPlugin(packageName, options.context.releaseTag, {
|
|
27
27
|
context: options.context,
|
|
28
|
-
installDeps:
|
|
28
|
+
installDeps: false,
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
console.log(`[Plugins] [${id}] Attempting to import from: ${entryPoint}`);
|
|
@@ -34,7 +34,7 @@ export const loadPipelabPlugin = async (id: string, options: { context: PipelabC
|
|
|
34
34
|
try {
|
|
35
35
|
const files = await readdir(packageDir, { recursive: true });
|
|
36
36
|
console.log(`[Plugins] [${id}] Directory contents:`, files);
|
|
37
|
-
} catch (e) {
|
|
37
|
+
} catch (e) {}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const pluginModule = await import(pathToFileURL(entryPoint).href);
|
|
@@ -52,27 +52,32 @@ export const loadPipelabPlugin = async (id: string, options: { context: PipelabC
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
export const builtInPlugins = async (options: { context: PipelabContext }) => {
|
|
55
|
-
console.log("[Plugins]
|
|
55
|
+
console.log("[Plugins] Starting background plugin loading...");
|
|
56
56
|
|
|
57
57
|
// Pre-ensure Node.js and PNPM once in parallel so plugins don't have to wait for them
|
|
58
58
|
sendStartupProgress("Preparing environment...");
|
|
59
|
-
await Promise.all([
|
|
60
|
-
ensureNodeJS(options.context),
|
|
61
|
-
ensurePNPM(options.context),
|
|
62
|
-
]);
|
|
59
|
+
await Promise.all([ensureNodeJS(options.context), ensurePNPM(options.context)]);
|
|
63
60
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return loadPipelabPlugin(id, options);
|
|
68
|
-
}),
|
|
69
|
-
);
|
|
61
|
+
const { usePlugins } = await import("@pipelab/shared");
|
|
62
|
+
const { registerPlugins } = usePlugins();
|
|
63
|
+
const { webSocketServer } = await import("./index");
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
// Load plugins asynchronously in the background
|
|
66
|
+
(async () => {
|
|
67
|
+
for (const id of DEFAULT_PLUGIN_IDS) {
|
|
68
|
+
sendStartupProgress(`Loading plugin: ${id}`);
|
|
69
|
+
const plugin = await loadPipelabPlugin(id, options);
|
|
70
|
+
if (plugin) {
|
|
71
|
+
registerPlugins([plugin]);
|
|
72
|
+
webSocketServer.broadcast("plugin:loaded", { plugin });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.log("[Plugins] All default plugins loaded.");
|
|
76
|
+
sendStartupProgress("All plugins loaded.");
|
|
77
|
+
setTimeout(() => {
|
|
78
|
+
webSocketServer.broadcast("startup:progress", { type: "ready" });
|
|
79
|
+
}, 2000);
|
|
80
|
+
})();
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
console.log(`[Plugins] Successfully loaded ${filtered.length} default plugins`);
|
|
77
|
-
return filtered;
|
|
82
|
+
return [];
|
|
78
83
|
};
|
package/src/runner.ts
CHANGED
|
@@ -79,10 +79,7 @@ export async function runPipelineCommand(file: string, options: RunOptions, vers
|
|
|
79
79
|
const cloudRunId = process.env.CLOUD_RUN_ID;
|
|
80
80
|
if (options.cloud && cloudRunId) {
|
|
81
81
|
const { createClient } = await import("@supabase/supabase-js");
|
|
82
|
-
supabase = createClient(
|
|
83
|
-
process.env.SUPABASE_URL!,
|
|
84
|
-
process.env.SUPABASE_SERVICE_ROLE_KEY!
|
|
85
|
-
);
|
|
82
|
+
supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
|
|
86
83
|
console.log(`Cloud mode enabled. Streaming logs for run: ${cloudRunId}`);
|
|
87
84
|
}
|
|
88
85
|
|
|
@@ -97,23 +94,29 @@ export async function runPipelineCommand(file: string, options: RunOptions, vers
|
|
|
97
94
|
onNodeEnter: (node) => {
|
|
98
95
|
console.log(`[ENTER] ${node.name} (${node.uid})`);
|
|
99
96
|
if (supabase) {
|
|
100
|
-
supabase
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
supabase
|
|
98
|
+
.from("cloud_run_logs")
|
|
99
|
+
.insert({
|
|
100
|
+
run_id: cloudRunId,
|
|
101
|
+
message: `[ENTER] ${node.name} (${node.uid})`,
|
|
102
|
+
node_uid: node.uid,
|
|
103
|
+
type: "node-enter",
|
|
104
|
+
})
|
|
105
|
+
.then();
|
|
106
106
|
}
|
|
107
107
|
},
|
|
108
108
|
onNodeExit: (node) => {
|
|
109
109
|
console.log(`[EXIT] ${node.name} (${node.uid})`);
|
|
110
110
|
if (supabase) {
|
|
111
|
-
supabase
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
supabase
|
|
112
|
+
.from("cloud_run_logs")
|
|
113
|
+
.insert({
|
|
114
|
+
run_id: cloudRunId,
|
|
115
|
+
message: `[EXIT] ${node.name} (${node.uid})`,
|
|
116
|
+
node_uid: node.uid,
|
|
117
|
+
type: "node-exit",
|
|
118
|
+
})
|
|
119
|
+
.then();
|
|
117
120
|
}
|
|
118
121
|
},
|
|
119
122
|
onLog: (data, node) => {
|
|
@@ -121,12 +124,15 @@ export async function runPipelineCommand(file: string, options: RunOptions, vers
|
|
|
121
124
|
const message = data.data.message.join(" ");
|
|
122
125
|
console.log(`[LOG] ${message}`);
|
|
123
126
|
if (supabase) {
|
|
124
|
-
supabase
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
supabase
|
|
128
|
+
.from("cloud_run_logs")
|
|
129
|
+
.insert({
|
|
130
|
+
run_id: cloudRunId,
|
|
131
|
+
message: message,
|
|
132
|
+
node_uid: node?.uid,
|
|
133
|
+
type: "log",
|
|
134
|
+
})
|
|
135
|
+
.then();
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
138
|
},
|
package/src/server.ts
CHANGED
|
@@ -38,13 +38,15 @@ export const sendStartupReady = () => {
|
|
|
38
38
|
|
|
39
39
|
export async function serveCommand(options: ServeOptions, version: string, _dirname: string) {
|
|
40
40
|
if (!options.userData) throw new Error("userDataPath is required for serveCommand");
|
|
41
|
+
const releaseTag = version.includes("beta") ? "beta" : "latest";
|
|
41
42
|
const context = new PipelabContext({
|
|
42
43
|
userDataPath: options.userData,
|
|
44
|
+
releaseTag,
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
let rawAssetFolder: string | undefined;
|
|
46
48
|
if (!isDev) {
|
|
47
|
-
rawAssetFolder = await fetchPipelabAsset("@pipelab/ui",
|
|
49
|
+
rawAssetFolder = await fetchPipelabAsset("@pipelab/ui", releaseTag, { context });
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
const server = http.createServer(async (request, response) => {
|
|
@@ -66,7 +68,7 @@ export async function serveCommand(options: ServeOptions, version: string, _dirn
|
|
|
66
68
|
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
67
69
|
response.end(
|
|
68
70
|
`Error: UI directory not found at ${rawAssetFolder}.\n` +
|
|
69
|
-
|
|
71
|
+
"Please run 'pnpm build' in apps/ui to generate the distribution.",
|
|
70
72
|
);
|
|
71
73
|
return;
|
|
72
74
|
}
|
package/src/utils/fs-extras.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { mkdir, createWriteStream, createReadStream } from "node:fs";
|
|
2
2
|
import { execa, Options, Subprocess } from "execa";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
mkdir as mkdirP,
|
|
5
|
+
access,
|
|
6
|
+
writeFile,
|
|
7
|
+
realpath,
|
|
8
|
+
mkdtemp,
|
|
9
|
+
chmod,
|
|
10
|
+
stat,
|
|
11
|
+
readdir,
|
|
12
|
+
} from "node:fs/promises";
|
|
4
13
|
import { join, dirname } from "node:path";
|
|
5
14
|
import { tmpdir } from "node:os";
|
|
6
15
|
import tar from "tar";
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import semver from "semver";
|
|
2
|
+
|
|
3
|
+
export interface GitHubRelease {
|
|
4
|
+
tag_name: string;
|
|
5
|
+
prerelease: boolean;
|
|
6
|
+
published_at: string;
|
|
7
|
+
html_url: string;
|
|
8
|
+
assets: Array<{
|
|
9
|
+
name: string;
|
|
10
|
+
browser_download_url: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface FetchReleaseOptions {
|
|
15
|
+
repo?: string;
|
|
16
|
+
allowPrerelease?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetches all releases for a specific package from GitHub.
|
|
21
|
+
* Tags are expected to follow the pattern "{packageName}@{version}"
|
|
22
|
+
*/
|
|
23
|
+
export async function fetchPackageReleases(
|
|
24
|
+
packageName: string,
|
|
25
|
+
options: FetchReleaseOptions = {},
|
|
26
|
+
): Promise<GitHubRelease[]> {
|
|
27
|
+
const { repo = "CynToolkit/pipelab", allowPrerelease = false } = options;
|
|
28
|
+
const url = `https://api.github.com/repos/${repo}/releases`;
|
|
29
|
+
|
|
30
|
+
console.log(`[GitHub] Fetching releases for ${packageName} from ${url}...`);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
headers: {
|
|
35
|
+
"User-Agent": "Pipelab-Desktop-Updater",
|
|
36
|
+
Accept: "application/vnd.github.v3+json",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const releases: GitHubRelease[] = await response.json();
|
|
45
|
+
|
|
46
|
+
// Filter for releases that follow the {packageName}@X.Y.Z tag pattern
|
|
47
|
+
const packageReleases = releases
|
|
48
|
+
.filter((r) => r.tag_name.startsWith(`${packageName}@`))
|
|
49
|
+
.filter((r) => allowPrerelease || !r.prerelease);
|
|
50
|
+
|
|
51
|
+
return packageReleases;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(`[GitHub] Failed to fetch releases for ${packageName}:`, error);
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetches the latest release for a specific package.
|
|
60
|
+
* Supports PIPELAB_OVERRIDE_RELEASE environment variable.
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchLatestPackageRelease(
|
|
63
|
+
packageName: string,
|
|
64
|
+
options: FetchReleaseOptions = {},
|
|
65
|
+
): Promise<GitHubRelease | null> {
|
|
66
|
+
const releases = await fetchPackageReleases(packageName, options);
|
|
67
|
+
|
|
68
|
+
if (releases.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle environment variable override
|
|
73
|
+
const override = process.env.PIPELAB_OVERRIDE_RELEASE;
|
|
74
|
+
if (override) {
|
|
75
|
+
const targetTag = override.includes("@") ? override : `${packageName}@${override}`;
|
|
76
|
+
const release = releases.find((r) => r.tag_name === targetTag);
|
|
77
|
+
if (release) {
|
|
78
|
+
console.log(
|
|
79
|
+
`[GitHub] Using release override from PIPELAB_OVERRIDE_RELEASE: ${release.tag_name}`,
|
|
80
|
+
);
|
|
81
|
+
return release;
|
|
82
|
+
}
|
|
83
|
+
console.warn(`[GitHub] Override release ${override} not found for package ${packageName}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Sort by semver
|
|
87
|
+
const getVersion = (tagName: string) => tagName.split("@").pop() || "0.0.0";
|
|
88
|
+
|
|
89
|
+
releases.sort((a, b) => {
|
|
90
|
+
const vA = getVersion(a.tag_name);
|
|
91
|
+
const vB = getVersion(b.tag_name);
|
|
92
|
+
if (semver.valid(vA) && semver.valid(vB)) {
|
|
93
|
+
return semver.rcompare(vA, vB);
|
|
94
|
+
}
|
|
95
|
+
return new Date(b.published_at).getTime() - new Date(a.published_at).getTime();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return releases[0];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Fetches all releases from GitHub and finds the latest one that matches the Pipelab Desktop app tag.
|
|
103
|
+
* This ensures we don't accidentally pick a package release (e.g. @pipelab/shared) as the "latest".
|
|
104
|
+
*/
|
|
105
|
+
export async function fetchLatestDesktopRelease(
|
|
106
|
+
options: FetchReleaseOptions = {},
|
|
107
|
+
): Promise<GitHubRelease | null> {
|
|
108
|
+
const latest = await fetchLatestPackageRelease("@pipelab/app", options);
|
|
109
|
+
|
|
110
|
+
if (latest) {
|
|
111
|
+
console.log(
|
|
112
|
+
`[GitHub] Found latest desktop release: ${latest.tag_name} (${latest.prerelease ? "pre-release" : "stable"})`,
|
|
113
|
+
);
|
|
114
|
+
} else {
|
|
115
|
+
console.warn(`[GitHub] No desktop releases found`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return latest;
|
|
119
|
+
}
|