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