@pipelab/core-node 1.0.0-beta.2 → 1.0.0-beta.21
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 +1 -1
- package/CHANGELOG.md +164 -0
- package/dist/config-Bi0ORcTK.mjs +2 -0
- package/dist/config-CFgGRD9U.mjs +265 -0
- package/dist/config-CFgGRD9U.mjs.map +1 -0
- package/dist/index.d.mts +74 -53
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1705 -576
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -5
- package/scratch/simulate-updates.ts +120 -0
- package/src/config.test.ts +232 -0
- package/src/config.ts +111 -50
- package/src/context.ts +117 -5
- package/src/handler-func.ts +2 -77
- package/src/handlers/build-history.ts +60 -90
- package/src/handlers/config.ts +265 -55
- package/src/handlers/engine.ts +32 -35
- package/src/handlers/history.ts +0 -40
- package/src/handlers/index.ts +4 -0
- package/src/handlers/migration.ts +621 -0
- package/src/handlers/plugins.ts +305 -0
- package/src/handlers/system.ts +7 -2
- package/src/index.ts +9 -2
- package/src/plugins-registry.ts +280 -38
- package/src/presets/c3toSteam.ts +13 -7
- package/src/presets/demo.ts +9 -9
- package/src/presets/list.ts +0 -6
- package/src/presets/moreToCome.ts +3 -2
- package/src/presets/newProject.ts +3 -2
- package/src/presets/test-c3-offline.ts +15 -6
- package/src/presets/test-c3-unzip.ts +19 -8
- package/src/runner.ts +2 -8
- package/src/server.ts +45 -4
- package/src/types/runner.ts +2 -30
- package/src/utils/fs-extras.ts +6 -24
- package/src/utils/github.test.ts +211 -0
- package/src/utils/github.ts +90 -10
- package/src/utils/remote.test.ts +209 -0
- package/src/utils/remote.ts +325 -87
- package/src/utils/storage.ts +2 -1
- package/src/utils.ts +20 -24
- package/src/migrations.ts +0 -72
- package/src/presets/if.ts +0 -69
- package/src/presets/loop.ts +0 -65
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.21",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The Pipelab automation engine for Node.js",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"type-fest": "4.39.0",
|
|
41
41
|
"ws": "8.18.3",
|
|
42
42
|
"yauzl": "2.10.0",
|
|
43
|
-
"@pipelab/constants": "1.0.0-beta.
|
|
44
|
-
"@pipelab/shared": "1.0.0-beta.
|
|
43
|
+
"@pipelab/constants": "1.0.0-beta.19",
|
|
44
|
+
"@pipelab/shared": "1.0.0-beta.17"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/adm-zip": "0.5.7",
|
|
@@ -52,13 +52,17 @@
|
|
|
52
52
|
"@types/tar": "7.0.87",
|
|
53
53
|
"@types/ws": "8.18.1",
|
|
54
54
|
"@types/yauzl": "2.10.3",
|
|
55
|
+
"memfs": "4.57.6",
|
|
55
56
|
"tsdown": "0.21.2",
|
|
56
57
|
"typescript": "^5.0.0",
|
|
57
|
-
"
|
|
58
|
+
"vitest": "3.1.4",
|
|
59
|
+
"@pipelab/tsconfig": "1.0.0-beta.17"
|
|
58
60
|
},
|
|
59
61
|
"scripts": {
|
|
60
62
|
"build": "tsdown",
|
|
61
63
|
"format": "oxfmt .",
|
|
62
|
-
"lint": "oxlint ."
|
|
64
|
+
"lint": "oxlint .",
|
|
65
|
+
"typecheck": "tsc -b",
|
|
66
|
+
"test": "vitest run"
|
|
63
67
|
}
|
|
64
68
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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(
|
|
48
|
+
`[STABLE] Resolved version: ${stableApp ? stableApp.tag_name : "None (Stable users protected)"}`,
|
|
49
|
+
);
|
|
50
|
+
console.log(`[BETA] Resolved version: ${betaApp ? betaApp.tag_name : "None"}`);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error("Failed to fetch desktop releases from GitHub:", error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log("\n--- 2. Simulating NPM Packages (CLI, UI, Plugins, Assets) ---");
|
|
56
|
+
console.log("Fetching data from NPM registry. Please wait...\n");
|
|
57
|
+
|
|
58
|
+
const results: Array<{
|
|
59
|
+
package: string;
|
|
60
|
+
stable: string;
|
|
61
|
+
beta: string;
|
|
62
|
+
notes: string;
|
|
63
|
+
}> = [];
|
|
64
|
+
|
|
65
|
+
for (const pkg of PACKAGES) {
|
|
66
|
+
try {
|
|
67
|
+
const packument = await pacote.packument(pkg);
|
|
68
|
+
const stableVersion = packument["dist-tags"]?.latest || "N/A";
|
|
69
|
+
const betaVersion = packument["dist-tags"]?.beta || "N/A";
|
|
70
|
+
|
|
71
|
+
let notes = "";
|
|
72
|
+
if (stableVersion === "N/A" && betaVersion === "N/A") {
|
|
73
|
+
notes = "⚠️ Package not published on NPM";
|
|
74
|
+
} else if (betaVersion !== "N/A" && stableVersion !== "N/A") {
|
|
75
|
+
if (betaVersion === stableVersion) {
|
|
76
|
+
notes = "Stable and Beta are in sync";
|
|
77
|
+
} else {
|
|
78
|
+
// Attempt semver comparison
|
|
79
|
+
const cleanStable = semver.coerce(stableVersion);
|
|
80
|
+
const cleanBeta = semver.coerce(betaVersion);
|
|
81
|
+
if (cleanStable && cleanBeta) {
|
|
82
|
+
const comp = semver.compare(stableVersion, betaVersion);
|
|
83
|
+
if (comp < 0) {
|
|
84
|
+
notes = "🚀 Beta is ahead";
|
|
85
|
+
} else if (comp > 0) {
|
|
86
|
+
notes = "Stable is ahead";
|
|
87
|
+
} else {
|
|
88
|
+
notes = "Stable and Beta are in sync (coerced)";
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
notes = "Different versions (non-semver)";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} else if (betaVersion !== "N/A" && stableVersion === "N/A") {
|
|
95
|
+
notes = "Only Beta version exists";
|
|
96
|
+
} else if (stableVersion !== "N/A" && betaVersion === "N/A") {
|
|
97
|
+
notes = "No Beta tag published";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
results.push({
|
|
101
|
+
package: pkg,
|
|
102
|
+
stable: stableVersion,
|
|
103
|
+
beta: betaVersion,
|
|
104
|
+
notes,
|
|
105
|
+
});
|
|
106
|
+
} catch (error: any) {
|
|
107
|
+
results.push({
|
|
108
|
+
package: pkg,
|
|
109
|
+
stable: "ERROR",
|
|
110
|
+
beta: "ERROR",
|
|
111
|
+
notes: `Failed to fetch: ${error.message}`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.table(results);
|
|
117
|
+
console.log("\n==========================================\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
simulate().catch(console.error);
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
setupConfigFile,
|
|
4
|
+
setupProjectsConfigFile,
|
|
5
|
+
deletePipelineConfigFileByName,
|
|
6
|
+
deletePipelineConfigFileByPath,
|
|
7
|
+
} from "./config";
|
|
8
|
+
import { FileRepo } from "@pipelab/shared";
|
|
9
|
+
import { PipelabContext } from "./context";
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { vol } from "memfs";
|
|
14
|
+
|
|
15
|
+
// Mock the node:fs and node:fs/promises modules to use memfs
|
|
16
|
+
vi.mock("node:fs", async () => {
|
|
17
|
+
const memfs = await import("memfs");
|
|
18
|
+
return {
|
|
19
|
+
...memfs.fs,
|
|
20
|
+
default: memfs.fs,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock("node:fs/promises", async () => {
|
|
25
|
+
const memfs = await import("memfs");
|
|
26
|
+
return {
|
|
27
|
+
...memfs.fs.promises,
|
|
28
|
+
default: memfs.fs.promises,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("setupConfigFile & Backup Creation", () => {
|
|
33
|
+
let tempDir: string;
|
|
34
|
+
let context: PipelabContext;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vol.reset();
|
|
38
|
+
tempDir = "/tmp/pipelab-test-config";
|
|
39
|
+
context = new PipelabContext({ userDataPath: tempDir });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("should migrate config and create backup files on disk", async () => {
|
|
43
|
+
// 1. Setup V1 configuration file in the context's config path
|
|
44
|
+
const configDir = context.getConfigPath();
|
|
45
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
46
|
+
|
|
47
|
+
const projectsFilePath = context.getProjectsPath();
|
|
48
|
+
|
|
49
|
+
const v1Config = {
|
|
50
|
+
version: "1.0.0",
|
|
51
|
+
data: {
|
|
52
|
+
"pipeline-1": {
|
|
53
|
+
project: "main",
|
|
54
|
+
type: "internal",
|
|
55
|
+
configName: "pipeline-1",
|
|
56
|
+
lastModified: "2026-06-04",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await fs.writeFile(projectsFilePath, JSON.stringify(v1Config));
|
|
62
|
+
|
|
63
|
+
// 2. Initialize setupConfigFile
|
|
64
|
+
const configInstance = await setupProjectsConfigFile(context);
|
|
65
|
+
|
|
66
|
+
// Verify file exists
|
|
67
|
+
expect(existsSync(projectsFilePath)).toBe(true);
|
|
68
|
+
|
|
69
|
+
// 3. Retrieve config (this triggers the migration process and onStep callback)
|
|
70
|
+
const migratedConfig = await configInstance.getConfig();
|
|
71
|
+
|
|
72
|
+
// 4. Assert the main config is updated on disk to version 3.0.0
|
|
73
|
+
expect(migratedConfig.version).toBe("3.0.0");
|
|
74
|
+
|
|
75
|
+
const updatedContent = JSON.parse(await fs.readFile(projectsFilePath, "utf8"));
|
|
76
|
+
expect(updatedContent.version).toBe("3.0.0");
|
|
77
|
+
|
|
78
|
+
// 5. Assert that the intermediate backup file was created on disk
|
|
79
|
+
const backupFilePath = path.join(configDir, "projects.v2.0.0.json");
|
|
80
|
+
expect(existsSync(backupFilePath)).toBe(true);
|
|
81
|
+
|
|
82
|
+
const backupContent = JSON.parse(await fs.readFile(backupFilePath, "utf8"));
|
|
83
|
+
expect(backupContent.version).toBe("2.0.0");
|
|
84
|
+
expect(backupContent.projects).toHaveLength(1);
|
|
85
|
+
expect(backupContent.pipelines).toHaveLength(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("should create default config file if missing on setup", async () => {
|
|
89
|
+
const configDir = context.getConfigPath();
|
|
90
|
+
const projectsFilePath = context.getProjectsPath();
|
|
91
|
+
|
|
92
|
+
expect(existsSync(projectsFilePath)).toBe(false);
|
|
93
|
+
|
|
94
|
+
// Initialize setupConfigFile
|
|
95
|
+
await setupProjectsConfigFile(context);
|
|
96
|
+
|
|
97
|
+
// Should create file with default value
|
|
98
|
+
expect(existsSync(projectsFilePath)).toBe(true);
|
|
99
|
+
const content = JSON.parse(await fs.readFile(projectsFilePath, "utf8"));
|
|
100
|
+
expect(content.version).toBe("3.0.0");
|
|
101
|
+
expect(content.projects).toHaveLength(1);
|
|
102
|
+
expect(content.pipelines).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("should fallback to default config and preserve corrupted file if parsing fails", async () => {
|
|
106
|
+
const configDir = context.getConfigPath();
|
|
107
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
108
|
+
const projectsFilePath = context.getProjectsPath();
|
|
109
|
+
|
|
110
|
+
await fs.writeFile(projectsFilePath, "{ corrupted json... }");
|
|
111
|
+
|
|
112
|
+
const configInstance = await setupProjectsConfigFile(context);
|
|
113
|
+
const config = await configInstance.getConfig();
|
|
114
|
+
|
|
115
|
+
expect(config.version).toBe("3.0.0");
|
|
116
|
+
expect(config.projects).toHaveLength(1);
|
|
117
|
+
expect(config.pipelines).toEqual([]);
|
|
118
|
+
|
|
119
|
+
// Verify a timestamped corrupted backup file exists
|
|
120
|
+
const files = await fs.readdir(configDir);
|
|
121
|
+
const corruptedFile = files.find(
|
|
122
|
+
(f) => f.startsWith("projects.corrupted.") && f.endsWith(".json"),
|
|
123
|
+
);
|
|
124
|
+
expect(corruptedFile).toBeDefined();
|
|
125
|
+
const corruptedContent = await fs.readFile(path.join(configDir, corruptedFile!), "utf8");
|
|
126
|
+
expect(corruptedContent).toBe("{ corrupted json... }");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("should fallback to default config and preserve file if migration throws", async () => {
|
|
130
|
+
const configDir = context.getConfigPath();
|
|
131
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
132
|
+
const projectsFilePath = context.getProjectsPath();
|
|
133
|
+
|
|
134
|
+
const customMigrator = {
|
|
135
|
+
defaultValue: { version: "2.0.0", projects: [], pipelines: [] } as any,
|
|
136
|
+
migrate: async () => {
|
|
137
|
+
throw new Error("Migration failed!");
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
await fs.writeFile(projectsFilePath, JSON.stringify({ version: "1.0.0", invalidData: true }));
|
|
142
|
+
|
|
143
|
+
const configInstance = await setupConfigFile<any>(context.getProjectsPath(), {
|
|
144
|
+
context,
|
|
145
|
+
migrator: customMigrator,
|
|
146
|
+
});
|
|
147
|
+
const config = await configInstance.getConfig();
|
|
148
|
+
|
|
149
|
+
expect(config.version).toBe("2.0.0");
|
|
150
|
+
|
|
151
|
+
// Verify a timestamped corrupted backup file exists
|
|
152
|
+
const files = await fs.readdir(configDir);
|
|
153
|
+
const corruptedFile = files.find(
|
|
154
|
+
(f) => f.startsWith("projects.corrupted.") && f.endsWith(".json"),
|
|
155
|
+
);
|
|
156
|
+
expect(corruptedFile).toBeDefined();
|
|
157
|
+
const corruptedContent = JSON.parse(
|
|
158
|
+
await fs.readFile(path.join(configDir, corruptedFile!), "utf8"),
|
|
159
|
+
);
|
|
160
|
+
expect(corruptedContent.invalidData).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("should save config to disk via setConfig", async () => {
|
|
164
|
+
const configInstance = await setupProjectsConfigFile(context);
|
|
165
|
+
const initialConfig = await configInstance.getConfig();
|
|
166
|
+
|
|
167
|
+
const newConfig: FileRepo = {
|
|
168
|
+
...initialConfig,
|
|
169
|
+
pipelines: [
|
|
170
|
+
{
|
|
171
|
+
id: "pipeline-new",
|
|
172
|
+
project: "main",
|
|
173
|
+
type: "internal",
|
|
174
|
+
configName: "pipeline-new",
|
|
175
|
+
lastModified: "2026-06-05",
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const success = await configInstance.setConfig(newConfig);
|
|
181
|
+
expect(success).toBe(true);
|
|
182
|
+
|
|
183
|
+
const savedContent = JSON.parse(
|
|
184
|
+
await fs.readFile(context.getProjectsPath(), "utf8"),
|
|
185
|
+
);
|
|
186
|
+
expect(savedContent.pipelines).toHaveLength(1);
|
|
187
|
+
expect(savedContent.pipelines[0].id).toBe("pipeline-new");
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("deletePipelineConfigFile", () => {
|
|
192
|
+
let tempDir: string;
|
|
193
|
+
let context: PipelabContext;
|
|
194
|
+
|
|
195
|
+
beforeEach(() => {
|
|
196
|
+
vol.reset();
|
|
197
|
+
tempDir = "/tmp/pipelab-test-config";
|
|
198
|
+
context = new PipelabContext({ userDataPath: tempDir });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("should delete pipeline config file using relative name", async () => {
|
|
202
|
+
const configDir = context.getConfigPath();
|
|
203
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
204
|
+
|
|
205
|
+
const pipelineFilePath = path.join(configDir, "my-pipeline.json");
|
|
206
|
+
await fs.writeFile(pipelineFilePath, JSON.stringify({ name: "My Pipeline" }));
|
|
207
|
+
expect(existsSync(pipelineFilePath)).toBe(true);
|
|
208
|
+
|
|
209
|
+
await deletePipelineConfigFileByName("my-pipeline", context);
|
|
210
|
+
expect(existsSync(pipelineFilePath)).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("should delete pipeline config file using absolute path", async () => {
|
|
214
|
+
const configDir = context.getConfigPath();
|
|
215
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
216
|
+
|
|
217
|
+
const pipelineFilePath = path.join(configDir, "another-pipeline.json");
|
|
218
|
+
await fs.writeFile(pipelineFilePath, JSON.stringify({ name: "Another Pipeline" }));
|
|
219
|
+
expect(existsSync(pipelineFilePath)).toBe(true);
|
|
220
|
+
|
|
221
|
+
await deletePipelineConfigFileByPath(pipelineFilePath, context);
|
|
222
|
+
expect(existsSync(pipelineFilePath)).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("should not throw error when attempting to delete non-existent pipeline file", async () => {
|
|
226
|
+
const nonExistentPath = context.getConfigPath("does-not-exist.json");
|
|
227
|
+
expect(existsSync(nonExistentPath)).toBe(false);
|
|
228
|
+
|
|
229
|
+
await expect(deletePipelineConfigFileByName("does-not-exist", context)).resolves.not.toThrow();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
package/src/config.ts
CHANGED
|
@@ -2,36 +2,26 @@ import { PipelabContext } from "./context";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ensure } from "./utils/fs-extras";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return undefined;
|
|
18
|
-
};
|
|
5
|
+
import {
|
|
6
|
+
useLogger,
|
|
7
|
+
Migrator,
|
|
8
|
+
AppConfig,
|
|
9
|
+
ConnectionsConfig,
|
|
10
|
+
FileRepo,
|
|
11
|
+
SavedFile,
|
|
12
|
+
appSettingsMigrator,
|
|
13
|
+
connectionsMigrator,
|
|
14
|
+
fileRepoMigrations,
|
|
15
|
+
savedFileMigrator,
|
|
16
|
+
} from "@pipelab/shared";
|
|
19
17
|
|
|
20
18
|
export const setupConfigFile = async <T>(
|
|
21
|
-
|
|
22
|
-
options: { context: PipelabContext; migrator
|
|
19
|
+
filesPath: string,
|
|
20
|
+
options: { context: PipelabContext; migrator: Migrator<T> },
|
|
23
21
|
) => {
|
|
24
22
|
const ctx = options.context;
|
|
25
|
-
const
|
|
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`);
|
|
23
|
+
const parsedPath = path.parse(filesPath);
|
|
24
|
+
const migrator = options.migrator;
|
|
35
25
|
|
|
36
26
|
await ensure(filesPath, JSON.stringify(migrator.defaultValue));
|
|
37
27
|
|
|
@@ -42,7 +32,7 @@ export const setupConfigFile = async <T>(
|
|
|
42
32
|
await fs.writeFile(filesPath, JSON.stringify(config));
|
|
43
33
|
return true;
|
|
44
34
|
} catch (e) {
|
|
45
|
-
logger().error(`Error saving config ${name}:`, e);
|
|
35
|
+
logger().error(`Error saving config ${parsedPath.name}:`, e);
|
|
46
36
|
return false;
|
|
47
37
|
}
|
|
48
38
|
},
|
|
@@ -50,6 +40,7 @@ export const setupConfigFile = async <T>(
|
|
|
50
40
|
const { logger } = useLogger();
|
|
51
41
|
let content = undefined;
|
|
52
42
|
let originalJson: any = undefined;
|
|
43
|
+
let parseFailed = false;
|
|
53
44
|
|
|
54
45
|
try {
|
|
55
46
|
content = await fs.readFile(filesPath, "utf8");
|
|
@@ -57,45 +48,66 @@ export const setupConfigFile = async <T>(
|
|
|
57
48
|
originalJson = JSON.parse(content);
|
|
58
49
|
}
|
|
59
50
|
} catch (e) {
|
|
60
|
-
logger().error(`Error reading or parsing config ${name}:`, e);
|
|
51
|
+
logger().error(`Error reading or parsing config ${parsedPath.name}:`, e);
|
|
52
|
+
parseFailed = true;
|
|
61
53
|
}
|
|
62
54
|
|
|
63
|
-
// console.log("content", content);
|
|
64
|
-
|
|
65
55
|
let json: any = undefined;
|
|
56
|
+
let migrationFailed = false;
|
|
66
57
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
58
|
+
if (!parseFailed) {
|
|
59
|
+
json = await migrator.migrate(originalJson, {
|
|
60
|
+
debug: false,
|
|
61
|
+
onStep: async (state: any, version: string) => {
|
|
62
|
+
const versionedPath = path.join(parsedPath.dir, `${parsedPath.name}.v${version}.json`);
|
|
63
|
+
try {
|
|
64
|
+
await fs.writeFile(versionedPath, JSON.stringify(state));
|
|
65
|
+
logger().info(`Intermediate backup created for ${parsedPath.name} at ${versionedPath}`);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
logger().error(
|
|
68
|
+
`Failed to create intermediate backup for ${parsedPath.name} at v${version}:`,
|
|
69
|
+
e,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
} else {
|
|
75
|
+
json = migrator.defaultValue;
|
|
76
|
+
}
|
|
70
77
|
} catch (e) {
|
|
71
|
-
logger().error(`Error migrating config ${name}:`, e);
|
|
78
|
+
logger().error(`Error migrating config ${parsedPath.name}:`, e);
|
|
79
|
+
migrationFailed = true;
|
|
72
80
|
json = migrator.defaultValue;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
const originalVersion = originalJson?.version;
|
|
76
84
|
const newVersion = json?.version;
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const shouldSaveBack =
|
|
87
|
+
originalVersion !== newVersion || content === undefined || parseFailed || migrationFailed;
|
|
88
|
+
|
|
89
|
+
if (shouldSaveBack) {
|
|
90
|
+
if (parseFailed || migrationFailed) {
|
|
91
|
+
try {
|
|
92
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
93
|
+
const corruptedPath = path.join(
|
|
94
|
+
parsedPath.dir,
|
|
95
|
+
`${parsedPath.name}.corrupted.${timestamp}.json`,
|
|
96
|
+
);
|
|
97
|
+
const backupContent = parseFailed
|
|
98
|
+
? content || ""
|
|
99
|
+
: JSON.stringify(originalJson, null, 2);
|
|
100
|
+
await fs.writeFile(corruptedPath, backupContent);
|
|
101
|
+
logger().info(`Corrupted config file preserved at ${corruptedPath}`);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
logger().error(`Failed to backup corrupted config ${parsedPath.name}:`, e);
|
|
104
|
+
}
|
|
90
105
|
}
|
|
91
|
-
}
|
|
92
106
|
|
|
93
|
-
// Save back migrated config if changed or if it's a new file
|
|
94
|
-
if (originalVersion !== newVersion || content === undefined) {
|
|
95
107
|
try {
|
|
96
108
|
await fs.writeFile(filesPath, JSON.stringify(json));
|
|
97
109
|
} catch (e) {
|
|
98
|
-
logger().error(`Error saving migrated config ${name}:`, e);
|
|
110
|
+
logger().error(`Error saving migrated config ${parsedPath.name}:`, e);
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
|
|
@@ -103,3 +115,52 @@ export const setupConfigFile = async <T>(
|
|
|
103
115
|
},
|
|
104
116
|
};
|
|
105
117
|
};
|
|
118
|
+
|
|
119
|
+
export const setupSettingsConfigFile = (context: PipelabContext) => {
|
|
120
|
+
return setupConfigFile<AppConfig>(context.getSettingsPath(), {
|
|
121
|
+
context,
|
|
122
|
+
migrator: appSettingsMigrator,
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const setupConnectionsConfigFile = (context: PipelabContext) => {
|
|
127
|
+
return setupConfigFile<ConnectionsConfig>(context.getConnectionsPath(), {
|
|
128
|
+
context,
|
|
129
|
+
migrator: connectionsMigrator,
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const setupProjectsConfigFile = (context: PipelabContext) => {
|
|
134
|
+
return setupConfigFile<FileRepo>(context.getProjectsPath(), {
|
|
135
|
+
context,
|
|
136
|
+
migrator: fileRepoMigrations,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const setupPipelineConfigFileByName = (name: string, context: PipelabContext) => {
|
|
141
|
+
const filesPath = context.getConfigPath(`${name}.json`);
|
|
142
|
+
return setupConfigFile<SavedFile>(filesPath, {
|
|
143
|
+
context,
|
|
144
|
+
migrator: savedFileMigrator,
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const setupPipelineConfigFileByPath = (absolutePath: string, context: PipelabContext) => {
|
|
149
|
+
return setupConfigFile<SavedFile>(absolutePath, {
|
|
150
|
+
context,
|
|
151
|
+
migrator: savedFileMigrator,
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const deleteConfigFile = async (filesPath: string) => {
|
|
156
|
+
await fs.rm(filesPath, { force: true });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const deletePipelineConfigFileByName = async (name: string, context: PipelabContext) => {
|
|
160
|
+
const filesPath = context.getConfigPath(`${name}.json`);
|
|
161
|
+
await deleteConfigFile(filesPath);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const deletePipelineConfigFileByPath = async (absolutePath: string, context: PipelabContext) => {
|
|
165
|
+
await deleteConfigFile(absolutePath);
|
|
166
|
+
};
|