@quiltdata/benchling-webhook 0.6.3-20251104T170954Z → 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.
Files changed (159) hide show
  1. package/README.md +166 -5
  2. package/dist/bin/benchling-webhook.d.ts +2 -16
  3. package/dist/bin/benchling-webhook.d.ts.map +1 -1
  4. package/dist/bin/benchling-webhook.js +96 -158
  5. package/dist/bin/benchling-webhook.js.map +1 -1
  6. package/dist/bin/cli.js +96 -8
  7. package/dist/bin/cli.js.map +1 -1
  8. package/dist/bin/{config-profiles.d.ts → commands/config-profiles.d.ts} +10 -9
  9. package/dist/bin/commands/config-profiles.d.ts.map +1 -0
  10. package/dist/bin/{config-profiles.js → commands/config-profiles.js} +109 -102
  11. package/dist/bin/commands/config-profiles.js.map +1 -0
  12. package/dist/bin/commands/create-secret.d.ts.map +1 -0
  13. package/dist/bin/commands/create-secret.js.map +1 -0
  14. package/dist/bin/commands/deploy.d.ts +12 -1
  15. package/dist/bin/commands/deploy.d.ts.map +1 -1
  16. package/dist/bin/commands/deploy.js +117 -107
  17. package/dist/bin/commands/deploy.js.map +1 -1
  18. package/dist/bin/{get-env.d.ts → commands/get-env.d.ts} +1 -1
  19. package/dist/bin/commands/get-env.d.ts.map +1 -0
  20. package/dist/bin/{get-env.js → commands/get-env.js} +2 -2
  21. package/dist/bin/commands/get-env.js.map +1 -0
  22. package/dist/bin/commands/health-check.d.ts +47 -0
  23. package/dist/bin/commands/health-check.d.ts.map +1 -0
  24. package/dist/bin/commands/health-check.js +357 -0
  25. package/dist/bin/commands/health-check.js.map +1 -0
  26. package/dist/{scripts → bin/commands}/infer-quilt-config.d.ts +6 -15
  27. package/dist/bin/commands/infer-quilt-config.d.ts.map +1 -0
  28. package/dist/{scripts → bin/commands}/infer-quilt-config.js +37 -63
  29. package/dist/bin/commands/infer-quilt-config.js.map +1 -0
  30. package/dist/bin/commands/init.d.ts.map +1 -1
  31. package/dist/bin/commands/init.js +2 -32
  32. package/dist/bin/commands/init.js.map +1 -1
  33. package/dist/bin/commands/publish.d.ts.map +1 -0
  34. package/dist/bin/{publish.js → commands/publish.js} +2 -2
  35. package/dist/bin/commands/publish.js.map +1 -0
  36. package/dist/bin/commands/setup-profile.d.ts +29 -0
  37. package/dist/bin/commands/setup-profile.d.ts.map +1 -0
  38. package/dist/bin/commands/setup-profile.js +218 -0
  39. package/dist/bin/commands/setup-profile.js.map +1 -0
  40. package/dist/bin/commands/setup-wizard.d.ts +24 -11
  41. package/dist/bin/commands/setup-wizard.d.ts.map +1 -1
  42. package/dist/bin/commands/setup-wizard.js +607 -51
  43. package/dist/bin/commands/setup-wizard.js.map +1 -1
  44. package/dist/{scripts → bin/commands}/sync-secrets.d.ts +6 -1
  45. package/dist/bin/commands/sync-secrets.d.ts.map +1 -0
  46. package/dist/{scripts → bin/commands}/sync-secrets.js +159 -55
  47. package/dist/bin/commands/sync-secrets.js.map +1 -0
  48. package/dist/bin/commands/validate.d.ts.map +1 -1
  49. package/dist/bin/commands/validate.js +2 -12
  50. package/dist/bin/commands/validate.js.map +1 -1
  51. package/dist/lib/alb-api-gateway.d.ts +7 -1
  52. package/dist/lib/alb-api-gateway.d.ts.map +1 -1
  53. package/dist/lib/alb-api-gateway.js +9 -6
  54. package/dist/lib/alb-api-gateway.js.map +1 -1
  55. package/dist/lib/benchling-webhook-stack.d.ts +13 -12
  56. package/dist/lib/benchling-webhook-stack.d.ts.map +1 -1
  57. package/dist/lib/benchling-webhook-stack.js +44 -31
  58. package/dist/lib/benchling-webhook-stack.js.map +1 -1
  59. package/dist/lib/configuration-saver.d.ts +4 -24
  60. package/dist/lib/configuration-saver.d.ts.map +1 -1
  61. package/dist/lib/configuration-saver.js +14 -71
  62. package/dist/lib/configuration-saver.js.map +1 -1
  63. package/dist/lib/fargate-service.d.ts +11 -21
  64. package/dist/lib/fargate-service.d.ts.map +1 -1
  65. package/dist/lib/fargate-service.js +79 -176
  66. package/dist/lib/fargate-service.js.map +1 -1
  67. package/dist/lib/types/config.d.ts +538 -232
  68. package/dist/lib/types/config.d.ts.map +1 -1
  69. package/dist/lib/types/config.js +133 -3
  70. package/dist/lib/types/config.js.map +1 -1
  71. package/dist/lib/utils/config-loader.d.ts.map +1 -1
  72. package/dist/lib/utils/config-loader.js +3 -2
  73. package/dist/lib/utils/config-loader.js.map +1 -1
  74. package/dist/lib/utils/config-resolver.d.ts +2 -2
  75. package/dist/lib/utils/config-resolver.d.ts.map +1 -1
  76. package/dist/lib/utils/config-resolver.js +14 -7
  77. package/dist/lib/utils/config-resolver.js.map +1 -1
  78. package/dist/lib/utils/config.d.ts +1 -1
  79. package/dist/lib/utils/config.d.ts.map +1 -1
  80. package/dist/lib/utils/config.js +7 -3
  81. package/dist/lib/utils/config.js.map +1 -1
  82. package/dist/lib/utils/sqs.d.ts +13 -0
  83. package/dist/lib/utils/sqs.d.ts.map +1 -0
  84. package/dist/lib/utils/sqs.js +22 -0
  85. package/dist/lib/utils/sqs.js.map +1 -0
  86. package/dist/lib/utils/stack-inference.d.ts.map +1 -1
  87. package/dist/lib/utils/stack-inference.js +8 -6
  88. package/dist/lib/utils/stack-inference.js.map +1 -1
  89. package/dist/lib/xdg-config.d.ts +224 -106
  90. package/dist/lib/xdg-config.d.ts.map +1 -1
  91. package/dist/lib/xdg-config.js +454 -387
  92. package/dist/lib/xdg-config.js.map +1 -1
  93. package/dist/package.json +19 -14
  94. package/dist/scripts/check-logs.d.ts +12 -0
  95. package/dist/scripts/check-logs.d.ts.map +1 -0
  96. package/dist/{bin → scripts}/check-logs.js +65 -15
  97. package/dist/scripts/check-logs.js.map +1 -0
  98. package/dist/scripts/check-webhook-verification.d.ts +3 -0
  99. package/dist/scripts/check-webhook-verification.d.ts.map +1 -0
  100. package/dist/{bin/test-invalid-signature.js → scripts/check-webhook-verification.js} +1 -1
  101. package/dist/scripts/check-webhook-verification.js.map +1 -0
  102. package/dist/scripts/get-dev-version.d.ts +16 -0
  103. package/dist/scripts/get-dev-version.d.ts.map +1 -0
  104. package/dist/scripts/get-dev-version.js +57 -0
  105. package/dist/scripts/get-dev-version.js.map +1 -0
  106. package/dist/scripts/send-event.d.ts.map +1 -0
  107. package/dist/scripts/send-event.js.map +1 -0
  108. package/dist/{bin → scripts}/version.d.ts +3 -1
  109. package/dist/scripts/version.d.ts.map +1 -0
  110. package/dist/{bin → scripts}/version.js +95 -9
  111. package/dist/scripts/version.js.map +1 -0
  112. package/package.json +19 -14
  113. package/dist/bin/check-logs.d.ts +0 -7
  114. package/dist/bin/check-logs.d.ts.map +0 -1
  115. package/dist/bin/check-logs.js.map +0 -1
  116. package/dist/bin/config-profiles.d.ts.map +0 -1
  117. package/dist/bin/config-profiles.js.map +0 -1
  118. package/dist/bin/create-secret.d.ts.map +0 -1
  119. package/dist/bin/create-secret.js.map +0 -1
  120. package/dist/bin/dev-deploy.d.ts +0 -20
  121. package/dist/bin/dev-deploy.d.ts.map +0 -1
  122. package/dist/bin/dev-deploy.js +0 -289
  123. package/dist/bin/dev-deploy.js.map +0 -1
  124. package/dist/bin/get-env.d.ts.map +0 -1
  125. package/dist/bin/get-env.js.map +0 -1
  126. package/dist/bin/publish.d.ts.map +0 -1
  127. package/dist/bin/publish.js.map +0 -1
  128. package/dist/bin/release.d.ts +0 -11
  129. package/dist/bin/release.d.ts.map +0 -1
  130. package/dist/bin/release.js +0 -141
  131. package/dist/bin/release.js.map +0 -1
  132. package/dist/bin/send-event.d.ts.map +0 -1
  133. package/dist/bin/send-event.js.map +0 -1
  134. package/dist/bin/test-invalid-signature.d.ts +0 -3
  135. package/dist/bin/test-invalid-signature.d.ts.map +0 -1
  136. package/dist/bin/test-invalid-signature.js.map +0 -1
  137. package/dist/bin/version.d.ts.map +0 -1
  138. package/dist/bin/version.js.map +0 -1
  139. package/dist/lib/xdg-cli-wrapper.d.ts +0 -113
  140. package/dist/lib/xdg-cli-wrapper.d.ts.map +0 -1
  141. package/dist/lib/xdg-cli-wrapper.js +0 -289
  142. package/dist/lib/xdg-cli-wrapper.js.map +0 -1
  143. package/dist/scripts/config-health-check.d.ts +0 -84
  144. package/dist/scripts/config-health-check.d.ts.map +0 -1
  145. package/dist/scripts/config-health-check.js +0 -659
  146. package/dist/scripts/config-health-check.js.map +0 -1
  147. package/dist/scripts/infer-quilt-config.d.ts.map +0 -1
  148. package/dist/scripts/infer-quilt-config.js.map +0 -1
  149. package/dist/scripts/install-wizard.d.ts +0 -34
  150. package/dist/scripts/install-wizard.d.ts.map +0 -1
  151. package/dist/scripts/install-wizard.js +0 -719
  152. package/dist/scripts/install-wizard.js.map +0 -1
  153. package/dist/scripts/sync-secrets.d.ts.map +0 -1
  154. package/dist/scripts/sync-secrets.js.map +0 -1
  155. /package/dist/bin/{create-secret.d.ts → commands/create-secret.d.ts} +0 -0
  156. /package/dist/bin/{create-secret.js → commands/create-secret.js} +0 -0
  157. /package/dist/bin/{publish.d.ts → commands/publish.d.ts} +0 -0
  158. /package/dist/{bin → scripts}/send-event.d.ts +0 -0
  159. /package/dist/{bin → scripts}/send-event.js +0 -0
