@quiltdata/benchling-webhook 0.7.10 → 0.8.0-20251117T215047Z

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 (79) hide show
  1. package/README.md +1 -0
  2. package/dist/bin/cli.js +8 -5
  3. package/dist/bin/cli.js.map +1 -1
  4. package/dist/bin/commands/deploy.d.ts.map +1 -1
  5. package/dist/bin/commands/deploy.js +64 -8
  6. package/dist/bin/commands/deploy.js.map +1 -1
  7. package/dist/bin/commands/health-check.d.ts.map +1 -1
  8. package/dist/bin/commands/health-check.js +2 -35
  9. package/dist/bin/commands/health-check.js.map +1 -1
  10. package/dist/bin/commands/infer-quilt-config.d.ts +6 -0
  11. package/dist/bin/commands/infer-quilt-config.d.ts.map +1 -1
  12. package/dist/bin/commands/infer-quilt-config.js +50 -2
  13. package/dist/bin/commands/infer-quilt-config.js.map +1 -1
  14. package/dist/bin/commands/install.d.ts.map +1 -1
  15. package/dist/bin/commands/install.js +10 -2
  16. package/dist/bin/commands/install.js.map +1 -1
  17. package/dist/bin/commands/manifest.d.ts.map +1 -1
  18. package/dist/bin/commands/manifest.js +2 -3
  19. package/dist/bin/commands/manifest.js.map +1 -1
  20. package/dist/bin/commands/setup-wizard.d.ts +2 -0
  21. package/dist/bin/commands/setup-wizard.d.ts.map +1 -1
  22. package/dist/bin/commands/setup-wizard.js +3 -1
  23. package/dist/bin/commands/setup-wizard.js.map +1 -1
  24. package/dist/bin/commands/status.d.ts +2 -0
  25. package/dist/bin/commands/status.d.ts.map +1 -1
  26. package/dist/bin/commands/status.js +44 -13
  27. package/dist/bin/commands/status.js.map +1 -1
  28. package/dist/bin/commands/sync-secrets.d.ts.map +1 -1
  29. package/dist/bin/commands/sync-secrets.js +2 -35
  30. package/dist/bin/commands/sync-secrets.js.map +1 -1
  31. package/dist/bin/commands/validate.js +1 -1
  32. package/dist/bin/commands/validate.js.map +1 -1
  33. package/dist/bin/xdg-launch.d.ts +74 -0
  34. package/dist/bin/xdg-launch.d.ts.map +1 -0
  35. package/dist/bin/xdg-launch.js +588 -0
  36. package/dist/bin/xdg-launch.js.map +1 -0
  37. package/dist/lib/benchling-webhook-stack.d.ts.map +1 -1
  38. package/dist/lib/benchling-webhook-stack.js +57 -7
  39. package/dist/lib/benchling-webhook-stack.js.map +1 -1
  40. package/dist/lib/fargate-service.d.ts +24 -4
  41. package/dist/lib/fargate-service.d.ts.map +1 -1
  42. package/dist/lib/fargate-service.js +75 -27
  43. package/dist/lib/fargate-service.js.map +1 -1
  44. package/dist/lib/types/config.d.ts +99 -5
  45. package/dist/lib/types/config.d.ts.map +1 -1
  46. package/dist/lib/types/config.js +4 -1
  47. package/dist/lib/types/config.js.map +1 -1
  48. package/dist/lib/utils/service-resolver.d.ts +155 -0
  49. package/dist/lib/utils/service-resolver.d.ts.map +1 -0
  50. package/dist/lib/utils/service-resolver.js +195 -0
  51. package/dist/lib/utils/service-resolver.js.map +1 -0
  52. package/dist/lib/utils/stack-inference.d.ts +58 -0
  53. package/dist/lib/utils/stack-inference.d.ts.map +1 -1
  54. package/dist/lib/utils/stack-inference.js +76 -2
  55. package/dist/lib/utils/stack-inference.js.map +1 -1
  56. package/dist/lib/wizard/phase2-stack-query.d.ts.map +1 -1
  57. package/dist/lib/wizard/phase2-stack-query.js +45 -8
  58. package/dist/lib/wizard/phase2-stack-query.js.map +1 -1
  59. package/dist/lib/wizard/phase4-validation.d.ts.map +1 -1
  60. package/dist/lib/wizard/phase4-validation.js +3 -4
  61. package/dist/lib/wizard/phase4-validation.js.map +1 -1
  62. package/dist/lib/wizard/phase6-integrated-mode.d.ts.map +1 -1
  63. package/dist/lib/wizard/phase6-integrated-mode.js +19 -0
  64. package/dist/lib/wizard/phase6-integrated-mode.js.map +1 -1
  65. package/dist/lib/wizard/phase7-standalone-mode.d.ts.map +1 -1
  66. package/dist/lib/wizard/phase7-standalone-mode.js +24 -10
  67. package/dist/lib/wizard/phase7-standalone-mode.js.map +1 -1
  68. package/dist/lib/wizard/types.d.ts +14 -0
  69. package/dist/lib/wizard/types.d.ts.map +1 -1
  70. package/dist/package.json +16 -7
  71. package/package.json +16 -7
  72. package/dist/lib/utils/config-loader.d.ts +0 -48
  73. package/dist/lib/utils/config-loader.d.ts.map +0 -1
  74. package/dist/lib/utils/config-loader.js +0 -110
  75. package/dist/lib/utils/config-loader.js.map +0 -1
  76. package/dist/lib/utils/config-resolver.d.ts +0 -138
  77. package/dist/lib/utils/config-resolver.d.ts.map +0 -1
  78. package/dist/lib/utils/config-resolver.js +0 -279
  79. package/dist/lib/utils/config-resolver.js.map +0 -1
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ts-node
2
+ /**
3
+ * XDG Launch: Unified Configuration Bridge
4
+ *
5
+ * Single command to launch Flask application in different modes (native, docker, docker-dev)
6
+ * using profile-based XDG configuration as the single source of truth.
7
+ *
8
+ * Eliminates .env files and manual environment variable management.
9
+ *
10
+ * @module xdg-launch
11
+ * @version 0.8.0
12
+ */
13
+ import { ProfileConfig } from "../lib/types/config";
14
+ /**
15
+ * Launch mode options
16
+ */
17
+ type LaunchMode = "native" | "docker" | "docker-dev";
18
+ /**
19
+ * Launch configuration options
20
+ */
21
+ interface LaunchOptions {
22
+ mode: LaunchMode;
23
+ profile: string;
24
+ port?: number;
25
+ verbose: boolean;
26
+ test: boolean;
27
+ }
28
+ /**
29
+ * Environment variables map
30
+ */
31
+ type EnvVars = Record<string, string>;
32
+ /**
33
+ * Parse command-line arguments
34
+ *
35
+ * @param argv - Command-line arguments
36
+ * @returns Parsed launch options
37
+ */
38
+ declare function parseArguments(argv: string[]): LaunchOptions;
39
+ /**
40
+ * Load XDG profile configuration
41
+ *
42
+ * @param profileName - Profile name
43
+ * @returns Profile configuration
44
+ */
45
+ declare function loadProfile(profileName: string): ProfileConfig;
46
+ /**
47
+ * Build environment variables from profile configuration
48
+ *
49
+ * Maps XDG config fields to service-specific environment variables.
50
+ *
51
+ * @param config - Profile configuration
52
+ * @param mode - Launch mode
53
+ * @param options - Launch options
54
+ * @returns Environment variables map
55
+ */
56
+ declare function buildEnvVars(config: ProfileConfig, mode: LaunchMode, options: LaunchOptions): EnvVars;
57
+ /**
58
+ * Validate required configuration
59
+ *
60
+ * Ensures all required service variables are present and well-formed.
61
+ *
62
+ * @param envVars - Environment variables to validate
63
+ * @param profile - Profile name (for error messages)
64
+ */
65
+ declare function validateConfig(envVars: EnvVars, profile: string): void;
66
+ /**
67
+ * Filter secrets from environment variables for verbose logging
68
+ *
69
+ * @param envVars - Environment variables
70
+ * @returns Filtered environment variables (secrets masked)
71
+ */
72
+ declare function filterSecrets(envVars: EnvVars): EnvVars;
73
+ export { parseArguments, loadProfile, buildEnvVars, validateConfig, filterSecrets };
74
+ //# sourceMappingURL=xdg-launch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xdg-launch.d.ts","sourceRoot":"","sources":["../../bin/xdg-launch.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG;AAMH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;GAEG;AACH,KAAK,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,YAAY,CAAC;AAErD;;GAEG;AACH,UAAU,aAAa;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,KAAK,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEtC;;;;;GAKG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAyCrD;AA2BD;;;;;GAKG;AACH,iBAAS,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,CAmBvD;AAgCD;;;;;;;;;GASG;AACH,iBAAS,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CA+C9F;AAED;;;;;;;GAOG;AACH,iBAAS,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CA2C/D;AAED;;;;;GAKG;AACH,iBAAS,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAWhD;AAgZD,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,CAAC"}
@@ -0,0 +1,588 @@
1
+ #!/usr/bin/env ts-node
2
+ "use strict";
3
+ /**
4
+ * XDG Launch: Unified Configuration Bridge
5
+ *
6
+ * Single command to launch Flask application in different modes (native, docker, docker-dev)
7
+ * using profile-based XDG configuration as the single source of truth.
8
+ *
9
+ * Eliminates .env files and manual environment variable management.
10
+ *
11
+ * @module xdg-launch
12
+ * @version 0.8.0
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.parseArguments = parseArguments;
16
+ exports.loadProfile = loadProfile;
17
+ exports.buildEnvVars = buildEnvVars;
18
+ exports.validateConfig = validateConfig;
19
+ exports.filterSecrets = filterSecrets;
20
+ const child_process_1 = require("child_process");
21
+ const path_1 = require("path");
22
+ const fs_1 = require("fs");
23
+ const xdg_config_1 = require("../lib/xdg-config");
24
+ /**
25
+ * Parse command-line arguments
26
+ *
27
+ * @param argv - Command-line arguments
28
+ * @returns Parsed launch options
29
+ */
30
+ function parseArguments(argv) {
31
+ const args = argv.slice(2);
32
+ const options = {
33
+ profile: "default",
34
+ verbose: false,
35
+ test: false,
36
+ };
37
+ for (let i = 0; i < args.length; i++) {
38
+ const arg = args[i];
39
+ if (arg === "--mode" && i + 1 < args.length) {
40
+ const mode = args[++i];
41
+ if (mode !== "native" && mode !== "docker" && mode !== "docker-dev") {
42
+ throw new Error(`Invalid mode: ${mode}. Must be: native, docker, or docker-dev`);
43
+ }
44
+ options.mode = mode;
45
+ }
46
+ else if (arg === "--profile" && i + 1 < args.length) {
47
+ options.profile = args[++i];
48
+ }
49
+ else if (arg === "--port" && i + 1 < args.length) {
50
+ options.port = parseInt(args[++i], 10);
51
+ if (isNaN(options.port) || options.port < 1 || options.port > 65535) {
52
+ throw new Error(`Invalid port: ${args[i]}. Must be between 1 and 65535`);
53
+ }
54
+ }
55
+ else if (arg === "--verbose") {
56
+ options.verbose = true;
57
+ }
58
+ else if (arg === "--test") {
59
+ options.test = true;
60
+ }
61
+ else if (arg === "--help" || arg === "-h") {
62
+ printUsage();
63
+ process.exit(0);
64
+ }
65
+ else {
66
+ throw new Error(`Unknown option: ${arg}`);
67
+ }
68
+ }
69
+ if (!options.mode) {
70
+ throw new Error("Missing required option: --mode");
71
+ }
72
+ return options;
73
+ }
74
+ /**
75
+ * Print usage information
76
+ */
77
+ function printUsage() {
78
+ console.log(`
79
+ XDG Launch - Unified Configuration Bridge
80
+
81
+ Usage: npx ts-node bin/xdg-launch.ts [OPTIONS]
82
+
83
+ Options:
84
+ --mode <mode> Execution mode: native, docker, docker-dev (required)
85
+ --profile <name> XDG profile name (default: "default")
86
+ --port <number> Override default port
87
+ --verbose Enable verbose logging
88
+ --test Run in test mode
89
+ --help, -h Show this help message
90
+
91
+ Examples:
92
+ npx ts-node bin/xdg-launch.ts --mode native --profile dev
93
+ npx ts-node bin/xdg-launch.ts --mode docker --profile default
94
+ npx ts-node bin/xdg-launch.ts --mode docker-dev --profile dev --verbose
95
+ npx ts-node bin/xdg-launch.ts --mode native --profile dev --test
96
+ `.trim());
97
+ }
98
+ /**
99
+ * Load XDG profile configuration
100
+ *
101
+ * @param profileName - Profile name
102
+ * @returns Profile configuration
103
+ */
104
+ function loadProfile(profileName) {
105
+ const xdg = new xdg_config_1.XDGConfig();
106
+ if (!xdg.profileExists(profileName)) {
107
+ const available = xdg.listProfiles();
108
+ throw new Error(`Profile not found: "${profileName}"\n\n` +
109
+ "Available profiles:\n" +
110
+ (available.length > 0
111
+ ? available.map((p) => ` - ${p}`).join("\n")
112
+ : " (none)") +
113
+ "\n\n" +
114
+ "Create a new profile:\n" +
115
+ ` npm run setup -- --profile ${profileName}`);
116
+ }
117
+ return xdg.readProfile(profileName);
118
+ }
119
+ /**
120
+ * Extract secret name from Secrets Manager ARN
121
+ *
122
+ * AWS Secrets Manager automatically appends a 6-character random suffix to secret names
123
+ * in ARNs (e.g., "my-secret-Ab12Cd"). This function extracts the base secret name by
124
+ * removing the suffix.
125
+ *
126
+ * @param arn - Secrets Manager ARN
127
+ * @returns Secret name without the random suffix
128
+ */
129
+ function extractSecretName(arn) {
130
+ if (!arn) {
131
+ return "";
132
+ }
133
+ // ARN format: arn:aws:secretsmanager:region:account:secret:name-XXXXXX
134
+ // where XXXXXX is a 6-character random suffix added by AWS
135
+ const match = arn.match(/secret:([^:]+)/);
136
+ if (!match) {
137
+ return arn;
138
+ }
139
+ const fullName = match[1];
140
+ // Remove the AWS-generated 6-character suffix (format: -XXXXXX)
141
+ // The suffix is always a hyphen followed by 6 alphanumeric characters
142
+ const withoutSuffix = fullName.replace(/-[A-Za-z0-9]{6}$/, "");
143
+ return withoutSuffix;
144
+ }
145
+ /**
146
+ * Build environment variables from profile configuration
147
+ *
148
+ * Maps XDG config fields to service-specific environment variables.
149
+ *
150
+ * @param config - Profile configuration
151
+ * @param mode - Launch mode
152
+ * @param options - Launch options
153
+ * @returns Environment variables map
154
+ */
155
+ function buildEnvVars(config, mode, options) {
156
+ const envVars = {
157
+ // Preserve existing process.env
158
+ ...process.env,
159
+ // Quilt Services (v0.8.0+ service-specific - NO MORE STACK ARN!)
160
+ QUILT_WEB_HOST: config.quilt.catalog,
161
+ ATHENA_USER_DATABASE: config.quilt.database,
162
+ ATHENA_USER_WORKGROUP: config.quilt.athenaUserWorkgroup || "primary",
163
+ ATHENA_RESULTS_BUCKET: config.quilt.athenaResultsBucket || "",
164
+ ICEBERG_DATABASE: config.quilt.icebergDatabase || "",
165
+ ICEBERG_WORKGROUP: config.quilt.icebergWorkgroup || "",
166
+ PACKAGER_SQS_URL: config.quilt.queueUrl,
167
+ // AWS Configuration
168
+ AWS_REGION: config.quilt.region || config.deployment.region,
169
+ AWS_DEFAULT_REGION: config.quilt.region || config.deployment.region,
170
+ // Benchling Configuration (credentials from Secrets Manager, NOT environment)
171
+ BenchlingSecret: extractSecretName(config.benchling.secretArn || ""),
172
+ // Security Configuration
173
+ ENABLE_WEBHOOK_VERIFICATION: String(config.security?.enableVerification !== false),
174
+ };
175
+ // Mode-specific variables
176
+ if (mode === "native" || mode === "docker-dev") {
177
+ envVars.FLASK_ENV = "development";
178
+ envVars.FLASK_DEBUG = "true";
179
+ envVars.LOG_LEVEL = config.logging?.level || "DEBUG";
180
+ // Disable verification in dev mode for easier testing
181
+ if (mode === "docker-dev") {
182
+ envVars.ENABLE_WEBHOOK_VERIFICATION = "false";
183
+ }
184
+ }
185
+ else {
186
+ // docker (production)
187
+ envVars.FLASK_ENV = "production";
188
+ envVars.LOG_LEVEL = config.logging?.level || "INFO";
189
+ }
190
+ // Test mode flag
191
+ if (options.test) {
192
+ envVars.BENCHLING_TEST_MODE = "true";
193
+ }
194
+ return envVars;
195
+ }
196
+ /**
197
+ * Validate required configuration
198
+ *
199
+ * Ensures all required service variables are present and well-formed.
200
+ *
201
+ * @param envVars - Environment variables to validate
202
+ * @param profile - Profile name (for error messages)
203
+ */
204
+ function validateConfig(envVars, profile) {
205
+ // Required service variables (NO BENCHLING_TENANT - comes from Secrets Manager!)
206
+ const required = [
207
+ "QUILT_WEB_HOST",
208
+ "ATHENA_USER_DATABASE",
209
+ "PACKAGER_SQS_URL",
210
+ "AWS_REGION",
211
+ "BenchlingSecret",
212
+ ];
213
+ const missing = required.filter((key) => !envVars[key]);
214
+ if (missing.length > 0) {
215
+ throw new Error("Missing required configuration:\n" +
216
+ missing.map((key) => ` - ${key}`).join("\n") +
217
+ "\n\n" +
218
+ "Check profile configuration at:\n" +
219
+ ` ~/.config/benchling-webhook/${profile}/config.json\n\n` +
220
+ "Run setup wizard to configure:\n" +
221
+ ` npm run setup -- --profile ${profile}`);
222
+ }
223
+ // Format validation
224
+ if (!envVars.PACKAGER_SQS_URL.match(/^https:\/\/sqs\.[a-z0-9-]+\.amazonaws\.com\/\d+\/.+/)) {
225
+ throw new Error(`Invalid SQS URL format: ${envVars.PACKAGER_SQS_URL}\n\n` +
226
+ "Expected format:\n" +
227
+ " https://sqs.{region}.amazonaws.com/{account}/{queue-name}\n\n" +
228
+ "Example:\n" +
229
+ " https://sqs.us-east-1.amazonaws.com/123456789012/packager-queue");
230
+ }
231
+ // Validate BenchlingSecret is present (secret name, not ARN)
232
+ if (!envVars.BenchlingSecret) {
233
+ throw new Error("Missing BenchlingSecret\n\n" +
234
+ "BenchlingSecret must be the name of your AWS Secrets Manager secret.\n" +
235
+ "Example: benchling-webhook-prod");
236
+ }
237
+ }
238
+ /**
239
+ * Filter secrets from environment variables for verbose logging
240
+ *
241
+ * @param envVars - Environment variables
242
+ * @returns Filtered environment variables (secrets masked)
243
+ */
244
+ function filterSecrets(envVars) {
245
+ const filtered = {};
246
+ for (const [key, value] of Object.entries(envVars)) {
247
+ const upperKey = key.toUpperCase();
248
+ if (upperKey.includes("SECRET") || upperKey.includes("PASSWORD") || upperKey.includes("TOKEN")) {
249
+ filtered[key] = "***REDACTED***";
250
+ }
251
+ else {
252
+ filtered[key] = value;
253
+ }
254
+ }
255
+ return filtered;
256
+ }
257
+ /**
258
+ * Spawn docker-compose command with consistent environment
259
+ *
260
+ * @param args - docker-compose arguments
261
+ * @param dockerDir - Docker working directory
262
+ * @param envVars - Environment variables
263
+ * @param stdio - stdio option for spawn
264
+ * @returns Child process
265
+ */
266
+ function spawnDockerCompose(args, dockerDir, envVars, stdio = "inherit") {
267
+ return (0, child_process_1.spawn)("docker-compose", args, {
268
+ cwd: dockerDir,
269
+ env: envVars,
270
+ stdio,
271
+ });
272
+ }
273
+ /**
274
+ * Wait for server to be healthy
275
+ *
276
+ * @param url - Server URL to check
277
+ * @param maxAttempts - Maximum number of attempts
278
+ * @param delayMs - Delay between attempts in milliseconds
279
+ * @returns Promise that resolves when server is healthy
280
+ */
281
+ async function waitForHealth(url, maxAttempts = 30, delayMs = 1000) {
282
+ console.log(`\nā³ Waiting for server to be healthy at ${url}...`);
283
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
284
+ try {
285
+ const response = await fetch(`${url}/health`, {
286
+ signal: AbortSignal.timeout(5000),
287
+ });
288
+ if (response.ok) {
289
+ console.log(`āœ… Server is healthy (attempt ${attempt}/${maxAttempts})\n`);
290
+ return;
291
+ }
292
+ }
293
+ catch {
294
+ // Ignore errors and retry
295
+ }
296
+ if (attempt < maxAttempts) {
297
+ await new Promise(resolve => setTimeout(resolve, delayMs));
298
+ }
299
+ }
300
+ throw new Error(`Server did not become healthy after ${maxAttempts} attempts`);
301
+ }
302
+ /**
303
+ * Run tests against the server
304
+ *
305
+ * @param url - Server URL to test
306
+ * @param profile - Profile name
307
+ * @returns Promise that resolves with test exit code
308
+ */
309
+ async function runTests(url, profile) {
310
+ console.log("🧪 Running tests...\n");
311
+ const dockerDir = (0, path_1.resolve)(__dirname, "..", "docker");
312
+ const testScript = (0, path_1.resolve)(dockerDir, "scripts", "test_webhook.py");
313
+ if (!(0, fs_1.existsSync)(testScript)) {
314
+ throw new Error(`Test script not found: ${testScript}`);
315
+ }
316
+ return new Promise((resolve) => {
317
+ const proc = (0, child_process_1.spawn)("uv", ["run", "python", testScript, url, "--profile", profile], {
318
+ cwd: dockerDir,
319
+ stdio: "inherit",
320
+ });
321
+ proc.on("exit", (code) => {
322
+ resolve(code || 0);
323
+ });
324
+ proc.on("error", (error) => {
325
+ console.error(`\nāŒ Failed to run tests: ${error.message}`);
326
+ resolve(1);
327
+ });
328
+ });
329
+ }
330
+ /**
331
+ * Launch Flask application in native mode
332
+ *
333
+ * Runs Flask directly on host using uv.
334
+ *
335
+ * @param envVars - Environment variables
336
+ * @param options - Launch options
337
+ */
338
+ async function launchNative(envVars, options) {
339
+ const port = options.port || 5001;
340
+ envVars.PORT = String(port);
341
+ console.log(`šŸš€ Launching native Flask (port ${port})...`);
342
+ if (options.verbose) {
343
+ console.log("\nEnvironment Variables:");
344
+ const filtered = filterSecrets(envVars);
345
+ Object.entries(filtered)
346
+ .sort(([a], [b]) => a.localeCompare(b))
347
+ .forEach(([key, value]) => console.log(` ${key}=${value}`));
348
+ console.log();
349
+ }
350
+ // Check if uv is installed
351
+ const dockerDir = (0, path_1.resolve)(__dirname, "..", "docker");
352
+ if (!(0, fs_1.existsSync)(dockerDir)) {
353
+ throw new Error(`Docker directory not found: ${dockerDir}`);
354
+ }
355
+ console.log(`Working directory: ${dockerDir}`);
356
+ console.log("Command: uv run python -m src.app\n");
357
+ const proc = (0, child_process_1.spawn)("uv", ["run", "python", "-m", "src.app"], {
358
+ cwd: dockerDir,
359
+ env: envVars,
360
+ stdio: options.test ? "pipe" : "inherit",
361
+ });
362
+ // If in test mode, run tests after server is healthy
363
+ if (options.test) {
364
+ try {
365
+ const serverUrl = `http://localhost:${port}`;
366
+ await waitForHealth(serverUrl);
367
+ const exitCode = await runTests(serverUrl, options.profile);
368
+ console.log("\nšŸ›‘ Shutting down server...");
369
+ proc.kill("SIGTERM");
370
+ // Wait a moment for cleanup
371
+ await new Promise(resolve => setTimeout(resolve, 1000));
372
+ process.exit(exitCode);
373
+ }
374
+ catch (error) {
375
+ console.error(`\nāŒ Test failed: ${error.message}`);
376
+ proc.kill("SIGTERM");
377
+ process.exit(1);
378
+ }
379
+ }
380
+ else {
381
+ // Non-test mode: run server interactively with graceful shutdown
382
+ const cleanup = () => {
383
+ console.log("\n\nšŸ›‘ Shutting down...");
384
+ proc.kill("SIGTERM");
385
+ };
386
+ process.on("SIGINT", cleanup);
387
+ process.on("SIGTERM", cleanup);
388
+ proc.on("error", (error) => {
389
+ console.error(`\nāŒ Failed to start native Flask: ${error.message}`);
390
+ if (error.code === "ENOENT") {
391
+ console.error("\nIs \"uv\" installed and in your PATH?");
392
+ console.error("Install uv: https://docs.astral.sh/uv/getting-started/installation/");
393
+ }
394
+ process.exit(1);
395
+ });
396
+ proc.on("exit", (code) => {
397
+ if (code !== 0 && code !== null) {
398
+ console.error(`\nāŒ Native Flask exited with code ${code}`);
399
+ }
400
+ process.exit(code || 0);
401
+ });
402
+ }
403
+ }
404
+ /**
405
+ * Launch Flask application in Docker production mode
406
+ *
407
+ * Runs production Docker container via docker-compose.
408
+ *
409
+ * @param envVars - Environment variables
410
+ * @param options - Launch options
411
+ */
412
+ async function launchDocker(envVars, options) {
413
+ const port = options.port || 5003;
414
+ envVars.PORT = String(port);
415
+ console.log(`🐳 Launching Docker production (port ${port})...`);
416
+ if (options.verbose) {
417
+ console.log("\nEnvironment Variables:");
418
+ const filtered = filterSecrets(envVars);
419
+ Object.entries(filtered)
420
+ .sort(([a], [b]) => a.localeCompare(b))
421
+ .forEach(([key, value]) => console.log(` ${key}=${value}`));
422
+ console.log();
423
+ }
424
+ const dockerDir = (0, path_1.resolve)(__dirname, "..", "docker");
425
+ if (!(0, fs_1.existsSync)(dockerDir)) {
426
+ throw new Error(`Docker directory not found: ${dockerDir}`);
427
+ }
428
+ console.log(`Working directory: ${dockerDir}`);
429
+ console.log("Command: docker-compose up app\n");
430
+ const proc = spawnDockerCompose(["up", "app"], dockerDir, envVars, options.test ? "pipe" : "inherit");
431
+ // If in test mode, run tests after server is healthy
432
+ if (options.test) {
433
+ const dockerCleanup = () => {
434
+ spawnDockerCompose(["down"], dockerDir, envVars);
435
+ };
436
+ try {
437
+ const serverUrl = `http://localhost:${port}`;
438
+ await waitForHealth(serverUrl);
439
+ const exitCode = await runTests(serverUrl, options.profile);
440
+ console.log("\nšŸ›‘ Shutting down Docker container...");
441
+ dockerCleanup();
442
+ // Wait a moment for cleanup
443
+ await new Promise(resolve => setTimeout(resolve, 2000));
444
+ process.exit(exitCode);
445
+ }
446
+ catch (error) {
447
+ console.error(`\nāŒ Test failed: ${error.message}`);
448
+ dockerCleanup();
449
+ process.exit(1);
450
+ }
451
+ }
452
+ else {
453
+ // Non-test mode: run server interactively with graceful shutdown
454
+ const cleanup = () => {
455
+ console.log("\n\nšŸ›‘ Shutting down Docker container...");
456
+ spawnDockerCompose(["down"], dockerDir, envVars);
457
+ proc.kill("SIGTERM");
458
+ };
459
+ process.on("SIGINT", cleanup);
460
+ process.on("SIGTERM", cleanup);
461
+ proc.on("error", (error) => {
462
+ console.error(`\nāŒ Failed to start Docker: ${error.message}`);
463
+ if (error.code === "ENOENT") {
464
+ console.error("\nIs \"docker-compose\" installed and in your PATH?");
465
+ }
466
+ process.exit(1);
467
+ });
468
+ proc.on("exit", (code) => {
469
+ if (code !== 0 && code !== null) {
470
+ console.error(`\nāŒ Docker exited with code ${code}`);
471
+ }
472
+ process.exit(code || 0);
473
+ });
474
+ }
475
+ }
476
+ /**
477
+ * Launch Flask application in Docker development mode
478
+ *
479
+ * Runs Docker container with hot-reload enabled via docker-compose --profile dev.
480
+ *
481
+ * @param envVars - Environment variables
482
+ * @param options - Launch options
483
+ */
484
+ async function launchDockerDev(envVars, options) {
485
+ const port = options.port || 5002;
486
+ envVars.PORT = String(port);
487
+ console.log(`🐳 Launching Docker development (port ${port}, hot-reload enabled)...`);
488
+ if (options.verbose) {
489
+ console.log("\nEnvironment Variables:");
490
+ const filtered = filterSecrets(envVars);
491
+ Object.entries(filtered)
492
+ .sort(([a], [b]) => a.localeCompare(b))
493
+ .forEach(([key, value]) => console.log(` ${key}=${value}`));
494
+ console.log();
495
+ }
496
+ const dockerDir = (0, path_1.resolve)(__dirname, "..", "docker");
497
+ if (!(0, fs_1.existsSync)(dockerDir)) {
498
+ throw new Error(`Docker directory not found: ${dockerDir}`);
499
+ }
500
+ console.log(`Working directory: ${dockerDir}`);
501
+ console.log("Command: docker-compose --profile dev up app-dev\n");
502
+ const proc = spawnDockerCompose(["--profile", "dev", "up", "app-dev"], dockerDir, envVars, options.test ? "pipe" : "inherit");
503
+ // If in test mode, run tests after server is healthy
504
+ if (options.test) {
505
+ const dockerCleanup = () => {
506
+ spawnDockerCompose(["--profile", "dev", "down"], dockerDir, envVars);
507
+ };
508
+ try {
509
+ const serverUrl = `http://localhost:${port}`;
510
+ await waitForHealth(serverUrl);
511
+ const exitCode = await runTests(serverUrl, options.profile);
512
+ console.log("\nšŸ›‘ Shutting down Docker dev container...");
513
+ dockerCleanup();
514
+ // Wait a moment for cleanup
515
+ await new Promise(resolve => setTimeout(resolve, 2000));
516
+ process.exit(exitCode);
517
+ }
518
+ catch (error) {
519
+ console.error(`\nāŒ Test failed: ${error.message}`);
520
+ dockerCleanup();
521
+ process.exit(1);
522
+ }
523
+ }
524
+ else {
525
+ // Non-test mode: run server interactively with graceful shutdown
526
+ const cleanup = () => {
527
+ console.log("\n\nšŸ›‘ Shutting down Docker dev container...");
528
+ spawnDockerCompose(["--profile", "dev", "down"], dockerDir, envVars);
529
+ proc.kill("SIGTERM");
530
+ };
531
+ process.on("SIGINT", cleanup);
532
+ process.on("SIGTERM", cleanup);
533
+ proc.on("error", (error) => {
534
+ console.error(`\nāŒ Failed to start Docker dev: ${error.message}`);
535
+ if (error.code === "ENOENT") {
536
+ console.error("\nIs \"docker-compose\" installed and in your PATH?");
537
+ }
538
+ process.exit(1);
539
+ });
540
+ proc.on("exit", (code) => {
541
+ if (code !== 0 && code !== null) {
542
+ console.error(`\nāŒ Docker dev exited with code ${code}`);
543
+ }
544
+ process.exit(code || 0);
545
+ });
546
+ }
547
+ }
548
+ /**
549
+ * Main entry point
550
+ */
551
+ async function main() {
552
+ try {
553
+ // Parse command-line arguments
554
+ const options = parseArguments(process.argv);
555
+ // Load XDG profile configuration
556
+ const config = loadProfile(options.profile);
557
+ // Build environment variables
558
+ const envVars = buildEnvVars(config, options.mode, options);
559
+ // Validate configuration
560
+ validateConfig(envVars, options.profile);
561
+ // Launch appropriate mode
562
+ switch (options.mode) {
563
+ case "native":
564
+ await launchNative(envVars, options);
565
+ break;
566
+ case "docker":
567
+ await launchDocker(envVars, options);
568
+ break;
569
+ case "docker-dev":
570
+ await launchDockerDev(envVars, options);
571
+ break;
572
+ default:
573
+ throw new Error(`Unknown mode: ${options.mode}`);
574
+ }
575
+ }
576
+ catch (error) {
577
+ console.error(`\nāŒ Error: ${error.message}\n`);
578
+ process.exit(1);
579
+ }
580
+ }
581
+ // Run if called directly
582
+ if (require.main === module) {
583
+ main().catch((error) => {
584
+ console.error(`\nāŒ Fatal error: ${error.message}\n`);
585
+ process.exit(1);
586
+ });
587
+ }
588
+ //# sourceMappingURL=xdg-launch.js.map