@quiltdata/benchling-webhook 0.6.3-20251104T170954Z → 0.7.2-20251106T003353Z
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/README.md +166 -5
- package/dist/bin/benchling-webhook.d.ts +2 -16
- package/dist/bin/benchling-webhook.d.ts.map +1 -1
- package/dist/bin/benchling-webhook.js +98 -158
- package/dist/bin/benchling-webhook.js.map +1 -1
- package/dist/bin/cli.js +96 -8
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/{config-profiles.d.ts → commands/config-profiles.d.ts} +10 -9
- package/dist/bin/commands/config-profiles.d.ts.map +1 -0
- package/dist/bin/{config-profiles.js → commands/config-profiles.js} +110 -102
- package/dist/bin/commands/config-profiles.js.map +1 -0
- package/dist/bin/commands/create-secret.d.ts.map +1 -0
- package/dist/bin/commands/create-secret.js.map +1 -0
- package/dist/bin/commands/deploy.d.ts +11 -0
- package/dist/bin/commands/deploy.d.ts.map +1 -1
- package/dist/bin/commands/deploy.js +65 -110
- package/dist/bin/commands/deploy.js.map +1 -1
- package/dist/bin/{get-env.d.ts → commands/get-env.d.ts} +1 -1
- package/dist/bin/commands/get-env.d.ts.map +1 -0
- package/dist/bin/{get-env.js → commands/get-env.js} +2 -2
- package/dist/bin/commands/get-env.js.map +1 -0
- package/dist/bin/commands/health-check.d.ts +47 -0
- package/dist/bin/commands/health-check.d.ts.map +1 -0
- package/dist/bin/commands/health-check.js +357 -0
- package/dist/bin/commands/health-check.js.map +1 -0
- package/dist/bin/commands/infer-quilt-config.d.ts +50 -0
- package/dist/bin/commands/infer-quilt-config.d.ts.map +1 -0
- package/dist/bin/commands/infer-quilt-config.js +356 -0
- package/dist/bin/commands/infer-quilt-config.js.map +1 -0
- package/dist/bin/commands/init.d.ts.map +1 -1
- package/dist/bin/commands/init.js +2 -32
- package/dist/bin/commands/init.js.map +1 -1
- package/dist/bin/commands/manifest.d.ts +11 -0
- package/dist/bin/commands/manifest.d.ts.map +1 -1
- package/dist/bin/commands/manifest.js +22 -8
- package/dist/bin/commands/manifest.js.map +1 -1
- package/dist/bin/commands/publish.d.ts.map +1 -0
- package/dist/bin/{publish.js → commands/publish.js} +2 -2
- package/dist/bin/commands/publish.js.map +1 -0
- package/dist/bin/commands/setup-profile.d.ts +29 -0
- package/dist/bin/commands/setup-profile.d.ts.map +1 -0
- package/dist/bin/commands/setup-profile.js +220 -0
- package/dist/bin/commands/setup-profile.js.map +1 -0
- package/dist/bin/commands/setup-wizard.d.ts +26 -11
- package/dist/bin/commands/setup-wizard.d.ts.map +1 -1
- package/dist/bin/commands/setup-wizard.js +844 -46
- package/dist/bin/commands/setup-wizard.js.map +1 -1
- package/dist/{scripts → bin/commands}/sync-secrets.d.ts +6 -1
- package/dist/bin/commands/sync-secrets.d.ts.map +1 -0
- package/dist/{scripts → bin/commands}/sync-secrets.js +159 -55
- package/dist/bin/commands/sync-secrets.js.map +1 -0
- package/dist/bin/commands/validate.d.ts.map +1 -1
- package/dist/bin/commands/validate.js +2 -12
- package/dist/bin/commands/validate.js.map +1 -1
- package/dist/lib/alb-api-gateway.d.ts +7 -1
- package/dist/lib/alb-api-gateway.d.ts.map +1 -1
- package/dist/lib/alb-api-gateway.js +9 -6
- package/dist/lib/alb-api-gateway.js.map +1 -1
- package/dist/lib/benchling-webhook-stack.d.ts +13 -12
- package/dist/lib/benchling-webhook-stack.d.ts.map +1 -1
- package/dist/lib/benchling-webhook-stack.js +43 -30
- package/dist/lib/benchling-webhook-stack.js.map +1 -1
- package/dist/lib/configuration-saver.d.ts +4 -16
- package/dist/lib/configuration-saver.d.ts.map +1 -1
- package/dist/lib/configuration-saver.js +14 -54
- package/dist/lib/configuration-saver.js.map +1 -1
- package/dist/lib/fargate-service.d.ts +11 -21
- package/dist/lib/fargate-service.d.ts.map +1 -1
- package/dist/lib/fargate-service.js +79 -176
- package/dist/lib/fargate-service.js.map +1 -1
- package/dist/lib/types/config.d.ts +591 -224
- package/dist/lib/types/config.d.ts.map +1 -1
- package/dist/lib/types/config.js +134 -3
- package/dist/lib/types/config.js.map +1 -1
- package/dist/lib/utils/config.d.ts.map +1 -1
- package/dist/lib/utils/config.js.map +1 -1
- package/dist/lib/xdg-config.d.ts +222 -106
- package/dist/lib/xdg-config.d.ts.map +1 -1
- package/dist/lib/xdg-config.js +448 -387
- package/dist/lib/xdg-config.js.map +1 -1
- package/dist/package.json +16 -13
- package/dist/scripts/check-logs.d.ts +12 -0
- package/dist/scripts/check-logs.d.ts.map +1 -0
- package/dist/{bin → scripts}/check-logs.js +65 -15
- package/dist/scripts/check-logs.js.map +1 -0
- package/dist/scripts/check-webhook-verification.d.ts +3 -0
- package/dist/scripts/check-webhook-verification.d.ts.map +1 -0
- package/dist/{bin/test-invalid-signature.js → scripts/check-webhook-verification.js} +1 -1
- package/dist/scripts/check-webhook-verification.js.map +1 -0
- package/dist/scripts/infer-quilt-config.d.ts +23 -26
- package/dist/scripts/infer-quilt-config.d.ts.map +1 -1
- package/dist/scripts/infer-quilt-config.js +58 -96
- package/dist/scripts/infer-quilt-config.js.map +1 -1
- package/dist/scripts/send-event.d.ts.map +1 -0
- package/dist/scripts/send-event.js.map +1 -0
- package/dist/{bin → scripts}/version.d.ts +3 -1
- package/dist/scripts/version.d.ts.map +1 -0
- package/dist/{bin → scripts}/version.js +95 -9
- package/dist/scripts/version.js.map +1 -0
- package/package.json +16 -13
- package/dist/bin/check-logs.d.ts +0 -7
- package/dist/bin/check-logs.d.ts.map +0 -1
- package/dist/bin/check-logs.js.map +0 -1
- package/dist/bin/config-profiles.d.ts.map +0 -1
- package/dist/bin/config-profiles.js.map +0 -1
- package/dist/bin/create-secret.d.ts.map +0 -1
- package/dist/bin/create-secret.js.map +0 -1
- package/dist/bin/dev-deploy.d.ts +0 -20
- package/dist/bin/dev-deploy.d.ts.map +0 -1
- package/dist/bin/dev-deploy.js +0 -289
- package/dist/bin/dev-deploy.js.map +0 -1
- package/dist/bin/get-env.d.ts.map +0 -1
- package/dist/bin/get-env.js.map +0 -1
- package/dist/bin/publish.d.ts.map +0 -1
- package/dist/bin/publish.js.map +0 -1
- package/dist/bin/release.d.ts +0 -11
- package/dist/bin/release.d.ts.map +0 -1
- package/dist/bin/release.js +0 -141
- package/dist/bin/release.js.map +0 -1
- package/dist/bin/send-event.d.ts.map +0 -1
- package/dist/bin/send-event.js.map +0 -1
- package/dist/bin/test-invalid-signature.d.ts +0 -3
- package/dist/bin/test-invalid-signature.d.ts.map +0 -1
- package/dist/bin/test-invalid-signature.js.map +0 -1
- package/dist/bin/version.d.ts.map +0 -1
- package/dist/bin/version.js.map +0 -1
- package/dist/scripts/config-health-check.d.ts +0 -84
- package/dist/scripts/config-health-check.d.ts.map +0 -1
- package/dist/scripts/config-health-check.js +0 -659
- package/dist/scripts/config-health-check.js.map +0 -1
- package/dist/scripts/install-wizard.d.ts +0 -34
- package/dist/scripts/install-wizard.d.ts.map +0 -1
- package/dist/scripts/install-wizard.js +0 -719
- package/dist/scripts/install-wizard.js.map +0 -1
- package/dist/scripts/sync-secrets.d.ts.map +0 -1
- package/dist/scripts/sync-secrets.js.map +0 -1
- /package/dist/bin/{create-secret.d.ts → commands/create-secret.d.ts} +0 -0
- /package/dist/bin/{create-secret.js → commands/create-secret.js} +0 -0
- /package/dist/bin/{publish.d.ts → commands/publish.d.ts} +0 -0
- /package/dist/{bin → scripts}/send-event.d.ts +0 -0
- /package/dist/{bin → scripts}/send-event.js +0 -0
package/dist/lib/xdg-config.js
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* XDG Configuration Management
|
|
3
|
+
* XDG Configuration Management (v0.7.0 - BREAKING CHANGE)
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* Implements a three-file configuration model:
|
|
7
|
-
* - User configuration: User-provided default settings
|
|
8
|
-
* - Derived configuration: CLI-inferred configuration
|
|
9
|
-
* - Deployment configuration: Deployment-specific artifacts
|
|
5
|
+
* Complete rewrite with NO backward compatibility with v0.6.x.
|
|
10
6
|
*
|
|
11
|
-
*
|
|
7
|
+
* This module provides XDG-compliant configuration management for the Benchling Webhook system
|
|
8
|
+
* with a simplified, profile-first architecture:
|
|
9
|
+
*
|
|
10
|
+
* - Single unified configuration file per profile (`config.json`)
|
|
11
|
+
* - Per-profile deployment tracking (`deployments.json`)
|
|
12
|
+
* - Profile inheritance support with deep merging
|
|
13
|
+
* - Comprehensive validation and helpful error messages
|
|
14
|
+
*
|
|
15
|
+
* Directory Structure:
|
|
16
|
+
* ```
|
|
17
|
+
* ~/.config/benchling-webhook/
|
|
18
|
+
* ├── default/
|
|
19
|
+
* │ ├── config.json # Profile configuration
|
|
20
|
+
* │ └── deployments.json # Deployment history
|
|
21
|
+
* └── dev/
|
|
22
|
+
* ├── config.json
|
|
23
|
+
* └── deployments.json
|
|
24
|
+
* ```
|
|
12
25
|
*
|
|
13
26
|
* @module xdg-config
|
|
27
|
+
* @version 0.7.0
|
|
14
28
|
*/
|
|
15
29
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
30
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -20,67 +34,36 @@ exports.XDGConfig = void 0;
|
|
|
20
34
|
const fs_1 = require("fs");
|
|
21
35
|
const path_1 = require("path");
|
|
22
36
|
const os_1 = require("os");
|
|
23
|
-
const os_2 = require("os");
|
|
24
37
|
const ajv_1 = __importDefault(require("ajv"));
|
|
38
|
+
const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
25
39
|
const lodash_merge_1 = __importDefault(require("lodash.merge"));
|
|
40
|
+
const config_1 = require("./types/config");
|
|
26
41
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type: "object",
|
|
32
|
-
properties: {
|
|
33
|
-
quiltCatalog: { type: "string" },
|
|
34
|
-
quiltUserBucket: { type: "string" },
|
|
35
|
-
quiltDatabase: { type: "string" },
|
|
36
|
-
quiltStackArn: { type: "string" },
|
|
37
|
-
quiltRegion: { type: "string" },
|
|
38
|
-
catalogUrl: { type: "string" },
|
|
39
|
-
benchlingTenant: { type: "string" },
|
|
40
|
-
benchlingClientId: { type: "string" },
|
|
41
|
-
benchlingClientSecret: { type: "string" },
|
|
42
|
-
benchlingAppDefinitionId: { type: "string" },
|
|
43
|
-
benchlingPkgBucket: { type: "string" },
|
|
44
|
-
benchlingTestEntry: { type: "string" },
|
|
45
|
-
benchlingSecret: { type: "string" },
|
|
46
|
-
benchlingSecrets: { type: "string" },
|
|
47
|
-
cdkAccount: { type: "string" },
|
|
48
|
-
cdkRegion: { type: "string" },
|
|
49
|
-
awsProfile: { type: "string" },
|
|
50
|
-
queueArn: { type: "string" },
|
|
51
|
-
pkgPrefix: { type: "string" },
|
|
52
|
-
pkgKey: { type: "string" },
|
|
53
|
-
logLevel: { type: "string" },
|
|
54
|
-
webhookAllowList: { type: "string" },
|
|
55
|
-
webhookEndpoint: { type: "string" },
|
|
56
|
-
enableWebhookVerification: { type: "string" },
|
|
57
|
-
createEcrRepository: { type: "string" },
|
|
58
|
-
ecrRepositoryName: { type: "string" },
|
|
59
|
-
imageTag: { type: "string" },
|
|
60
|
-
webhookUrl: { type: "string" },
|
|
61
|
-
deploymentTimestamp: { type: "string" },
|
|
62
|
-
deployedAt: { type: "string" },
|
|
63
|
-
stackArn: { type: "string" },
|
|
64
|
-
_metadata: {
|
|
65
|
-
type: "object",
|
|
66
|
-
properties: {
|
|
67
|
-
savedAt: { type: "string" },
|
|
68
|
-
source: { type: "string" },
|
|
69
|
-
version: { type: "string" },
|
|
70
|
-
inferredAt: { type: "string" },
|
|
71
|
-
inferredFrom: { type: "string" },
|
|
72
|
-
deployedBy: { type: "string" },
|
|
73
|
-
stackName: { type: "string" },
|
|
74
|
-
},
|
|
75
|
-
additionalProperties: true,
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
additionalProperties: true,
|
|
79
|
-
};
|
|
80
|
-
/**
|
|
81
|
-
* XDG Configuration Manager
|
|
42
|
+
* XDG Configuration Manager (v0.7.0)
|
|
43
|
+
*
|
|
44
|
+
* Manages profile-based configuration with deployment tracking.
|
|
45
|
+
* NO backward compatibility with v0.6.x configuration files.
|
|
82
46
|
*
|
|
83
|
-
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const xdg = new XDGConfig();
|
|
50
|
+
*
|
|
51
|
+
* // Read profile configuration
|
|
52
|
+
* const config = xdg.readProfile("default");
|
|
53
|
+
*
|
|
54
|
+
* // Write profile configuration
|
|
55
|
+
* xdg.writeProfile("default", config);
|
|
56
|
+
*
|
|
57
|
+
* // Record deployment
|
|
58
|
+
* xdg.recordDeployment("default", {
|
|
59
|
+
* stage: "prod",
|
|
60
|
+
* timestamp: new Date().toISOString(),
|
|
61
|
+
* imageTag: "0.7.0",
|
|
62
|
+
* endpoint: "https://...",
|
|
63
|
+
* stackName: "BenchlingWebhookStack",
|
|
64
|
+
* region: "us-east-1"
|
|
65
|
+
* });
|
|
66
|
+
* ```
|
|
84
67
|
*/
|
|
85
68
|
class XDGConfig {
|
|
86
69
|
/**
|
|
@@ -90,108 +73,48 @@ class XDGConfig {
|
|
|
90
73
|
*/
|
|
91
74
|
constructor(baseDir) {
|
|
92
75
|
this.baseDir = baseDir || this.getDefaultBaseDir();
|
|
76
|
+
this.ensureBaseDirectoryExists();
|
|
93
77
|
}
|
|
94
78
|
/**
|
|
95
79
|
* Gets the default XDG base directory
|
|
96
80
|
*
|
|
97
|
-
* @returns The default base directory path
|
|
81
|
+
* @returns The default base directory path (~/.config/benchling-webhook)
|
|
98
82
|
*/
|
|
99
83
|
getDefaultBaseDir() {
|
|
100
84
|
const home = (0, os_1.homedir)();
|
|
101
85
|
return (0, path_1.resolve)(home, ".config", "benchling-webhook");
|
|
102
86
|
}
|
|
103
87
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* @param path - Path potentially containing ~ for home directory
|
|
107
|
-
* @returns Expanded absolute path
|
|
108
|
-
*/
|
|
109
|
-
static expandHomeDir(path) {
|
|
110
|
-
const home = (0, os_1.homedir)();
|
|
111
|
-
return path.replace(/^~/, home);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Gets the configuration file paths
|
|
115
|
-
*
|
|
116
|
-
* @returns Object containing paths to all configuration files
|
|
117
|
-
*/
|
|
118
|
-
static getPaths() {
|
|
119
|
-
const home = (0, os_1.homedir)();
|
|
120
|
-
const baseDir = (0, path_1.resolve)(home, ".config", "benchling-webhook");
|
|
121
|
-
return {
|
|
122
|
-
userConfig: (0, path_1.resolve)(baseDir, "default.json"),
|
|
123
|
-
derivedConfig: (0, path_1.resolve)(baseDir, "config", "default.json"),
|
|
124
|
-
deployConfig: (0, path_1.resolve)(baseDir, "deploy", "default.json"),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Gets the configuration file paths for this instance
|
|
129
|
-
*
|
|
130
|
-
* @returns Object containing paths to all configuration files
|
|
131
|
-
*/
|
|
132
|
-
getPaths() {
|
|
133
|
-
return {
|
|
134
|
-
userConfig: (0, path_1.resolve)(this.baseDir, "default.json"),
|
|
135
|
-
derivedConfig: (0, path_1.resolve)(this.baseDir, "config", "default.json"),
|
|
136
|
-
deployConfig: (0, path_1.resolve)(this.baseDir, "deploy", "default.json"),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* Ensures all required configuration directories exist
|
|
141
|
-
*
|
|
142
|
-
* Creates the base configuration directory and subdirectories if they don't exist.
|
|
88
|
+
* Ensures the base configuration directory exists
|
|
143
89
|
*
|
|
144
90
|
* @throws {Error} If directory creation fails
|
|
145
91
|
*/
|
|
146
|
-
|
|
147
|
-
// Create base directory
|
|
92
|
+
ensureBaseDirectoryExists() {
|
|
148
93
|
if (!(0, fs_1.existsSync)(this.baseDir)) {
|
|
149
94
|
(0, fs_1.mkdirSync)(this.baseDir, { recursive: true });
|
|
150
95
|
}
|
|
151
|
-
// Create config subdirectory
|
|
152
|
-
const configDir = (0, path_1.resolve)(this.baseDir, "config");
|
|
153
|
-
if (!(0, fs_1.existsSync)(configDir)) {
|
|
154
|
-
(0, fs_1.mkdirSync)(configDir, { recursive: true });
|
|
155
|
-
}
|
|
156
|
-
// Create deploy subdirectory
|
|
157
|
-
const deployDir = (0, path_1.resolve)(this.baseDir, "deploy");
|
|
158
|
-
if (!(0, fs_1.existsSync)(deployDir)) {
|
|
159
|
-
(0, fs_1.mkdirSync)(deployDir, { recursive: true });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Gets the file path for a specific configuration type
|
|
164
|
-
*
|
|
165
|
-
* @param configType - Type of configuration to read
|
|
166
|
-
* @returns Absolute path to the configuration file
|
|
167
|
-
*/
|
|
168
|
-
getConfigPath(configType) {
|
|
169
|
-
const paths = this.getPaths();
|
|
170
|
-
switch (configType) {
|
|
171
|
-
case "user":
|
|
172
|
-
return paths.userConfig;
|
|
173
|
-
case "derived":
|
|
174
|
-
return paths.derivedConfig;
|
|
175
|
-
case "deploy":
|
|
176
|
-
return paths.deployConfig;
|
|
177
|
-
default:
|
|
178
|
-
throw new Error(`Unknown configuration type: ${configType}`);
|
|
179
|
-
}
|
|
180
96
|
}
|
|
97
|
+
// ====================================================================
|
|
98
|
+
// Configuration Management
|
|
99
|
+
// ====================================================================
|
|
181
100
|
/**
|
|
182
|
-
* Reads
|
|
101
|
+
* Reads configuration for a profile
|
|
183
102
|
*
|
|
184
|
-
* @param
|
|
103
|
+
* @param profile - Profile name (e.g., "default", "dev", "prod")
|
|
185
104
|
* @returns Parsed configuration object
|
|
186
|
-
* @throws {Error} If
|
|
105
|
+
* @throws {Error} If profile not found or configuration is invalid
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const config = xdg.readProfile("default");
|
|
110
|
+
* console.log(config.benchling.tenant);
|
|
111
|
+
* ```
|
|
187
112
|
*/
|
|
188
|
-
|
|
189
|
-
const configPath = this.
|
|
190
|
-
// Check if file exists
|
|
113
|
+
readProfile(profile) {
|
|
114
|
+
const configPath = this.getProfileConfigPath(profile);
|
|
191
115
|
if (!(0, fs_1.existsSync)(configPath)) {
|
|
192
|
-
throw new Error(
|
|
116
|
+
throw new Error(this.buildProfileNotFoundError(profile));
|
|
193
117
|
}
|
|
194
|
-
// Read file content
|
|
195
118
|
let fileContent;
|
|
196
119
|
try {
|
|
197
120
|
fileContent = (0, fs_1.readFileSync)(configPath, "utf-8");
|
|
@@ -199,7 +122,6 @@ class XDGConfig {
|
|
|
199
122
|
catch (error) {
|
|
200
123
|
throw new Error(`Failed to read configuration file: ${configPath}. ${error.message}`);
|
|
201
124
|
}
|
|
202
|
-
// Parse JSON
|
|
203
125
|
let config;
|
|
204
126
|
try {
|
|
205
127
|
config = JSON.parse(fileContent);
|
|
@@ -208,61 +130,52 @@ class XDGConfig {
|
|
|
208
130
|
throw new Error(`Invalid JSON in configuration file: ${configPath}. ${error.message}`);
|
|
209
131
|
}
|
|
210
132
|
// Validate schema
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!valid) {
|
|
215
|
-
const errors = validate.errors?.map((err) => `${err.instancePath} ${err.message}`).join(", ");
|
|
216
|
-
throw new Error(`Invalid configuration schema in ${configPath}: ${errors}`);
|
|
133
|
+
const validation = this.validateProfile(config);
|
|
134
|
+
if (!validation.isValid) {
|
|
135
|
+
throw new Error(`Invalid configuration in ${configPath}:\n${validation.errors.join("\n")}`);
|
|
217
136
|
}
|
|
218
137
|
return config;
|
|
219
138
|
}
|
|
220
139
|
/**
|
|
221
|
-
*
|
|
140
|
+
* Writes configuration for a profile
|
|
222
141
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*/
|
|
226
|
-
getBackupPath(configType) {
|
|
227
|
-
const configPath = this.getConfigPath(configType);
|
|
228
|
-
return `${configPath}.backup`;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Validates configuration against schema
|
|
142
|
+
* Creates the profile directory if it doesn't exist.
|
|
143
|
+
* Performs atomic write with automatic backup.
|
|
232
144
|
*
|
|
233
|
-
* @param
|
|
234
|
-
* @throws {Error} If validation fails
|
|
235
|
-
*/
|
|
236
|
-
validateConfigSchema(config) {
|
|
237
|
-
const ajv = new ajv_1.default();
|
|
238
|
-
const validate = ajv.compile(CONFIG_SCHEMA);
|
|
239
|
-
const valid = validate(config);
|
|
240
|
-
if (!valid) {
|
|
241
|
-
const errors = validate.errors?.map((err) => `${err.instancePath} ${err.message}`).join(", ");
|
|
242
|
-
throw new Error(`Invalid configuration schema: ${errors}`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Writes a configuration file atomically with backup
|
|
247
|
-
*
|
|
248
|
-
* Uses a temporary file and rename operation for atomic writes.
|
|
249
|
-
* Creates a backup of the existing file before overwriting.
|
|
250
|
-
*
|
|
251
|
-
* @param configType - Type of configuration to write ("user", "derived", or "deploy")
|
|
145
|
+
* @param profile - Profile name
|
|
252
146
|
* @param config - Configuration object to write
|
|
253
147
|
* @throws {Error} If validation fails or write operation fails
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* xdg.writeProfile("default", {
|
|
152
|
+
* quilt: { ... },
|
|
153
|
+
* benchling: { ... },
|
|
154
|
+
* packages: { ... },
|
|
155
|
+
* deployment: { ... },
|
|
156
|
+
* _metadata: {
|
|
157
|
+
* version: "0.7.0",
|
|
158
|
+
* createdAt: new Date().toISOString(),
|
|
159
|
+
* updatedAt: new Date().toISOString(),
|
|
160
|
+
* source: "wizard"
|
|
161
|
+
* }
|
|
162
|
+
* });
|
|
163
|
+
* ```
|
|
254
164
|
*/
|
|
255
|
-
|
|
165
|
+
writeProfile(profile, config) {
|
|
256
166
|
// Validate configuration before writing
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// Ensure
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
167
|
+
const validation = this.validateProfile(config);
|
|
168
|
+
if (!validation.isValid) {
|
|
169
|
+
throw new Error(`Invalid configuration:\n${validation.errors.join("\n")}`);
|
|
170
|
+
}
|
|
171
|
+
// Ensure profile directory exists
|
|
172
|
+
const profileDir = this.getProfileDir(profile);
|
|
173
|
+
if (!(0, fs_1.existsSync)(profileDir)) {
|
|
174
|
+
(0, fs_1.mkdirSync)(profileDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
const configPath = this.getProfileConfigPath(profile);
|
|
177
|
+
const backupPath = `${configPath}.backup`;
|
|
178
|
+
// Create backup if file exists
|
|
266
179
|
if ((0, fs_1.existsSync)(configPath)) {
|
|
267
180
|
try {
|
|
268
181
|
(0, fs_1.copyFileSync)(configPath, backupPath);
|
|
@@ -272,7 +185,7 @@ class XDGConfig {
|
|
|
272
185
|
}
|
|
273
186
|
}
|
|
274
187
|
// Write to temporary file first (atomic write)
|
|
275
|
-
const tempPath = (0, path_1.
|
|
188
|
+
const tempPath = (0, path_1.join)(profileDir, `.config.json.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
276
189
|
const configJson = JSON.stringify(config, null, 4);
|
|
277
190
|
try {
|
|
278
191
|
(0, fs_1.writeFileSync)(tempPath, configJson, "utf-8");
|
|
@@ -282,9 +195,10 @@ class XDGConfig {
|
|
|
282
195
|
}
|
|
283
196
|
catch {
|
|
284
197
|
// Fall back to copy+delete for cross-device scenarios (Windows)
|
|
285
|
-
// Ensure
|
|
286
|
-
|
|
287
|
-
|
|
198
|
+
// Ensure target directory exists before copying
|
|
199
|
+
const targetDir = (0, path_1.dirname)(configPath);
|
|
200
|
+
if (!(0, fs_1.existsSync)(targetDir)) {
|
|
201
|
+
(0, fs_1.mkdirSync)(targetDir, { recursive: true });
|
|
288
202
|
}
|
|
289
203
|
(0, fs_1.copyFileSync)(tempPath, configPath);
|
|
290
204
|
(0, fs_1.unlinkSync)(tempPath);
|
|
@@ -295,271 +209,418 @@ class XDGConfig {
|
|
|
295
209
|
}
|
|
296
210
|
}
|
|
297
211
|
/**
|
|
298
|
-
*
|
|
212
|
+
* Deletes a profile and all its files
|
|
299
213
|
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* Uses deep merge to handle nested objects.
|
|
214
|
+
* WARNING: This is a destructive operation!
|
|
215
|
+
* Cannot delete the "default" profile.
|
|
303
216
|
*
|
|
304
|
-
* @param
|
|
305
|
-
* @
|
|
306
|
-
*/
|
|
307
|
-
mergeConfigs(configs) {
|
|
308
|
-
// Start with empty config
|
|
309
|
-
let merged = {};
|
|
310
|
-
// Merge in priority order: user → derived → deploy
|
|
311
|
-
// Each subsequent config overrides previous values
|
|
312
|
-
if (configs.user) {
|
|
313
|
-
merged = (0, lodash_merge_1.default)({}, merged, configs.user);
|
|
314
|
-
}
|
|
315
|
-
if (configs.derived) {
|
|
316
|
-
merged = (0, lodash_merge_1.default)({}, merged, configs.derived);
|
|
317
|
-
}
|
|
318
|
-
if (configs.deploy) {
|
|
319
|
-
merged = (0, lodash_merge_1.default)({}, merged, configs.deploy);
|
|
320
|
-
}
|
|
321
|
-
return merged;
|
|
322
|
-
}
|
|
323
|
-
// ====================================================================
|
|
324
|
-
// Profile Management Methods (Phase 1.1)
|
|
325
|
-
// ====================================================================
|
|
326
|
-
/**
|
|
327
|
-
* Gets the profile directory path
|
|
217
|
+
* @param profile - Profile name to delete
|
|
218
|
+
* @throws {Error} If attempting to delete default profile or if deletion fails
|
|
328
219
|
*
|
|
329
|
-
* @
|
|
330
|
-
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```typescript
|
|
222
|
+
* xdg.deleteProfile("dev");
|
|
223
|
+
* ```
|
|
331
224
|
*/
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
225
|
+
deleteProfile(profile) {
|
|
226
|
+
if (profile === "default") {
|
|
227
|
+
throw new Error("Cannot delete the default profile");
|
|
335
228
|
}
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Gets configuration file paths for a specific profile
|
|
340
|
-
*
|
|
341
|
-
* @param profileName - Profile name (defaults to "default")
|
|
342
|
-
* @returns Configuration file paths for the profile
|
|
343
|
-
*/
|
|
344
|
-
getProfilePaths(profileName = "default") {
|
|
345
|
-
const profileDir = this.getProfileDir(profileName);
|
|
346
|
-
return {
|
|
347
|
-
userConfig: (0, path_1.resolve)(profileDir, "default.json"),
|
|
348
|
-
derivedConfig: (0, path_1.resolve)(profileDir, "config", "default.json"),
|
|
349
|
-
deployConfig: (0, path_1.resolve)(profileDir, "deploy", "default.json"),
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Ensures profile directories exist
|
|
354
|
-
*
|
|
355
|
-
* @param profileName - Profile name (defaults to "default")
|
|
356
|
-
*/
|
|
357
|
-
ensureProfileDirectories(profileName = "default") {
|
|
358
|
-
const profileDir = this.getProfileDir(profileName);
|
|
359
|
-
// Create profile directory
|
|
229
|
+
const profileDir = this.getProfileDir(profile);
|
|
360
230
|
if (!(0, fs_1.existsSync)(profileDir)) {
|
|
361
|
-
|
|
231
|
+
throw new Error(`Profile does not exist: ${profile}`);
|
|
362
232
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (!(0, fs_1.existsSync)(configDir)) {
|
|
366
|
-
(0, fs_1.mkdirSync)(configDir, { recursive: true });
|
|
233
|
+
try {
|
|
234
|
+
(0, fs_1.rmSync)(profileDir, { recursive: true, force: true });
|
|
367
235
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (!(0, fs_1.existsSync)(deployDir)) {
|
|
371
|
-
(0, fs_1.mkdirSync)(deployDir, { recursive: true });
|
|
236
|
+
catch (error) {
|
|
237
|
+
throw new Error(`Failed to delete profile: ${error.message}`);
|
|
372
238
|
}
|
|
373
239
|
}
|
|
374
240
|
/**
|
|
375
241
|
* Lists all available profiles
|
|
376
242
|
*
|
|
377
243
|
* @returns Array of profile names
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* const profiles = xdg.listProfiles();
|
|
248
|
+
* console.log(profiles); // ["default", "dev", "prod"]
|
|
249
|
+
* ```
|
|
378
250
|
*/
|
|
379
251
|
listProfiles() {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
252
|
+
if (!(0, fs_1.existsSync)(this.baseDir)) {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
const entries = (0, fs_1.readdirSync)(this.baseDir, { withFileTypes: true });
|
|
256
|
+
return entries
|
|
257
|
+
.filter((entry) => entry.isDirectory())
|
|
258
|
+
.map((entry) => entry.name)
|
|
259
|
+
.filter((name) => {
|
|
260
|
+
// Only include directories with config.json
|
|
261
|
+
const configPath = this.getProfileConfigPath(name);
|
|
262
|
+
return (0, fs_1.existsSync)(configPath);
|
|
263
|
+
});
|
|
390
264
|
}
|
|
391
265
|
/**
|
|
392
266
|
* Checks if a profile exists
|
|
393
267
|
*
|
|
394
|
-
* @param
|
|
395
|
-
* @returns True if profile exists, false otherwise
|
|
268
|
+
* @param profile - Profile name to check
|
|
269
|
+
* @returns True if profile exists and has valid config.json, false otherwise
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* if (xdg.profileExists("dev")) {
|
|
274
|
+
* const config = xdg.readProfile("dev");
|
|
275
|
+
* }
|
|
276
|
+
* ```
|
|
396
277
|
*/
|
|
397
|
-
profileExists(
|
|
398
|
-
const
|
|
399
|
-
return (0, fs_1.existsSync)(
|
|
278
|
+
profileExists(profile) {
|
|
279
|
+
const configPath = this.getProfileConfigPath(profile);
|
|
280
|
+
return (0, fs_1.existsSync)(configPath);
|
|
400
281
|
}
|
|
282
|
+
// ====================================================================
|
|
283
|
+
// Deployment Tracking
|
|
284
|
+
// ====================================================================
|
|
401
285
|
/**
|
|
402
|
-
*
|
|
286
|
+
* Gets deployment history for a profile
|
|
403
287
|
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* @
|
|
407
|
-
* @
|
|
288
|
+
* Returns empty history if deployments.json doesn't exist.
|
|
289
|
+
*
|
|
290
|
+
* @param profile - Profile name
|
|
291
|
+
* @returns Deployment history with active deployments and full history
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* const deployments = xdg.getDeployments("default");
|
|
296
|
+
* console.log(deployments.active["prod"]); // Active prod deployment
|
|
297
|
+
* console.log(deployments.history[0]); // Most recent deployment
|
|
298
|
+
* ```
|
|
408
299
|
*/
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
case "derived":
|
|
417
|
-
configPath = paths.derivedConfig;
|
|
418
|
-
break;
|
|
419
|
-
case "deploy":
|
|
420
|
-
configPath = paths.deployConfig;
|
|
421
|
-
break;
|
|
422
|
-
default:
|
|
423
|
-
throw new Error(`Unknown configuration type: ${configType}`);
|
|
424
|
-
}
|
|
425
|
-
// Check if file exists
|
|
426
|
-
if (!(0, fs_1.existsSync)(configPath)) {
|
|
427
|
-
throw new Error(`Configuration file not found: ${configPath}`);
|
|
300
|
+
getDeployments(profile) {
|
|
301
|
+
const deploymentsPath = this.getProfileDeploymentsPath(profile);
|
|
302
|
+
if (!(0, fs_1.existsSync)(deploymentsPath)) {
|
|
303
|
+
return {
|
|
304
|
+
active: {},
|
|
305
|
+
history: [],
|
|
306
|
+
};
|
|
428
307
|
}
|
|
429
|
-
// Read and parse
|
|
430
308
|
let fileContent;
|
|
431
309
|
try {
|
|
432
|
-
fileContent = (0, fs_1.readFileSync)(
|
|
310
|
+
fileContent = (0, fs_1.readFileSync)(deploymentsPath, "utf-8");
|
|
433
311
|
}
|
|
434
312
|
catch (error) {
|
|
435
|
-
throw new Error(`Failed to read
|
|
313
|
+
throw new Error(`Failed to read deployments file: ${deploymentsPath}. ${error.message}`);
|
|
436
314
|
}
|
|
437
|
-
let
|
|
315
|
+
let deployments;
|
|
438
316
|
try {
|
|
439
|
-
|
|
317
|
+
deployments = JSON.parse(fileContent);
|
|
440
318
|
}
|
|
441
319
|
catch (error) {
|
|
442
|
-
throw new Error(`Invalid JSON in
|
|
320
|
+
throw new Error(`Invalid JSON in deployments file: ${deploymentsPath}. ${error.message}`);
|
|
443
321
|
}
|
|
444
322
|
// Validate schema
|
|
445
|
-
|
|
446
|
-
|
|
323
|
+
const ajv = new ajv_1.default();
|
|
324
|
+
(0, ajv_formats_1.default)(ajv);
|
|
325
|
+
const validate = ajv.compile(config_1.DeploymentHistorySchema);
|
|
326
|
+
const valid = validate(deployments);
|
|
327
|
+
if (!valid) {
|
|
328
|
+
const errors = validate.errors?.map((err) => `${err.instancePath} ${err.message}`).join(", ");
|
|
329
|
+
throw new Error(`Invalid deployments schema in ${deploymentsPath}: ${errors}`);
|
|
330
|
+
}
|
|
331
|
+
return deployments;
|
|
447
332
|
}
|
|
448
333
|
/**
|
|
449
|
-
*
|
|
334
|
+
* Records a new deployment for a profile
|
|
450
335
|
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
* @
|
|
336
|
+
* Adds deployment to history and updates active deployment for the stage.
|
|
337
|
+
* Creates deployments.json if it doesn't exist.
|
|
338
|
+
*
|
|
339
|
+
* @param profile - Profile name
|
|
340
|
+
* @param deployment - Deployment record to add
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ```typescript
|
|
344
|
+
* xdg.recordDeployment("default", {
|
|
345
|
+
* stage: "prod",
|
|
346
|
+
* timestamp: new Date().toISOString(),
|
|
347
|
+
* imageTag: "0.7.0",
|
|
348
|
+
* endpoint: "https://abc123.execute-api.us-east-1.amazonaws.com/prod",
|
|
349
|
+
* stackName: "BenchlingWebhookStack",
|
|
350
|
+
* region: "us-east-1",
|
|
351
|
+
* deployedBy: "ernest@example.com",
|
|
352
|
+
* commit: "abc123f"
|
|
353
|
+
* });
|
|
354
|
+
* ```
|
|
455
355
|
*/
|
|
456
|
-
|
|
457
|
-
//
|
|
458
|
-
this.
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const paths = this.getProfilePaths(profileName);
|
|
462
|
-
let configPath;
|
|
463
|
-
switch (configType) {
|
|
464
|
-
case "user":
|
|
465
|
-
configPath = paths.userConfig;
|
|
466
|
-
break;
|
|
467
|
-
case "derived":
|
|
468
|
-
configPath = paths.derivedConfig;
|
|
469
|
-
break;
|
|
470
|
-
case "deploy":
|
|
471
|
-
configPath = paths.deployConfig;
|
|
472
|
-
break;
|
|
473
|
-
default:
|
|
474
|
-
throw new Error(`Unknown configuration type: ${configType}`);
|
|
356
|
+
recordDeployment(profile, deployment) {
|
|
357
|
+
// Ensure profile directory exists
|
|
358
|
+
const profileDir = this.getProfileDir(profile);
|
|
359
|
+
if (!(0, fs_1.existsSync)(profileDir)) {
|
|
360
|
+
(0, fs_1.mkdirSync)(profileDir, { recursive: true });
|
|
475
361
|
}
|
|
476
|
-
|
|
362
|
+
// Load existing deployments or create new
|
|
363
|
+
let deployments;
|
|
364
|
+
try {
|
|
365
|
+
deployments = this.getDeployments(profile);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
deployments = {
|
|
369
|
+
active: {},
|
|
370
|
+
history: [],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// Add to history (newest first)
|
|
374
|
+
deployments.history.unshift(deployment);
|
|
375
|
+
// Update active deployment for this stage
|
|
376
|
+
deployments.active[deployment.stage] = deployment;
|
|
377
|
+
// Write deployments file
|
|
378
|
+
const deploymentsPath = this.getProfileDeploymentsPath(profile);
|
|
379
|
+
const backupPath = `${deploymentsPath}.backup`;
|
|
477
380
|
// Create backup if file exists
|
|
478
|
-
if ((0, fs_1.existsSync)(
|
|
381
|
+
if ((0, fs_1.existsSync)(deploymentsPath)) {
|
|
479
382
|
try {
|
|
480
|
-
(0, fs_1.copyFileSync)(
|
|
383
|
+
(0, fs_1.copyFileSync)(deploymentsPath, backupPath);
|
|
481
384
|
}
|
|
482
385
|
catch (error) {
|
|
483
386
|
throw new Error(`Failed to create backup: ${error.message}`);
|
|
484
387
|
}
|
|
485
388
|
}
|
|
486
389
|
// Write to temporary file first (atomic write)
|
|
487
|
-
const tempPath = (0, path_1.
|
|
488
|
-
const
|
|
390
|
+
const tempPath = (0, path_1.join)(profileDir, `.deployments.json.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
391
|
+
const deploymentsJson = JSON.stringify(deployments, null, 4);
|
|
489
392
|
try {
|
|
490
|
-
(0, fs_1.writeFileSync)(tempPath,
|
|
393
|
+
(0, fs_1.writeFileSync)(tempPath, deploymentsJson, "utf-8");
|
|
491
394
|
// Atomic rename (with fallback for cross-device on Windows)
|
|
492
395
|
try {
|
|
493
|
-
(0, fs_1.renameSync)(tempPath,
|
|
396
|
+
(0, fs_1.renameSync)(tempPath, deploymentsPath);
|
|
494
397
|
}
|
|
495
398
|
catch {
|
|
496
399
|
// Fall back to copy+delete for cross-device scenarios (Windows)
|
|
497
|
-
|
|
400
|
+
// Ensure target directory exists before copying
|
|
401
|
+
const targetDir = (0, path_1.dirname)(deploymentsPath);
|
|
402
|
+
if (!(0, fs_1.existsSync)(targetDir)) {
|
|
403
|
+
(0, fs_1.mkdirSync)(targetDir, { recursive: true });
|
|
404
|
+
}
|
|
405
|
+
(0, fs_1.copyFileSync)(tempPath, deploymentsPath);
|
|
498
406
|
(0, fs_1.unlinkSync)(tempPath);
|
|
499
407
|
}
|
|
500
408
|
}
|
|
501
409
|
catch (error) {
|
|
502
|
-
throw new Error(`Failed to write
|
|
410
|
+
throw new Error(`Failed to write deployments file: ${error.message}`);
|
|
503
411
|
}
|
|
504
412
|
}
|
|
505
413
|
/**
|
|
506
|
-
*
|
|
414
|
+
* Gets the active deployment for a specific stage
|
|
415
|
+
*
|
|
416
|
+
* @param profile - Profile name
|
|
417
|
+
* @param stage - Stage name (e.g., "dev", "prod")
|
|
418
|
+
* @returns Active deployment record for the stage, or null if none exists
|
|
507
419
|
*
|
|
508
|
-
* @
|
|
509
|
-
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```typescript
|
|
422
|
+
* const prodDeployment = xdg.getActiveDeployment("default", "prod");
|
|
423
|
+
* if (prodDeployment) {
|
|
424
|
+
* console.log("Prod endpoint:", prodDeployment.endpoint);
|
|
425
|
+
* }
|
|
426
|
+
* ```
|
|
510
427
|
*/
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const paths = this.getProfilePaths(profileName);
|
|
516
|
-
// Load user config if exists
|
|
517
|
-
if ((0, fs_1.existsSync)(paths.userConfig)) {
|
|
518
|
-
try {
|
|
519
|
-
profile.user = this.readProfileConfig("user", profileName);
|
|
520
|
-
}
|
|
521
|
-
catch {
|
|
522
|
-
// User config is optional
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
// Load derived config if exists
|
|
526
|
-
if ((0, fs_1.existsSync)(paths.derivedConfig)) {
|
|
527
|
-
try {
|
|
528
|
-
profile.derived = this.readProfileConfig("derived", profileName);
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
// Derived config is optional
|
|
532
|
-
}
|
|
428
|
+
getActiveDeployment(profile, stage) {
|
|
429
|
+
try {
|
|
430
|
+
const deployments = this.getDeployments(profile);
|
|
431
|
+
return deployments.active[stage] || null;
|
|
533
432
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
try {
|
|
537
|
-
profile.deploy = this.readProfileConfig("deploy", profileName);
|
|
538
|
-
}
|
|
539
|
-
catch {
|
|
540
|
-
// Deploy config is optional
|
|
541
|
-
}
|
|
433
|
+
catch {
|
|
434
|
+
return null;
|
|
542
435
|
}
|
|
543
|
-
return profile;
|
|
544
436
|
}
|
|
437
|
+
// ====================================================================
|
|
438
|
+
// Profile Inheritance
|
|
439
|
+
// ====================================================================
|
|
545
440
|
/**
|
|
546
|
-
*
|
|
441
|
+
* Reads profile configuration with inheritance support
|
|
547
442
|
*
|
|
548
|
-
*
|
|
443
|
+
* If the profile has an `_inherits` field, loads the base profile first
|
|
444
|
+
* and deep merges the current profile on top.
|
|
445
|
+
*
|
|
446
|
+
* Detects and prevents circular inheritance chains.
|
|
549
447
|
*
|
|
550
|
-
* @param
|
|
551
|
-
* @
|
|
448
|
+
* @param profile - Profile name to read
|
|
449
|
+
* @param baseProfile - Optional explicit base profile (overrides `_inherits`)
|
|
450
|
+
* @returns Merged configuration with inheritance applied
|
|
451
|
+
* @throws {Error} If circular inheritance is detected
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* // dev/config.json has "_inherits": "default"
|
|
456
|
+
* const devConfig = xdg.readProfileWithInheritance("dev");
|
|
457
|
+
* // Returns default config deep-merged with dev overrides
|
|
458
|
+
* ```
|
|
552
459
|
*/
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
460
|
+
readProfileWithInheritance(profile, baseProfile) {
|
|
461
|
+
const visited = new Set();
|
|
462
|
+
return this.readProfileWithInheritanceInternal(profile, baseProfile, visited);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Internal recursive implementation of profile inheritance
|
|
466
|
+
*
|
|
467
|
+
* @param profile - Current profile name
|
|
468
|
+
* @param explicitBase - Explicitly specified base profile
|
|
469
|
+
* @param visited - Set of visited profiles (for circular detection)
|
|
470
|
+
* @returns Merged configuration
|
|
471
|
+
* @throws {Error} If circular inheritance is detected
|
|
472
|
+
*/
|
|
473
|
+
readProfileWithInheritanceInternal(profile, explicitBase, visited) {
|
|
474
|
+
// Detect circular inheritance
|
|
475
|
+
if (visited.has(profile)) {
|
|
476
|
+
const chain = Array.from(visited).join(" -> ");
|
|
477
|
+
throw new Error(`Circular inheritance detected: ${chain} -> ${profile}`);
|
|
478
|
+
}
|
|
479
|
+
visited.add(profile);
|
|
480
|
+
// Read current profile
|
|
481
|
+
const config = this.readProfile(profile);
|
|
482
|
+
// Determine base profile
|
|
483
|
+
const baseProfileName = explicitBase || config._inherits;
|
|
484
|
+
// No inheritance - return as-is
|
|
485
|
+
if (!baseProfileName) {
|
|
486
|
+
return config;
|
|
487
|
+
}
|
|
488
|
+
// Load base profile with inheritance
|
|
489
|
+
const baseConfig = this.readProfileWithInheritanceInternal(baseProfileName, undefined, visited);
|
|
490
|
+
// Deep merge: base config first, then current profile overrides
|
|
491
|
+
const merged = this.deepMergeConfigs(baseConfig, config);
|
|
492
|
+
// Remove _inherits from final result (it's already applied)
|
|
493
|
+
delete merged._inherits;
|
|
494
|
+
return merged;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Deep merges two profile configurations
|
|
498
|
+
*
|
|
499
|
+
* Nested objects are merged recursively.
|
|
500
|
+
* Arrays are replaced (not concatenated).
|
|
501
|
+
* Current config takes precedence over base config.
|
|
502
|
+
*
|
|
503
|
+
* @param base - Base configuration
|
|
504
|
+
* @param current - Current configuration (takes precedence)
|
|
505
|
+
* @returns Merged configuration
|
|
506
|
+
*/
|
|
507
|
+
deepMergeConfigs(base, current) {
|
|
508
|
+
return (0, lodash_merge_1.default)({}, base, current);
|
|
509
|
+
}
|
|
510
|
+
// ====================================================================
|
|
511
|
+
// Validation
|
|
512
|
+
// ====================================================================
|
|
513
|
+
/**
|
|
514
|
+
* Validates a profile configuration against the schema
|
|
515
|
+
*
|
|
516
|
+
* @param config - Configuration object to validate
|
|
517
|
+
* @returns Validation result with errors and warnings
|
|
518
|
+
*
|
|
519
|
+
* @example
|
|
520
|
+
* ```typescript
|
|
521
|
+
* const validation = xdg.validateProfile(config);
|
|
522
|
+
* if (!validation.isValid) {
|
|
523
|
+
* console.error("Validation errors:", validation.errors);
|
|
524
|
+
* }
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
validateProfile(config) {
|
|
528
|
+
const ajv = new ajv_1.default({ allErrors: true, strict: false });
|
|
529
|
+
(0, ajv_formats_1.default)(ajv);
|
|
530
|
+
const validate = ajv.compile(config_1.ProfileConfigSchema);
|
|
531
|
+
const valid = validate(config);
|
|
532
|
+
if (valid) {
|
|
533
|
+
return {
|
|
534
|
+
isValid: true,
|
|
535
|
+
errors: [],
|
|
536
|
+
warnings: [],
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const errors = validate.errors?.map((err) => {
|
|
540
|
+
const path = err.instancePath || "(root)";
|
|
541
|
+
return `${path}: ${err.message}`;
|
|
542
|
+
}) || [];
|
|
543
|
+
return {
|
|
544
|
+
isValid: false,
|
|
545
|
+
errors,
|
|
546
|
+
warnings: [],
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
// ====================================================================
|
|
550
|
+
// Path Helpers
|
|
551
|
+
// ====================================================================
|
|
552
|
+
/**
|
|
553
|
+
* Gets the directory path for a profile
|
|
554
|
+
*
|
|
555
|
+
* @param profile - Profile name
|
|
556
|
+
* @returns Absolute path to profile directory
|
|
557
|
+
*/
|
|
558
|
+
getProfileDir(profile) {
|
|
559
|
+
return (0, path_1.join)(this.baseDir, profile);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Gets the config.json path for a profile
|
|
563
|
+
*
|
|
564
|
+
* @param profile - Profile name
|
|
565
|
+
* @returns Absolute path to config.json
|
|
566
|
+
*/
|
|
567
|
+
getProfileConfigPath(profile) {
|
|
568
|
+
return (0, path_1.join)(this.getProfileDir(profile), "config.json");
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Gets the deployments.json path for a profile
|
|
572
|
+
*
|
|
573
|
+
* @param profile - Profile name
|
|
574
|
+
* @returns Absolute path to deployments.json
|
|
575
|
+
*/
|
|
576
|
+
getProfileDeploymentsPath(profile) {
|
|
577
|
+
return (0, path_1.join)(this.getProfileDir(profile), "deployments.json");
|
|
578
|
+
}
|
|
579
|
+
// ====================================================================
|
|
580
|
+
// Error Messages
|
|
581
|
+
// ====================================================================
|
|
582
|
+
/**
|
|
583
|
+
* Builds a helpful error message when a profile is not found
|
|
584
|
+
*
|
|
585
|
+
* Detects legacy v0.6.x configuration files and provides upgrade guidance.
|
|
586
|
+
*
|
|
587
|
+
* @param profile - Profile name that was not found
|
|
588
|
+
* @returns Formatted error message
|
|
589
|
+
*/
|
|
590
|
+
buildProfileNotFoundError(profile) {
|
|
591
|
+
const legacyFiles = [
|
|
592
|
+
(0, path_1.join)(this.baseDir, "default.json"),
|
|
593
|
+
(0, path_1.join)(this.baseDir, "deploy.json"),
|
|
594
|
+
(0, path_1.join)(this.baseDir, "profiles"),
|
|
595
|
+
];
|
|
596
|
+
const hasLegacyFiles = legacyFiles.some((f) => (0, fs_1.existsSync)(f));
|
|
597
|
+
if (hasLegacyFiles) {
|
|
598
|
+
return `
|
|
599
|
+
Profile not found: ${profile}
|
|
600
|
+
|
|
601
|
+
Configuration format changed in v0.7.0.
|
|
602
|
+
Your old configuration files are not compatible.
|
|
603
|
+
|
|
604
|
+
Please run setup wizard to create new configuration:
|
|
605
|
+
npx @quiltdata/benchling-webhook@latest setup
|
|
606
|
+
|
|
607
|
+
Your old configuration files remain at:
|
|
608
|
+
~/.config/benchling-webhook/default.json
|
|
609
|
+
~/.config/benchling-webhook/deploy.json
|
|
610
|
+
|
|
611
|
+
You can manually reference these files to re-enter your settings.
|
|
612
|
+
`.trim();
|
|
613
|
+
}
|
|
614
|
+
return `
|
|
615
|
+
Profile not found: ${profile}
|
|
616
|
+
|
|
617
|
+
No configuration found for profile: ${profile}
|
|
618
|
+
|
|
619
|
+
Run setup wizard to create configuration:
|
|
620
|
+
npx @quiltdata/benchling-webhook@latest setup
|
|
621
|
+
|
|
622
|
+
Available profiles: ${this.listProfiles().join(", ") || "(none)"}
|
|
623
|
+
`.trim();
|
|
563
624
|
}
|
|
564
625
|
}
|
|
565
626
|
exports.XDGConfig = XDGConfig;
|