@pipelab/core-node 1.0.0-beta.18 → 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.18",
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.16",
44
- "@pipelab/shared": "1.0.0-beta.14"
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.14"
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";
@@ -39,7 +44,7 @@ describe("setupConfigFile & Backup Creation", () => {
39
44
  const configDir = context.getConfigPath();
40
45
  await fs.mkdir(configDir, { recursive: true });
41
46
 
42
- const projectsFilePath = path.join(configDir, "projects.json");
47
+ const projectsFilePath = context.getProjectsPath();
43
48
 
44
49
  const v1Config = {
45
50
  version: "1.0.0",
@@ -56,7 +61,7 @@ describe("setupConfigFile & Backup Creation", () => {
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);
@@ -82,12 +87,12 @@ 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);
@@ -100,11 +105,11 @@ 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
115
  expect(config.version).toBe("3.0.0");
@@ -124,7 +129,7 @@ describe("setupConfigFile & Backup Creation", () => {
124
129
  test("should fallback to default config and preserve file if migration throws", async () => {
125
130
  const configDir = context.getConfigPath();
126
131
  await fs.mkdir(configDir, { recursive: true });
127
- const projectsFilePath = path.join(configDir, "projects.json");
132
+ const projectsFilePath = context.getProjectsPath();
128
133
 
129
134
  const customMigrator = {
130
135
  defaultValue: { version: "2.0.0", projects: [], pipelines: [] } as any,
@@ -135,7 +140,7 @@ describe("setupConfigFile & Backup Creation", () => {
135
140
 
136
141
  await fs.writeFile(projectsFilePath, JSON.stringify({ version: "1.0.0", invalidData: true }));
137
142
 
138
- const configInstance = await setupConfigFile<any>("projects", {
143
+ const configInstance = await setupConfigFile<any>(context.getProjectsPath(), {
139
144
  context,
140
145
  migrator: customMigrator,
141
146
  });
@@ -156,7 +161,7 @@ describe("setupConfigFile & Backup Creation", () => {
156
161
  });
157
162
 
158
163
  test("should save config to disk via setConfig", async () => {
159
- const configInstance = await setupConfigFile<FileRepo>("projects", { context });
164
+ const configInstance = await setupProjectsConfigFile(context);
160
165
  const initialConfig = await configInstance.getConfig();
161
166
 
162
167
  const newConfig: FileRepo = {
@@ -176,9 +181,52 @@ describe("setupConfigFile & Backup Creation", () => {
176
181
  expect(success).toBe(true);
177
182
 
178
183
  const savedContent = JSON.parse(
179
- await fs.readFile(path.join(context.getConfigPath(), "projects.json"), "utf8"),
184
+ await fs.readFile(context.getProjectsPath(), "utf8"),
180
185
  );
181
186
  expect(savedContent.pipelines).toHaveLength(1);
182
187
  expect(savedContent.pipelines[0].id).toBe("pipeline-new");
183
188
  });
184
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 } 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,14 +59,13 @@ 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
- const versionedPath = ctx.getConfigPath(`${parsedPath.name}.v${version}.json`);
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
67
  logger().error(
71
- `Failed to create intermediate backup for ${name} at v${version}:`,
68
+ `Failed to create intermediate backup for ${parsedPath.name} at v${version}:`,
72
69
  e,
73
70
  );
74
71
  }
@@ -78,7 +75,7 @@ export const setupConfigFile = async <T>(
78
75
  json = migrator.defaultValue;
79
76
  }
80
77
  } catch (e) {
81
- logger().error(`Error migrating config ${name}:`, e);
78
+ logger().error(`Error migrating config ${parsedPath.name}:`, e);
82
79
  migrationFailed = true;
83
80
  json = migrator.defaultValue;
84
81
  }
@@ -92,9 +89,9 @@ export const setupConfigFile = async <T>(
92
89
  if (shouldSaveBack) {
93
90
  if (parseFailed || migrationFailed) {
94
91
  try {
95
- const parsedPath = path.parse(filesPath);
96
92
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
97
- const corruptedPath = ctx.getConfigPath(
93
+ const corruptedPath = path.join(
94
+ parsedPath.dir,
98
95
  `${parsedPath.name}.corrupted.${timestamp}.json`,
99
96
  );
100
97
  const backupContent = parseFailed
@@ -103,14 +100,14 @@ export const setupConfigFile = async <T>(
103
100
  await fs.writeFile(corruptedPath, backupContent);
104
101
  logger().info(`Corrupted config file preserved at ${corruptedPath}`);
105
102
  } catch (e) {
106
- logger().error(`Failed to backup corrupted config ${name}:`, e);
103
+ logger().error(`Failed to backup corrupted config ${parsedPath.name}:`, e);
107
104
  }
108
105
  }
109
106
 
110
107
  try {
111
108
  await fs.writeFile(filesPath, JSON.stringify(json));
112
109
  } catch (e) {
113
- logger().error(`Error saving migrated config ${name}:`, e);
110
+ logger().error(`Error saving migrated config ${parsedPath.name}:`, e);
114
111
  }
115
112
  }
116
113
 
@@ -119,9 +116,51 @@ export const setupConfigFile = async <T>(
119
116
  };
120
117
  };
121
118
 
122
- export const deleteConfigFile = async (nameOrPath: string, context: PipelabContext) => {
123
- const isAbsolutePath = path.isAbsolute(nameOrPath);
124
- const filesPath = isAbsolutePath ? nameOrPath : context.getConfigPath(`${nameOrPath}.json`);
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
+ };
125
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) => {
126
156
  await fs.rm(filesPath, { force: true });
127
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
 
@@ -85,8 +85,44 @@ export class PipelabContext {
85
85
  return join(this.userDataPath, "config", ...subpaths);
86
86
  }
87
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
+
88
122
  getTempPath(...subpaths: string[]) {
89
- 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);
90
126
  }
91
127
 
92
128
  async createTempFolder(prefix = "pipelab-") {
@@ -99,10 +135,12 @@ export class PipelabContext {
99
135
  getCachePath(): string;
100
136
  getCachePath(folder: CacheFolderType, ...subpaths: string[]): string;
101
137
  getCachePath(folder?: CacheFolderType, ...subpaths: string[]) {
138
+ const settings = this.getSettings();
139
+ const base = settings?.cacheFolder || join(this.userDataPath, "cache");
102
140
  if (!folder) {
103
- return join(this.userDataPath, "cache");
141
+ return base;
104
142
  }
105
- return join(this.userDataPath, "cache", folder, ...subpaths);
143
+ return join(base, folder, ...subpaths);
106
144
  }
107
145
 
108
146
  getPnpmPath(...subpaths: string[]) {