@pipelab/core-node 1.0.0-beta.2 → 1.0.0-beta.22
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 +173 -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
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
import { useAPI } from "../ipc-core";
|
|
2
|
+
import { useLogger } from "@pipelab/shared";
|
|
3
|
+
import { PipelabContext, getDefaultUserDataPath, isDev, PipelabEnv } from "../context";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { FileRepo, SaveLocation, AppConfig, ConnectionsConfig, savedFileMigrator } from "@pipelab/shared";
|
|
8
|
+
import semver from "semver";
|
|
9
|
+
|
|
10
|
+
interface MigrationPipelineItem {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
type: "internal" | "external" | "pipelab-cloud";
|
|
15
|
+
lastModifiedStable?: string;
|
|
16
|
+
lastModifiedBeta?: string;
|
|
17
|
+
existsInBeta: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MigrationProjectItem {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
pipelines: MigrationPipelineItem[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const registerMigrationHandlers = (context: PipelabContext) => {
|
|
28
|
+
const { handle } = useAPI();
|
|
29
|
+
const { logger } = useLogger();
|
|
30
|
+
|
|
31
|
+
const getFileMetadata = async (filePath: string) => {
|
|
32
|
+
const exists = existsSync(filePath);
|
|
33
|
+
let mtime = Date.now();
|
|
34
|
+
let version: string | null = null;
|
|
35
|
+
|
|
36
|
+
if (exists) {
|
|
37
|
+
try {
|
|
38
|
+
const stat = await fs.stat(filePath);
|
|
39
|
+
mtime = stat.mtime ? stat.mtime.getTime() : Date.now();
|
|
40
|
+
} catch (e) {
|
|
41
|
+
mtime = Date.now();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
46
|
+
const json = JSON.parse(content);
|
|
47
|
+
version = json && typeof json.version === "string" ? json.version : null;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Version remains null
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
mtime = Date.now();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { exists, mtime, version };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isVersionImportable = (sourceVer: string | null, targetVer: string | null): boolean => {
|
|
59
|
+
if (!sourceVer) return false;
|
|
60
|
+
if (!targetVer) return true;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const coercedSource = semver.coerce(sourceVer);
|
|
64
|
+
const coercedTarget = semver.coerce(targetVer);
|
|
65
|
+
if (!coercedSource) return false;
|
|
66
|
+
if (!coercedTarget) return true;
|
|
67
|
+
return semver.lte(coercedSource, coercedTarget);
|
|
68
|
+
} catch {
|
|
69
|
+
return sourceVer <= targetVer;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
handle("migration:scan-stable", async (_, { send, value }) => {
|
|
74
|
+
logger().info("[Migration] Scanning other channel database...");
|
|
75
|
+
try {
|
|
76
|
+
let sourceEnv: PipelabEnv = "prod";
|
|
77
|
+
if (value?.sourceChannel === "stable") {
|
|
78
|
+
sourceEnv = "prod";
|
|
79
|
+
} else if (value?.sourceChannel === "beta") {
|
|
80
|
+
sourceEnv = "beta";
|
|
81
|
+
} else {
|
|
82
|
+
const isStable =
|
|
83
|
+
context.userDataPath.endsWith("app") || !context.userDataPath.includes("app-beta");
|
|
84
|
+
sourceEnv = isStable ? "beta" : "prod";
|
|
85
|
+
}
|
|
86
|
+
const sourcePath = getDefaultUserDataPath(sourceEnv);
|
|
87
|
+
const targetPath = context.userDataPath;
|
|
88
|
+
|
|
89
|
+
if (sourcePath === targetPath) {
|
|
90
|
+
logger().info("[Migration] Running in same channel. Disabling migration scan.");
|
|
91
|
+
return send({
|
|
92
|
+
type: "end",
|
|
93
|
+
data: {
|
|
94
|
+
type: "success",
|
|
95
|
+
result: {
|
|
96
|
+
sourceChannel: sourceEnv === "prod" ? "Stable" : "Beta",
|
|
97
|
+
targetChannel: isDev ? "Dev" : (context.userDataPath.endsWith("app") || !context.userDataPath.includes("app-beta") ? "Stable" : "Beta"),
|
|
98
|
+
settingsExists: false,
|
|
99
|
+
settingsVersion: null,
|
|
100
|
+
settingsVersionTarget: null,
|
|
101
|
+
settingsMtimeSource: Date.now(),
|
|
102
|
+
settingsMtimeTarget: Date.now(),
|
|
103
|
+
settingsImportable: false,
|
|
104
|
+
connectionsExists: false,
|
|
105
|
+
connectionsCount: 0,
|
|
106
|
+
connectionsVersion: null,
|
|
107
|
+
connectionsVersionTarget: null,
|
|
108
|
+
connectionsMtimeSource: Date.now(),
|
|
109
|
+
connectionsMtimeTarget: Date.now(),
|
|
110
|
+
connectionsImportable: false,
|
|
111
|
+
projectsExists: false,
|
|
112
|
+
projectsVersion: null,
|
|
113
|
+
projectsVersionTarget: null,
|
|
114
|
+
projectsMtimeSource: Date.now(),
|
|
115
|
+
projectsMtimeTarget: Date.now(),
|
|
116
|
+
projectsImportable: false,
|
|
117
|
+
projects: [],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const sourceContext = new PipelabContext({ userDataPath: sourcePath, releaseTag: sourceEnv });
|
|
124
|
+
|
|
125
|
+
const sourceSettingsPath = sourceContext.getSettingsPath();
|
|
126
|
+
const targetSettingsPath = context.getSettingsPath();
|
|
127
|
+
|
|
128
|
+
const sourceConnectionsPath = sourceContext.getConnectionsPath();
|
|
129
|
+
const targetConnectionsPath = context.getConnectionsPath();
|
|
130
|
+
|
|
131
|
+
const sourceProjectsPath = sourceContext.getProjectsPath();
|
|
132
|
+
const targetProjectsPath = context.getProjectsPath();
|
|
133
|
+
|
|
134
|
+
const sourceSettingsMeta = await getFileMetadata(sourceSettingsPath);
|
|
135
|
+
const targetSettingsMeta = await getFileMetadata(targetSettingsPath);
|
|
136
|
+
|
|
137
|
+
const sourceConnectionsMeta = await getFileMetadata(sourceConnectionsPath);
|
|
138
|
+
const targetConnectionsMeta = await getFileMetadata(targetConnectionsPath);
|
|
139
|
+
|
|
140
|
+
const sourceProjectsMeta = await getFileMetadata(sourceProjectsPath);
|
|
141
|
+
const targetProjectsMeta = await getFileMetadata(targetProjectsPath);
|
|
142
|
+
|
|
143
|
+
const settingsExists = sourceSettingsMeta.exists;
|
|
144
|
+
const settingsVersion = sourceSettingsMeta.version;
|
|
145
|
+
const settingsMtimeSource = sourceSettingsMeta.mtime;
|
|
146
|
+
const settingsMtimeTarget = targetSettingsMeta.mtime;
|
|
147
|
+
const settingsImportable = isVersionImportable(settingsVersion, targetSettingsMeta.version);
|
|
148
|
+
|
|
149
|
+
const connectionsExists = sourceConnectionsMeta.exists;
|
|
150
|
+
const connectionsVersion = sourceConnectionsMeta.version;
|
|
151
|
+
const connectionsMtimeSource = sourceConnectionsMeta.mtime;
|
|
152
|
+
const connectionsMtimeTarget = targetConnectionsMeta.mtime;
|
|
153
|
+
const connectionsImportable = isVersionImportable(
|
|
154
|
+
connectionsVersion,
|
|
155
|
+
targetConnectionsMeta.version,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const projectsExists = sourceProjectsMeta.exists;
|
|
159
|
+
const projectsVersion = sourceProjectsMeta.version;
|
|
160
|
+
const projectsMtimeSource = sourceProjectsMeta.mtime;
|
|
161
|
+
const projectsMtimeTarget = targetProjectsMeta.mtime;
|
|
162
|
+
const projectsImportable = isVersionImportable(projectsVersion, targetProjectsMeta.version);
|
|
163
|
+
|
|
164
|
+
let connectionsCount = 0;
|
|
165
|
+
try {
|
|
166
|
+
if (sourceConnectionsMeta.exists) {
|
|
167
|
+
const connContent = await fs.readFile(sourceConnectionsPath, "utf8");
|
|
168
|
+
const connJson = JSON.parse(connContent) as ConnectionsConfig;
|
|
169
|
+
if (connJson && Array.isArray(connJson.connections)) {
|
|
170
|
+
connectionsCount = connJson.connections.length;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
logger().error("[Migration] Error parsing source connections.json:", err);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let sourceProjectsList: FileRepo["projects"] = [];
|
|
178
|
+
let sourcePipelinesList: SaveLocation[] = [];
|
|
179
|
+
try {
|
|
180
|
+
if (sourceProjectsMeta.exists) {
|
|
181
|
+
const projContent = await fs.readFile(sourceProjectsPath, "utf8");
|
|
182
|
+
const projJson = JSON.parse(projContent) as FileRepo;
|
|
183
|
+
sourceProjectsList = projJson.projects || [];
|
|
184
|
+
sourcePipelinesList = projJson.pipelines || [];
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
logger().error("[Migration] Error parsing source projects.json:", err);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let targetPipelinesList: SaveLocation[] = [];
|
|
191
|
+
try {
|
|
192
|
+
if (targetProjectsMeta.exists) {
|
|
193
|
+
const targetProjContent = await fs.readFile(targetProjectsPath, "utf8");
|
|
194
|
+
const targetProjJson = JSON.parse(targetProjContent) as FileRepo;
|
|
195
|
+
targetPipelinesList = targetProjJson.pipelines || [];
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// Safe to ignore if target hasn't initialized projects yet
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const projectsReport: MigrationProjectItem[] = [];
|
|
202
|
+
for (const proj of sourceProjectsList) {
|
|
203
|
+
const projPipelines = sourcePipelinesList.filter((p) => p.project === proj.id);
|
|
204
|
+
const pipelinesReport: MigrationPipelineItem[] = [];
|
|
205
|
+
|
|
206
|
+
for (const pipe of projPipelines) {
|
|
207
|
+
const targetPipe = targetPipelinesList.find((bp) => bp.id === pipe.id);
|
|
208
|
+
const existsInBeta = !!targetPipe;
|
|
209
|
+
|
|
210
|
+
let pipeName = pipe.id;
|
|
211
|
+
let pipeDesc = "";
|
|
212
|
+
if (pipe.type === "internal") {
|
|
213
|
+
const sourcePipeFile = sourceContext.getConfigPath(`${pipe.configName}.json`);
|
|
214
|
+
try {
|
|
215
|
+
if (existsSync(sourcePipeFile)) {
|
|
216
|
+
const pipeContent = await fs.readFile(sourcePipeFile, "utf8");
|
|
217
|
+
const rawJson = JSON.parse(pipeContent);
|
|
218
|
+
const pipeJson = await savedFileMigrator.migrate(rawJson);
|
|
219
|
+
pipeName = pipeJson.name || pipeName;
|
|
220
|
+
pipeDesc = pipeJson.description || pipeDesc;
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
logger().error(
|
|
224
|
+
`[Migration] Error reading source pipeline file ${sourcePipeFile}:`,
|
|
225
|
+
err,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
} else if (pipe.type === "external") {
|
|
229
|
+
pipeName = pipe.summary?.name || pipeName;
|
|
230
|
+
pipeDesc = pipe.summary?.description || pipeDesc;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
pipelinesReport.push({
|
|
234
|
+
id: pipe.id,
|
|
235
|
+
name: pipeName,
|
|
236
|
+
description: pipeDesc,
|
|
237
|
+
type: pipe.type,
|
|
238
|
+
lastModifiedStable: pipe.type !== "pipelab-cloud" ? pipe.lastModified : undefined,
|
|
239
|
+
lastModifiedBeta:
|
|
240
|
+
targetPipe && targetPipe.type !== "pipelab-cloud"
|
|
241
|
+
? targetPipe.lastModified
|
|
242
|
+
: undefined,
|
|
243
|
+
existsInBeta,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
projectsReport.push({
|
|
248
|
+
id: proj.id,
|
|
249
|
+
name: proj.name,
|
|
250
|
+
description: proj.description || "",
|
|
251
|
+
pipelines: pipelinesReport,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
send({
|
|
256
|
+
type: "end",
|
|
257
|
+
data: {
|
|
258
|
+
type: "success",
|
|
259
|
+
result: {
|
|
260
|
+
sourceChannel: sourceEnv === "prod" ? "Stable" : "Beta",
|
|
261
|
+
targetChannel: isDev ? "Dev" : (context.userDataPath.endsWith("app") || !context.userDataPath.includes("app-beta") ? "Stable" : "Beta"),
|
|
262
|
+
settingsExists,
|
|
263
|
+
settingsVersion,
|
|
264
|
+
settingsVersionTarget: targetSettingsMeta.version,
|
|
265
|
+
settingsMtimeSource,
|
|
266
|
+
settingsMtimeTarget,
|
|
267
|
+
settingsImportable,
|
|
268
|
+
connectionsExists,
|
|
269
|
+
connectionsCount,
|
|
270
|
+
connectionsVersion,
|
|
271
|
+
connectionsVersionTarget: targetConnectionsMeta.version,
|
|
272
|
+
connectionsMtimeSource,
|
|
273
|
+
connectionsMtimeTarget,
|
|
274
|
+
connectionsImportable,
|
|
275
|
+
projectsExists,
|
|
276
|
+
projectsVersion,
|
|
277
|
+
projectsVersionTarget: targetProjectsMeta.version,
|
|
278
|
+
projectsMtimeSource,
|
|
279
|
+
projectsMtimeTarget,
|
|
280
|
+
projectsImportable,
|
|
281
|
+
projects: projectsReport,
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
} catch (e) {
|
|
286
|
+
logger().error("[Migration] Error scanning other channel folder:", e);
|
|
287
|
+
send({
|
|
288
|
+
type: "end",
|
|
289
|
+
data: {
|
|
290
|
+
type: "error",
|
|
291
|
+
ipcError: e instanceof Error ? e.message : "Failed to scan other channel database",
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
handle("migration:perform", async (_, { send, value }) => {
|
|
298
|
+
logger().info("[Migration] Performing migration...");
|
|
299
|
+
try {
|
|
300
|
+
const { migrateSettings, migrateConnections, selectedProjects, selectedPipelines, sourceChannel } = value;
|
|
301
|
+
|
|
302
|
+
let sourceEnv: PipelabEnv = "prod";
|
|
303
|
+
if (sourceChannel === "stable") {
|
|
304
|
+
sourceEnv = "prod";
|
|
305
|
+
} else if (sourceChannel === "beta") {
|
|
306
|
+
sourceEnv = "beta";
|
|
307
|
+
} else {
|
|
308
|
+
const isStable =
|
|
309
|
+
context.userDataPath.endsWith("app") || !context.userDataPath.includes("app-beta");
|
|
310
|
+
sourceEnv = isStable ? "beta" : "prod";
|
|
311
|
+
}
|
|
312
|
+
const sourcePath = getDefaultUserDataPath(sourceEnv);
|
|
313
|
+
const sourceContext = new PipelabContext({ userDataPath: sourcePath, releaseTag: sourceEnv });
|
|
314
|
+
|
|
315
|
+
const sourceConfigDir = sourceContext.getConfigPath();
|
|
316
|
+
const targetConfigDir = context.getConfigPath();
|
|
317
|
+
|
|
318
|
+
// 1. Check folder validity
|
|
319
|
+
if (sourcePath === context.userDataPath) {
|
|
320
|
+
throw new Error("Cannot migrate data: Source and target directories are identical.");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 2. Create backup of target's config directory (excluding backups folder itself)
|
|
324
|
+
if (existsSync(targetConfigDir)) {
|
|
325
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
326
|
+
const backupDir = context.getConfigPath("backups", `backup_${timestamp}`);
|
|
327
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
328
|
+
|
|
329
|
+
const entries = await fs.readdir(targetConfigDir, { withFileTypes: true });
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
if (entry.name === "backups") continue;
|
|
332
|
+
const srcPath = join(targetConfigDir, entry.name);
|
|
333
|
+
const destPath = join(backupDir, entry.name);
|
|
334
|
+
try {
|
|
335
|
+
await fs.cp(srcPath, destPath, { recursive: true });
|
|
336
|
+
} catch (copyErr) {
|
|
337
|
+
logger().error(`[Migration] Failed to backup ${entry.name}:`, copyErr);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
logger().info(`[Migration] Config backup successfully created at ${backupDir}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 3. Settings migration (Merge)
|
|
344
|
+
if (migrateSettings) {
|
|
345
|
+
const sourceSettingsFile = sourceContext.getSettingsPath();
|
|
346
|
+
const targetSettingsFile = context.getSettingsPath();
|
|
347
|
+
|
|
348
|
+
const sourceMeta = await getFileMetadata(sourceSettingsFile);
|
|
349
|
+
const targetMeta = await getFileMetadata(targetSettingsFile);
|
|
350
|
+
if (!isVersionImportable(sourceMeta.version, targetMeta.version)) {
|
|
351
|
+
throw new Error("Cannot migrate settings: Source version is newer than current version.");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (existsSync(sourceSettingsFile)) {
|
|
355
|
+
let stableSettings: AppConfig = {
|
|
356
|
+
version: "7.0.0",
|
|
357
|
+
locale: "en-US",
|
|
358
|
+
theme: "light",
|
|
359
|
+
autosave: true,
|
|
360
|
+
agents: [],
|
|
361
|
+
plugins: [],
|
|
362
|
+
tours: {
|
|
363
|
+
dashboard: { step: 0, completed: false },
|
|
364
|
+
editor: { step: 0, completed: false },
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
try {
|
|
368
|
+
const stableContent = await fs.readFile(sourceSettingsFile, "utf8");
|
|
369
|
+
stableSettings = JSON.parse(stableContent) as AppConfig;
|
|
370
|
+
} catch (e) {
|
|
371
|
+
logger().error("[Migration] Failed to read source settings.json:", e);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let betaSettings: AppConfig = {
|
|
375
|
+
version: "7.0.0",
|
|
376
|
+
locale: "en-US",
|
|
377
|
+
theme: "light",
|
|
378
|
+
autosave: true,
|
|
379
|
+
agents: [],
|
|
380
|
+
plugins: [],
|
|
381
|
+
tours: {
|
|
382
|
+
dashboard: { step: 0, completed: false },
|
|
383
|
+
editor: { step: 0, completed: false },
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
if (existsSync(targetSettingsFile)) {
|
|
387
|
+
try {
|
|
388
|
+
const betaContent = await fs.readFile(targetSettingsFile, "utf8");
|
|
389
|
+
betaSettings = JSON.parse(betaContent) as AppConfig;
|
|
390
|
+
} catch (e) {
|
|
391
|
+
// Keep default
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Merge primitives
|
|
396
|
+
betaSettings.theme = stableSettings.theme ?? betaSettings.theme;
|
|
397
|
+
betaSettings.locale = stableSettings.locale ?? betaSettings.locale;
|
|
398
|
+
betaSettings.autosave = stableSettings.autosave ?? betaSettings.autosave;
|
|
399
|
+
|
|
400
|
+
// Merge tours
|
|
401
|
+
if (stableSettings.tours) {
|
|
402
|
+
betaSettings.tours = betaSettings.tours || {
|
|
403
|
+
dashboard: { step: 0, completed: false },
|
|
404
|
+
editor: { step: 0, completed: false },
|
|
405
|
+
};
|
|
406
|
+
if (stableSettings.tours.dashboard) {
|
|
407
|
+
betaSettings.tours.dashboard = stableSettings.tours.dashboard;
|
|
408
|
+
}
|
|
409
|
+
if (stableSettings.tours.editor) {
|
|
410
|
+
betaSettings.tours.editor = stableSettings.tours.editor;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Merge agents (by id)
|
|
415
|
+
const betaAgents = betaSettings.agents || [];
|
|
416
|
+
const stableAgents = stableSettings.agents || [];
|
|
417
|
+
for (const sAgent of stableAgents) {
|
|
418
|
+
const existingIdx = betaAgents.findIndex((a) => a.id === sAgent.id);
|
|
419
|
+
if (existingIdx >= 0) {
|
|
420
|
+
betaAgents[existingIdx] = { ...sAgent };
|
|
421
|
+
} else {
|
|
422
|
+
betaAgents.push({ ...sAgent });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
betaSettings.agents = betaAgents;
|
|
426
|
+
|
|
427
|
+
// Merge plugins (by name)
|
|
428
|
+
const betaPlugins = betaSettings.plugins || [];
|
|
429
|
+
const stablePlugins = stableSettings.plugins || [];
|
|
430
|
+
for (const sPlugin of stablePlugins) {
|
|
431
|
+
const existingIdx = betaPlugins.findIndex((p) => p.name === sPlugin.name);
|
|
432
|
+
if (existingIdx >= 0) {
|
|
433
|
+
betaPlugins[existingIdx] = { ...sPlugin };
|
|
434
|
+
} else {
|
|
435
|
+
betaPlugins.push({ ...sPlugin });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
betaSettings.plugins = betaPlugins;
|
|
439
|
+
|
|
440
|
+
await fs.mkdir(dirname(targetSettingsFile), { recursive: true });
|
|
441
|
+
await fs.writeFile(targetSettingsFile, JSON.stringify(betaSettings, null, 2));
|
|
442
|
+
logger().info("[Migration] Settings merged successfully");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 4. Connections migration (Merge)
|
|
447
|
+
if (migrateConnections) {
|
|
448
|
+
const sourceConnectionsFile = sourceContext.getConnectionsPath();
|
|
449
|
+
const targetConnectionsFile = context.getConnectionsPath();
|
|
450
|
+
|
|
451
|
+
const sourceMeta = await getFileMetadata(sourceConnectionsFile);
|
|
452
|
+
const targetMeta = await getFileMetadata(targetConnectionsFile);
|
|
453
|
+
if (!isVersionImportable(sourceMeta.version, targetMeta.version)) {
|
|
454
|
+
throw new Error(
|
|
455
|
+
"Cannot migrate connections: Source version is newer than current version.",
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (existsSync(sourceConnectionsFile)) {
|
|
460
|
+
let stableConnections: ConnectionsConfig = { version: "1.0.0", connections: [] };
|
|
461
|
+
try {
|
|
462
|
+
const stableContent = await fs.readFile(sourceConnectionsFile, "utf8");
|
|
463
|
+
stableConnections = JSON.parse(stableContent) as ConnectionsConfig;
|
|
464
|
+
} catch (e) {
|
|
465
|
+
logger().error("[Migration] Failed to read source connections.json:", e);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let betaConnections: ConnectionsConfig = { version: "1.0.0", connections: [] };
|
|
469
|
+
if (existsSync(targetConnectionsFile)) {
|
|
470
|
+
try {
|
|
471
|
+
const betaContent = await fs.readFile(targetConnectionsFile, "utf8");
|
|
472
|
+
betaConnections = JSON.parse(betaContent) as ConnectionsConfig;
|
|
473
|
+
} catch (e) {
|
|
474
|
+
// Keep default
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
betaConnections.connections = betaConnections.connections || [];
|
|
479
|
+
stableConnections.connections = stableConnections.connections || [];
|
|
480
|
+
|
|
481
|
+
for (const sConn of stableConnections.connections) {
|
|
482
|
+
const existingIdx = betaConnections.connections.findIndex((c) => c.id === sConn.id);
|
|
483
|
+
if (existingIdx >= 0) {
|
|
484
|
+
betaConnections.connections[existingIdx] = { ...sConn };
|
|
485
|
+
} else {
|
|
486
|
+
betaConnections.connections.push({ ...sConn });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
await fs.mkdir(dirname(targetConnectionsFile), { recursive: true });
|
|
491
|
+
await fs.writeFile(targetConnectionsFile, JSON.stringify(betaConnections, null, 2));
|
|
492
|
+
logger().info("[Migration] Connections merged successfully");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 5. Projects and Pipelines migration
|
|
497
|
+
if (selectedPipelines.length > 0 || selectedProjects.length > 0) {
|
|
498
|
+
const sourceProjectsFile = sourceContext.getProjectsPath();
|
|
499
|
+
const targetProjectsFile = context.getProjectsPath();
|
|
500
|
+
|
|
501
|
+
const sourceMeta = await getFileMetadata(sourceProjectsFile);
|
|
502
|
+
const targetMeta = await getFileMetadata(targetProjectsFile);
|
|
503
|
+
if (!isVersionImportable(sourceMeta.version, targetMeta.version)) {
|
|
504
|
+
throw new Error("Cannot migrate projects: Source version is newer than current version.");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Read source projects
|
|
508
|
+
let stableFileRepo: FileRepo = { version: "3.0.0", projects: [], pipelines: [] };
|
|
509
|
+
if (existsSync(sourceProjectsFile)) {
|
|
510
|
+
const stableContent = await fs.readFile(sourceProjectsFile, "utf8");
|
|
511
|
+
stableFileRepo = JSON.parse(stableContent) as FileRepo;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Read or initialize current target projects
|
|
515
|
+
let betaFileRepo: FileRepo = { version: "3.0.0", projects: [], pipelines: [] };
|
|
516
|
+
if (existsSync(targetProjectsFile)) {
|
|
517
|
+
try {
|
|
518
|
+
const betaContent = await fs.readFile(targetProjectsFile, "utf8");
|
|
519
|
+
betaFileRepo = JSON.parse(betaContent) as FileRepo;
|
|
520
|
+
} catch (readErr) {
|
|
521
|
+
logger().warn(
|
|
522
|
+
"[Migration] Failed to parse existing target projects.json, starting fresh:",
|
|
523
|
+
readErr,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
betaFileRepo.projects = betaFileRepo.projects || [];
|
|
529
|
+
betaFileRepo.pipelines = betaFileRepo.pipelines || [];
|
|
530
|
+
|
|
531
|
+
// Copy selected projects metadata
|
|
532
|
+
for (const projId of selectedProjects) {
|
|
533
|
+
const stableProj = stableFileRepo.projects.find((p) => p.id === projId);
|
|
534
|
+
if (stableProj) {
|
|
535
|
+
const existingProjIdx = betaFileRepo.projects.findIndex((p) => p.id === projId);
|
|
536
|
+
if (existingProjIdx >= 0) {
|
|
537
|
+
betaFileRepo.projects[existingProjIdx] = { ...stableProj };
|
|
538
|
+
} else {
|
|
539
|
+
betaFileRepo.projects.push({ ...stableProj });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Copy selected pipelines metadata and copy the pipeline config files
|
|
545
|
+
for (const pipeId of selectedPipelines) {
|
|
546
|
+
const stablePipe = (stableFileRepo.pipelines || []).find((p) => p.id === pipeId);
|
|
547
|
+
if (stablePipe) {
|
|
548
|
+
// Ensure the parent project metadata exists in target
|
|
549
|
+
const parentProjId = stablePipe.project;
|
|
550
|
+
const projectExists = betaFileRepo.projects.some((p) => p.id === parentProjId);
|
|
551
|
+
if (!projectExists) {
|
|
552
|
+
const stableProj = stableFileRepo.projects.find((p) => p.id === parentProjId);
|
|
553
|
+
if (stableProj) {
|
|
554
|
+
betaFileRepo.projects.push({ ...stableProj });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Copy and migrate pipeline file if internal
|
|
559
|
+
if (stablePipe.type === "internal") {
|
|
560
|
+
const stablePipeFile = sourceContext.getConfigPath(`${stablePipe.configName}.json`);
|
|
561
|
+
const betaPipeFile = context.getConfigPath(`${stablePipe.configName}.json`);
|
|
562
|
+
if (existsSync(stablePipeFile)) {
|
|
563
|
+
await fs.mkdir(dirname(betaPipeFile), { recursive: true });
|
|
564
|
+
try {
|
|
565
|
+
const pipeContent = await fs.readFile(stablePipeFile, "utf8");
|
|
566
|
+
const rawJson = JSON.parse(pipeContent);
|
|
567
|
+
const migratedJson = await savedFileMigrator.migrate(rawJson);
|
|
568
|
+
await fs.writeFile(betaPipeFile, JSON.stringify(migratedJson, null, 2));
|
|
569
|
+
logger().info(`[Migration] Migrated and copied pipeline file: ${stablePipe.configName}`);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
await fs.copyFile(stablePipeFile, betaPipeFile);
|
|
572
|
+
logger().error(`[Migration] Error migrating during copy of ${stablePipe.configName}, fallback to direct copy:`, err);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Create updated pipeline metadata with modification date set to now timestamp
|
|
578
|
+
const updatedPipe = {
|
|
579
|
+
...stablePipe,
|
|
580
|
+
};
|
|
581
|
+
if (updatedPipe.type !== "pipelab-cloud") {
|
|
582
|
+
(updatedPipe as any).lastModified = new Date().toISOString();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Update target projects.json
|
|
586
|
+
const existingPipeIdx = betaFileRepo.pipelines.findIndex((p) => p.id === pipeId);
|
|
587
|
+
if (existingPipeIdx >= 0) {
|
|
588
|
+
betaFileRepo.pipelines[existingPipeIdx] = updatedPipe;
|
|
589
|
+
} else {
|
|
590
|
+
betaFileRepo.pipelines.push(updatedPipe);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Save projects.json
|
|
596
|
+
await fs.mkdir(dirname(targetProjectsFile), { recursive: true });
|
|
597
|
+
await fs.writeFile(targetProjectsFile, JSON.stringify(betaFileRepo, null, 2));
|
|
598
|
+
logger().info("[Migration] Merged and saved projects.json successfully");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
send({
|
|
602
|
+
type: "end",
|
|
603
|
+
data: {
|
|
604
|
+
type: "success",
|
|
605
|
+
result: {
|
|
606
|
+
result: "ok",
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
} catch (e) {
|
|
611
|
+
logger().error("[Migration] Error performing migration:", e);
|
|
612
|
+
send({
|
|
613
|
+
type: "end",
|
|
614
|
+
data: {
|
|
615
|
+
type: "error",
|
|
616
|
+
ipcError: e instanceof Error ? e.message : "Failed to migrate selected data",
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
};
|