@quiltdata/benchling-webhook 0.6.3-20251104T182406Z → 0.7.2-20251106T003353Z

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