@@ -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
- * Provides XDG-compliant configuration file management for the Benchling Webhook system.
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
- * Supports multiple named profiles (e.g., "default", "dev", "prod") for flexible configuration management.
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
- * JSON Schema for configuration validation
28
- * This is a lenient schema that allows additional properties
29
- */
30
- const CONFIG_SCHEMA = {
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
- * Manages XDG-compliant configuration files for the Benchling Webhook system.
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
- * @returns The default base directory path
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
- * Expands home directory in a path
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
- ensureDirectories() {
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 and parses a configuration file with schema validation
107
+ * Reads configuration for a profile
183
108
  *
184
- * @param configType - Type of configuration to read ("user", "derived", or "deploy")
109
+ * @param profile - Profile name (e.g., "default", "dev", "prod")
185
110
  * @returns Parsed configuration object
186
- * @throws {Error} If file not found, invalid JSON, or schema validation fails
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
- readConfig(configType) {
189
- const configPath = this.getConfigPath(configType);
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(`Configuration file not found: ${configPath}`);
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 ajv = new ajv_1.default();
212
- const validate = ajv.compile(CONFIG_SCHEMA);
213
- const valid = validate(config);
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
- * Gets the backup file path for a configuration type
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
- * @param config - Configuration object to validate
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
148
+ * Creates the profile directory if it doesn't exist.
149
+ * Performs atomic write with automatic backup.
247
150
  *
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")
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
- writeConfig(configType, config) {
171
+ writeProfile(profile, config) {
256
172
  // Validate configuration before writing
257
- this.validateConfigSchema(config);
258
- const configPath = this.getConfigPath(configType);
259
- const backupPath = this.getBackupPath(configType);
260
- const configDir = (0, path_1.dirname)(configPath);
261
- // Ensure config directory exists first
262
- if (!(0, fs_1.existsSync)(configDir)) {
263
- (0, fs_1.mkdirSync)(configDir, { recursive: true });
264
- }
265
- // Create backup only if file already exists
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.resolve)((0, os_2.tmpdir)(), `benchling-webhook-config-${Date.now()}.json`);
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 parent directory exists before copy (in case rename failed due to cross-device)
286
- if (!(0, fs_1.existsSync)(configDir)) {
287
- (0, fs_1.mkdirSync)(configDir, { recursive: true });
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
- * Merges multiple configuration sources with priority order
218
+ * Deletes a profile and all its files
299
219
  *
300
- * Merges configurations in priority order (user → derived → deploy),
301
- * where later configurations override earlier ones.
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 configs - Configuration set to merge
305
- * @returns Merged configuration object
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
- * @param profileName - Profile name (defaults to "default")
330
- * @returns Profile directory path
226
+ * @example
227
+ * ```typescript
228
+ * xdg.deleteProfile("dev");
229
+ * ```
331
230
  */
332
- getProfileDir(profileName = "default") {
333
- if (profileName === "default") {
334
- return this.baseDir;
231
+ deleteProfile(profile) {
232
+ if (profile === "default") {
233
+ throw new Error("Cannot delete the default profile");
335
234
  }
336
- return (0, path_1.resolve)(this.baseDir, "profiles", profileName);
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
- (0, fs_1.mkdirSync)(profileDir, { recursive: true });
237
+ throw new Error(`Profile does not exist: ${profile}`);
362
238
  }
363
- // Create config subdirectory
364
- const configDir = (0, path_1.resolve)(profileDir, "config");
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
- // Create deploy subdirectory
369
- const deployDir = (0, path_1.resolve)(profileDir, "deploy");
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
- const profiles = ["default"];
381
- const profilesDir = (0, path_1.resolve)(this.baseDir, "profiles");
382
- if ((0, fs_1.existsSync)(profilesDir)) {
383
- const entries = (0, fs_1.readdirSync)(profilesDir, { withFileTypes: true });
384
- const profileDirs = entries
385
- .filter((entry) => entry.isDirectory())
386
- .map((entry) => entry.name);
387
- profiles.push(...profileDirs);
388
- }
389
- return profiles;
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 profileName - Profile name to check
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(profileName) {
398
- const profileDir = this.getProfileDir(profileName);
399
- return (0, fs_1.existsSync)(profileDir);
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
- * Reads configuration for a specific profile
292
+ * Gets deployment history for a profile
403
293
  *
404
- * @param configType - Type of configuration to read
405
- * @param profileName - Profile name (defaults to "default")
406
- * @returns Parsed configuration object
407
- * @throws {Error} If file not found or validation fails
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
- readProfileConfig(configType, profileName = "default") {
410
- const paths = this.getProfilePaths(profileName);
411
- let configPath;
412
- switch (configType) {
413
- case "user":
414
- configPath = paths.userConfig;
415
- break;
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)(configPath, "utf-8");
316
+ fileContent = (0, fs_1.readFileSync)(deploymentsPath, "utf-8");
433
317
  }
434
318
  catch (error) {
435
- throw new Error(`Failed to read configuration file: ${configPath}. ${error.message}`);
319
+ throw new Error(`Failed to read deployments file: ${deploymentsPath}. ${error.message}`);
436
320
  }
437
- let config;
321
+ let deployments;
438
322
  try {
439
- config = JSON.parse(fileContent);
323
+ deployments = JSON.parse(fileContent);
440
324
  }
441
325
  catch (error) {
442
- throw new Error(`Invalid JSON in configuration file: ${configPath}. ${error.message}`);
326
+ throw new Error(`Invalid JSON in deployments file: ${deploymentsPath}. ${error.message}`);
443
327
  }
444
328
  // Validate schema
445
- this.validateConfigSchema(config);
446
- return config;
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
- * Writes configuration for a specific profile
340
+ * Records a new deployment for a profile
450
341
  *
451
- * @param configType - Type of configuration to write
452
- * @param config - Configuration object to write
453
- * @param profileName - Profile name (defaults to "default")
454
- * @throws {Error} If validation fails or write operation fails
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
- writeProfileConfig(configType, config, profileName = "default") {
457
- // Validate configuration before writing
458
- this.validateConfigSchema(config);
459
- // Ensure profile directories exist
460
- this.ensureProfileDirectories(profileName);
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
- const backupPath = `${configPath}.backup`;
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)(configPath)) {
387
+ if ((0, fs_1.existsSync)(deploymentsPath)) {
479
388
  try {
480
- (0, fs_1.copyFileSync)(configPath, backupPath);
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.resolve)((0, os_2.tmpdir)(), `benchling-webhook-config-${Date.now()}.json`);
488
- const configJson = JSON.stringify(config, null, 4);
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, configJson, "utf-8");
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, configPath);
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
- (0, fs_1.copyFileSync)(tempPath, configPath);
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 configuration file: ${error.message}`);
416
+ throw new Error(`Failed to write deployments file: ${error.message}`);
503
417
  }
504
418
  }
505
419
  /**
506
- * Loads a complete profile with all configuration files
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
- * @param profileName - Profile name (defaults to "default")
509
- * @returns Complete profile configuration
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
- loadProfile(profileName = "default") {
512
- const profile = {
513
- name: profileName,
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
- // Load deploy config if exists
535
- if ((0, fs_1.existsSync)(paths.deployConfig)) {
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
- * Deletes a profile and all its configuration files
447
+ * Reads profile configuration with inheritance support
547
448
  *
548
- * WARNING: This is a destructive operation!
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
- * @param profileName - Profile name to delete
551
- * @throws {Error} If attempting to delete the default profile or if deletion fails
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
- deleteProfile(profileName) {
554
- if (profileName === "default") {
555
- throw new Error("Cannot delete the default profile");
556
- }
557
- const profileDir = this.getProfileDir(profileName);
558
- if (!(0, fs_1.existsSync)(profileDir)) {
559
- throw new Error(`Profile does not exist: ${profileName}`);
560
- }
561
- // For safety, we'll require manual deletion
562
- throw new Error(`Profile deletion must be done manually. Profile directory: ${profileDir}`);
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;