@pipelab/core-node 1.0.0-beta.17 → 1.0.0-beta.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipelab/core-node",
3
- "version": "1.0.0-beta.17",
3
+ "version": "1.0.0-beta.19",
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.15",
44
- "@pipelab/shared": "1.0.0-beta.13"
43
+ "@pipelab/constants": "1.0.0-beta.17",
44
+ "@pipelab/shared": "1.0.0-beta.15"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/adm-zip": "0.5.7",
@@ -56,7 +56,7 @@
56
56
  "tsdown": "0.21.2",
57
57
  "typescript": "^5.0.0",
58
58
  "vitest": "3.1.4",
59
- "@pipelab/tsconfig": "1.0.0-beta.13"
59
+ "@pipelab/tsconfig": "1.0.0-beta.15"
60
60
  },
61
61
  "scripts": {
62
62
  "build": "tsdown",
@@ -1,5 +1,10 @@
1
1
  import { describe, test, expect, beforeEach, vi } from "vitest";
2
- import { setupConfigFile } from "./config";
2
+ import {
3
+ setupConfigFile,
4
+ setupProjectsConfigFile,
5
+ deletePipelineConfigFileByName,
6
+ deletePipelineConfigFileByPath,
7
+ } from "./config";
3
8
  import { FileRepo } from "@pipelab/shared";
4
9
  import { PipelabContext } from "./context";
5
10
  import fs from "node:fs/promises";
@@ -38,9 +43,9 @@ describe("setupConfigFile & Backup Creation", () => {
38
43
  // 1. Setup V1 configuration file in the context's config path
39
44
  const configDir = context.getConfigPath();
40
45
  await fs.mkdir(configDir, { recursive: true });
41
-
42
- const projectsFilePath = path.join(configDir, "projects.json");
43
-
46
+
47
+ const projectsFilePath = context.getProjectsPath();
48
+
44
49
  const v1Config = {
45
50
  version: "1.0.0",
46
51
  data: {
@@ -49,14 +54,14 @@ describe("setupConfigFile & Backup Creation", () => {
49
54
  type: "internal",
50
55
  configName: "pipeline-1",
51
56
  lastModified: "2026-06-04",
52
- }
53
- }
57
+ },
58
+ },
54
59
  };
55
-
60
+
56
61
  await fs.writeFile(projectsFilePath, JSON.stringify(v1Config));
57
62
 
58
63
  // 2. Initialize setupConfigFile
59
- const configInstance = await setupConfigFile<FileRepo>("projects", { context });
64
+ const configInstance = await setupProjectsConfigFile(context);
60
65
 
61
66
  // Verify file exists
62
67
  expect(existsSync(projectsFilePath)).toBe(true);
@@ -64,11 +69,11 @@ describe("setupConfigFile & Backup Creation", () => {
64
69
  // 3. Retrieve config (this triggers the migration process and onStep callback)
65
70
  const migratedConfig = await configInstance.getConfig();
66
71
 
67
- // 4. Assert the main config is updated on disk to version 2.0.0
68
- expect(migratedConfig.version).toBe("2.0.0");
69
-
72
+ // 4. Assert the main config is updated on disk to version 3.0.0
73
+ expect(migratedConfig.version).toBe("3.0.0");
74
+
70
75
  const updatedContent = JSON.parse(await fs.readFile(projectsFilePath, "utf8"));
71
- expect(updatedContent.version).toBe("2.0.0");
76
+ expect(updatedContent.version).toBe("3.0.0");
72
77
 
73
78
  // 5. Assert that the intermediate backup file was created on disk
74
79
  const backupFilePath = path.join(configDir, "projects.v2.0.0.json");
@@ -82,17 +87,17 @@ describe("setupConfigFile & Backup Creation", () => {
82
87
 
83
88
  test("should create default config file if missing on setup", async () => {
84
89
  const configDir = context.getConfigPath();
85
- const projectsFilePath = path.join(configDir, "projects.json");
90
+ const projectsFilePath = context.getProjectsPath();
86
91
 
87
92
  expect(existsSync(projectsFilePath)).toBe(false);
88
93
 
89
94
  // Initialize setupConfigFile
90
- await setupConfigFile<FileRepo>("projects", { context });
95
+ await setupProjectsConfigFile(context);
91
96
 
92
97
  // Should create file with default value
93
98
  expect(existsSync(projectsFilePath)).toBe(true);
94
99
  const content = JSON.parse(await fs.readFile(projectsFilePath, "utf8"));
95
- expect(content.version).toBe("2.0.0");
100
+ expect(content.version).toBe("3.0.0");
96
101
  expect(content.projects).toHaveLength(1);
97
102
  expect(content.pipelines).toEqual([]);
98
103
  });
@@ -100,20 +105,22 @@ describe("setupConfigFile & Backup Creation", () => {
100
105
  test("should fallback to default config and preserve corrupted file if parsing fails", async () => {
101
106
  const configDir = context.getConfigPath();
102
107
  await fs.mkdir(configDir, { recursive: true });
103
- const projectsFilePath = path.join(configDir, "projects.json");
108
+ const projectsFilePath = context.getProjectsPath();
104
109
 
105
110
  await fs.writeFile(projectsFilePath, "{ corrupted json... }");
106
111
 
107
- const configInstance = await setupConfigFile<FileRepo>("projects", { context });
112
+ const configInstance = await setupProjectsConfigFile(context);
108
113
  const config = await configInstance.getConfig();
109
114
 
110
- expect(config.version).toBe("2.0.0");
115
+ expect(config.version).toBe("3.0.0");
111
116
  expect(config.projects).toHaveLength(1);
112
117
  expect(config.pipelines).toEqual([]);
113
118
 
114
119
  // Verify a timestamped corrupted backup file exists
115
120
  const files = await fs.readdir(configDir);
116
- const corruptedFile = files.find(f => f.startsWith("projects.corrupted.") && f.endsWith(".json"));
121
+ const corruptedFile = files.find(
122
+ (f) => f.startsWith("projects.corrupted.") && f.endsWith(".json"),
123
+ );
117
124
  expect(corruptedFile).toBeDefined();
118
125
  const corruptedContent = await fs.readFile(path.join(configDir, corruptedFile!), "utf8");
119
126
  expect(corruptedContent).toBe("{ corrupted json... }");
@@ -122,7 +129,7 @@ describe("setupConfigFile & Backup Creation", () => {
122
129
  test("should fallback to default config and preserve file if migration throws", async () => {
123
130
  const configDir = context.getConfigPath();
124
131
  await fs.mkdir(configDir, { recursive: true });
125
- const projectsFilePath = path.join(configDir, "projects.json");
132
+ const projectsFilePath = context.getProjectsPath();
126
133
 
127
134
  const customMigrator = {
128
135
  defaultValue: { version: "2.0.0", projects: [], pipelines: [] } as any,
@@ -133,7 +140,7 @@ describe("setupConfigFile & Backup Creation", () => {
133
140
 
134
141
  await fs.writeFile(projectsFilePath, JSON.stringify({ version: "1.0.0", invalidData: true }));
135
142
 
136
- const configInstance = await setupConfigFile<any>("projects", {
143
+ const configInstance = await setupConfigFile<any>(context.getProjectsPath(), {
137
144
  context,
138
145
  migrator: customMigrator,
139
146
  });
@@ -143,14 +150,18 @@ describe("setupConfigFile & Backup Creation", () => {
143
150
 
144
151
  // Verify a timestamped corrupted backup file exists
145
152
  const files = await fs.readdir(configDir);
146
- const corruptedFile = files.find(f => f.startsWith("projects.corrupted.") && f.endsWith(".json"));
153
+ const corruptedFile = files.find(
154
+ (f) => f.startsWith("projects.corrupted.") && f.endsWith(".json"),
155
+ );
147
156
  expect(corruptedFile).toBeDefined();
148
- const corruptedContent = JSON.parse(await fs.readFile(path.join(configDir, corruptedFile!), "utf8"));
157
+ const corruptedContent = JSON.parse(
158
+ await fs.readFile(path.join(configDir, corruptedFile!), "utf8"),
159
+ );
149
160
  expect(corruptedContent.invalidData).toBe(true);
150
161
  });
151
162
 
152
163
  test("should save config to disk via setConfig", async () => {
153
- const configInstance = await setupConfigFile<FileRepo>("projects", { context });
164
+ const configInstance = await setupProjectsConfigFile(context);
154
165
  const initialConfig = await configInstance.getConfig();
155
166
 
156
167
  const newConfig: FileRepo = {
@@ -169,8 +180,53 @@ describe("setupConfigFile & Backup Creation", () => {
169
180
  const success = await configInstance.setConfig(newConfig);
170
181
  expect(success).toBe(true);
171
182
 
172
- const savedContent = JSON.parse(await fs.readFile(path.join(context.getConfigPath(), "projects.json"), "utf8"));
183
+ const savedContent = JSON.parse(
184
+ await fs.readFile(context.getProjectsPath(), "utf8"),
185
+ );
173
186
  expect(savedContent.pipelines).toHaveLength(1);
174
187
  expect(savedContent.pipelines[0].id).toBe("pipeline-new");
175
188
  });
176
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,28 +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 { useLogger } from "@pipelab/shared";
6
- import { configRegistry, Migrator, normalizePipelineConfig } from "@pipelab/shared";
7
-
8
- export const getMigrator = <T>(name: string) => {
9
- return (configRegistry[name] || configRegistry["pipeline"]) as Migrator<T>;
10
- };
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";
11
17
 
12
18
  export const setupConfigFile = async <T>(
13
- name: string,
14
- options: { context: PipelabContext; migrator?: Migrator<T> },
19
+ filesPath: string,
20
+ options: { context: PipelabContext; migrator: Migrator<T> },
15
21
  ) => {
16
22
  const ctx = options.context;
17
- const migrator = options.migrator || getMigrator<T>(name);
18
-
19
- if (!migrator) {
20
- throw new Error(
21
- `No migrator found for configuration: ${name}. All managed files must have a migration schema.`,
22
- );
23
- }
24
-
25
- const isAbsolutePath = path.isAbsolute(name);
26
- const filesPath = isAbsolutePath ? name : ctx.getConfigPath(`${name}.json`);
23
+ const parsedPath = path.parse(filesPath);
24
+ const migrator = options.migrator;
27
25
 
28
26
  await ensure(filesPath, JSON.stringify(migrator.defaultValue));
29
27
 
@@ -34,7 +32,7 @@ export const setupConfigFile = async <T>(
34
32
  await fs.writeFile(filesPath, JSON.stringify(config));
35
33
  return true;
36
34
  } catch (e) {
37
- logger().error(`Error saving config ${name}:`, e);
35
+ logger().error(`Error saving config ${parsedPath.name}:`, e);
38
36
  return false;
39
37
  }
40
38
  },
@@ -50,7 +48,7 @@ export const setupConfigFile = async <T>(
50
48
  originalJson = JSON.parse(content);
51
49
  }
52
50
  } catch (e) {
53
- logger().error(`Error reading or parsing config ${name}:`, e);
51
+ logger().error(`Error reading or parsing config ${parsedPath.name}:`, e);
54
52
  parseFailed = true;
55
53
  }
56
54
 
@@ -61,13 +59,15 @@ export const setupConfigFile = async <T>(
61
59
  json = await migrator.migrate(originalJson, {
62
60
  debug: false,
63
61
  onStep: async (state: any, version: string) => {
64
- const parsedPath = path.parse(filesPath);
65
62
  const versionedPath = path.join(parsedPath.dir, `${parsedPath.name}.v${version}.json`);
66
63
  try {
67
64
  await fs.writeFile(versionedPath, JSON.stringify(state));
68
- logger().info(`Intermediate backup created for ${name} at ${versionedPath}`);
65
+ logger().info(`Intermediate backup created for ${parsedPath.name} at ${versionedPath}`);
69
66
  } catch (e) {
70
- logger().error(`Failed to create intermediate backup for ${name} at v${version}:`, e);
67
+ logger().error(
68
+ `Failed to create intermediate backup for ${parsedPath.name} at v${version}:`,
69
+ e,
70
+ );
71
71
  }
72
72
  },
73
73
  });
@@ -75,52 +75,39 @@ export const setupConfigFile = async <T>(
75
75
  json = migrator.defaultValue;
76
76
  }
77
77
  } catch (e) {
78
- logger().error(`Error migrating config ${name}:`, e);
78
+ logger().error(`Error migrating config ${parsedPath.name}:`, e);
79
79
  migrationFailed = true;
80
80
  json = migrator.defaultValue;
81
81
  }
82
82
 
83
- let normalized = false;
84
- const isPipeline =
85
- name.startsWith("pipeline-") ||
86
- path.isAbsolute(name) ||
87
- name.endsWith(".json") ||
88
- name === "pipeline";
89
- if (isPipeline) {
90
- normalized = normalizePipelineConfig(json);
91
- }
92
-
93
83
  const originalVersion = originalJson?.version;
94
84
  const newVersion = json?.version;
95
85
 
96
86
  const shouldSaveBack =
97
- originalVersion !== newVersion ||
98
- normalized ||
99
- content === undefined ||
100
- parseFailed ||
101
- migrationFailed;
87
+ originalVersion !== newVersion || content === undefined || parseFailed || migrationFailed;
102
88
 
103
89
  if (shouldSaveBack) {
104
90
  if (parseFailed || migrationFailed) {
105
91
  try {
106
- const parsedPath = path.parse(filesPath);
107
92
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
108
93
  const corruptedPath = path.join(
109
94
  parsedPath.dir,
110
95
  `${parsedPath.name}.corrupted.${timestamp}.json`,
111
96
  );
112
- const backupContent = parseFailed ? (content || "") : JSON.stringify(originalJson, null, 2);
97
+ const backupContent = parseFailed
98
+ ? content || ""
99
+ : JSON.stringify(originalJson, null, 2);
113
100
  await fs.writeFile(corruptedPath, backupContent);
114
101
  logger().info(`Corrupted config file preserved at ${corruptedPath}`);
115
102
  } catch (e) {
116
- logger().error(`Failed to backup corrupted config ${name}:`, e);
103
+ logger().error(`Failed to backup corrupted config ${parsedPath.name}:`, e);
117
104
  }
118
105
  }
119
106
 
120
107
  try {
121
108
  await fs.writeFile(filesPath, JSON.stringify(json));
122
109
  } catch (e) {
123
- logger().error(`Error saving migrated config ${name}:`, e);
110
+ logger().error(`Error saving migrated config ${parsedPath.name}:`, e);
124
111
  }
125
112
  }
126
113
 
@@ -128,3 +115,52 @@ export const setupConfigFile = async <T>(
128
115
  },
129
116
  };
130
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
+ };
package/src/context.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { join, dirname, resolve } from "node:path";
2
2
  import { homedir, platform } from "node:os";
3
3
  import { fileURLToPath } from "node:url";
4
- import { existsSync } from "node:fs";
4
+ import { existsSync, readFileSync } from "node:fs";
5
5
  import { mkdir, realpath, mkdtemp } from "node:fs/promises";
6
6
  import { SandboxFolder, DEFAULT_NODE_VERSION, DEFAULT_PNPM_VERSION } from "@pipelab/constants";
7
7
 
@@ -51,6 +51,14 @@ function findProjectRoot(startDir: string): string | null {
51
51
 
52
52
  export const projectRoot = findProjectRoot(_dirname);
53
53
 
54
+ export const CacheFolder = {
55
+ Actions: "actions",
56
+ Pipelines: "pipelines",
57
+ Pacote: "pacote",
58
+ } as const;
59
+
60
+ export type CacheFolderType = (typeof CacheFolder)[keyof typeof CacheFolder];
61
+
54
62
  export interface PipelabContextOptions {
55
63
  userDataPath: string;
56
64
  releaseTag?: string;
@@ -77,8 +85,44 @@ export class PipelabContext {
77
85
  return join(this.userDataPath, "config", ...subpaths);
78
86
  }
79
87
 
88
+ getSettingsPath() {
89
+ return this.getConfigPath("settings.json");
90
+ }
91
+
92
+ getConnectionsPath() {
93
+ return this.getConfigPath("connections.json");
94
+ }
95
+
96
+ getProjectsPath() {
97
+ return this.getConfigPath("projects.json");
98
+ }
99
+
100
+ private _cachedSettings: any = null;
101
+ private _cachedSettingsTime: number = 0;
102
+
103
+ private getSettings() {
104
+ const settingsPath = this.getSettingsPath();
105
+ if (!existsSync(settingsPath)) {
106
+ return null;
107
+ }
108
+ const now = Date.now();
109
+ if (this._cachedSettings && now - this._cachedSettingsTime < 2000) {
110
+ return this._cachedSettings;
111
+ }
112
+ try {
113
+ const content = readFileSync(settingsPath, "utf8");
114
+ this._cachedSettings = JSON.parse(content);
115
+ this._cachedSettingsTime = now;
116
+ return this._cachedSettings;
117
+ } catch (e) {
118
+ return this._cachedSettings;
119
+ }
120
+ }
121
+
80
122
  getTempPath(...subpaths: string[]) {
81
- return join(this.userDataPath, "temp", ...subpaths);
123
+ const settings = this.getSettings();
124
+ const base = settings?.tempFolder || join(this.userDataPath, "temp");
125
+ return join(base, ...subpaths);
82
126
  }
83
127
 
84
128
  async createTempFolder(prefix = "pipelab-") {
@@ -88,14 +132,25 @@ export class PipelabContext {
88
132
  return await mkdtemp(join(realBaseDir, prefix));
89
133
  }
90
134
 
91
- getCachePath(...subpaths: string[]) {
92
- return join(this.userDataPath, "cache", ...subpaths);
135
+ getCachePath(): string;
136
+ getCachePath(folder: CacheFolderType, ...subpaths: string[]): string;
137
+ getCachePath(folder?: CacheFolderType, ...subpaths: string[]) {
138
+ const settings = this.getSettings();
139
+ const base = settings?.cacheFolder || join(this.userDataPath, "cache");
140
+ if (!folder) {
141
+ return base;
142
+ }
143
+ return join(base, folder, ...subpaths);
93
144
  }
94
145
 
95
146
  getPnpmPath(...subpaths: string[]) {
96
147
  return join(this.userDataPath, "pnpm", ...subpaths);
97
148
  }
98
149
 
150
+ getBuildHistoryPath(...subpaths: string[]) {
151
+ return join(this.userDataPath, "build-history", ...subpaths);
152
+ }
153
+
99
154
  getNodePath(version = DEFAULT_NODE_VERSION) {
100
155
  const isWindows = process.platform === "win32";
101
156
  return this.getThirdPartyPath("node", version, isWindows ? "node.exe" : "bin/node");