env-secrets 0.1.10 → 0.3.0

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 (61) hide show
  1. package/.devcontainer/devcontainer.json +33 -0
  2. package/.dockerignore +9 -0
  3. package/.eslintignore +4 -2
  4. package/.github/dependabot.yml +4 -0
  5. package/.github/workflows/build-main.yml +6 -2
  6. package/.github/workflows/deploy-docs.yml +50 -0
  7. package/.github/workflows/e2e-tests.yaml +54 -0
  8. package/.github/workflows/lint.yaml +6 -2
  9. package/.github/workflows/release.yml +13 -3
  10. package/.github/workflows/snyk.yaml +5 -1
  11. package/.github/workflows/unittests.yaml +18 -6
  12. package/.lintstagedrc +2 -7
  13. package/.prettierignore +6 -0
  14. package/AGENTS.md +149 -0
  15. package/Dockerfile +14 -0
  16. package/README.md +507 -36
  17. package/__e2e__/README.md +160 -0
  18. package/__e2e__/index.test.ts +339 -0
  19. package/__e2e__/setup.ts +58 -0
  20. package/__e2e__/utils/debug-logger.ts +45 -0
  21. package/__e2e__/utils/test-utils.ts +645 -0
  22. package/__tests__/index.test.ts +573 -31
  23. package/__tests__/vaults/secretsmanager.test.ts +460 -0
  24. package/__tests__/vaults/utils.test.ts +183 -0
  25. package/__tests__/version.test.ts +8 -0
  26. package/dist/index.js +36 -10
  27. package/dist/vaults/secretsmanager.js +44 -43
  28. package/dist/vaults/utils.js +2 -2
  29. package/docker-compose.yaml +29 -0
  30. package/docs/AWS.md +257 -0
  31. package/jest.config.js +4 -1
  32. package/jest.e2e.config.js +8 -0
  33. package/package.json +18 -10
  34. package/src/index.ts +44 -10
  35. package/src/vaults/secretsmanager.ts +48 -48
  36. package/src/vaults/utils.ts +6 -4
  37. package/website/docs/advanced-usage.mdx +399 -0
  38. package/website/docs/best-practices.mdx +416 -0
  39. package/website/docs/cli-reference.mdx +204 -0
  40. package/website/docs/examples.mdx +960 -0
  41. package/website/docs/faq.mdx +302 -0
  42. package/website/docs/index.mdx +56 -0
  43. package/website/docs/installation.mdx +30 -0
  44. package/website/docs/overview.mdx +17 -0
  45. package/website/docs/production-deployment.mdx +622 -0
  46. package/website/docs/providers/aws-secrets-manager.mdx +28 -0
  47. package/website/docs/security.mdx +122 -0
  48. package/website/docs/troubleshooting.mdx +236 -0
  49. package/website/docs/tutorials/local-dev/devcontainer-localstack.mdx +31 -0
  50. package/website/docs/tutorials/local-dev/docker-compose.mdx +22 -0
  51. package/website/docs/tutorials/local-dev/nextjs.mdx +18 -0
  52. package/website/docs/tutorials/local-dev/node-python-go.mdx +39 -0
  53. package/website/docs/tutorials/local-dev/quickstart.mdx +23 -0
  54. package/website/docusaurus.config.ts +89 -0
  55. package/website/package.json +21 -0
  56. package/website/sidebars.ts +33 -0
  57. package/website/src/css/custom.css +1 -0
  58. package/website/static/img/env-secrets.png +0 -0
  59. package/website/static/img/favicon.ico +0 -0
  60. package/website/static/img/logo.svg +4 -0
  61. package/website/yarn.lock +8764 -0
@@ -0,0 +1,645 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import * as os from 'os';
6
+ import * as crypto from 'crypto';
7
+ import { debugLog, debugError, debugWarn } from './debug-logger';
8
+
9
+ const execAsync = promisify(exec);
10
+
11
+ // Generate a unique character string for this test run
12
+ export function generateUniqueRunId(): string {
13
+ return crypto.randomBytes(8).toString('hex');
14
+ }
15
+
16
+ // Enhanced error class for awslocal command failures
17
+ export class AwslocalCommandError extends Error {
18
+ public readonly command: string;
19
+ public readonly exitCode: number;
20
+ public readonly stdout: string;
21
+ public readonly stderr: string;
22
+ public readonly environment: Record<string, string>;
23
+
24
+ constructor(
25
+ command: string,
26
+ exitCode: number,
27
+ stdout: string,
28
+ stderr: string,
29
+ environment: Record<string, string>,
30
+ originalError?: Error
31
+ ) {
32
+ const message =
33
+ `awslocal command failed with exit code ${exitCode}\n` +
34
+ `Command: ${command}\n` +
35
+ `Exit Code: ${exitCode}\n` +
36
+ `Stdout: ${stdout || '(empty)'}\n` +
37
+ `Stderr: ${stderr || '(empty)'}\n` +
38
+ `Environment: ${JSON.stringify(environment, null, 2)}`;
39
+
40
+ super(message);
41
+ this.name = 'AwslocalCommandError';
42
+ this.command = command;
43
+ this.exitCode = exitCode;
44
+ this.stdout = stdout;
45
+ this.stderr = stderr;
46
+ this.environment = environment;
47
+
48
+ // Preserve original error stack if available
49
+ if (originalError && originalError.stack) {
50
+ this.stack = originalError.stack;
51
+ }
52
+ }
53
+ }
54
+
55
+ // Enhanced execAsync function with better error handling
56
+ export async function execAwslocalCommand(
57
+ command: string,
58
+ environment: Record<string, string> = {},
59
+ options: { timeout?: number; cwd?: string } = {}
60
+ ): Promise<{ stdout: string; stderr: string }> {
61
+ const env = { ...process.env, ...environment };
62
+ const execOptions = {
63
+ env,
64
+ timeout: options.timeout || 30000, // 30 second default timeout
65
+ cwd: options.cwd || process.cwd(),
66
+ maxBuffer: 1024 * 1024 * 10 // 10MB buffer
67
+ };
68
+
69
+ debugLog(`Executing awslocal command: ${command}`);
70
+ debugLog(`Environment: ${JSON.stringify(environment, null, 2)}`);
71
+
72
+ try {
73
+ const result = await execAsync(command, execOptions);
74
+ debugLog(`Command succeeded: ${command}`);
75
+ if (result.stdout) {
76
+ debugLog(`Stdout: ${result.stdout}`);
77
+ }
78
+ return result;
79
+ } catch (error) {
80
+ const execError = error as {
81
+ code?: number;
82
+ stdout?: string;
83
+ stderr?: string;
84
+ };
85
+ const exitCode = execError.code || -1;
86
+ const stdout = execError.stdout || '';
87
+ const stderr = execError.stderr || '';
88
+
89
+ debugError(`Command failed: ${command}`);
90
+ debugError(`Exit code: ${exitCode}`);
91
+ debugError(`Stdout: ${stdout}`);
92
+ debugError(`Stderr: ${stderr}`);
93
+
94
+ throw new AwslocalCommandError(
95
+ command,
96
+ exitCode,
97
+ stdout,
98
+ stderr,
99
+ environment,
100
+ execError as Error
101
+ );
102
+ }
103
+ }
104
+
105
+ export interface CliResult {
106
+ code: number;
107
+ error: Error | null;
108
+ stdout: string;
109
+ stderr: string;
110
+ }
111
+
112
+ export interface TestSecret {
113
+ name: string;
114
+ value: string;
115
+ description?: string;
116
+ }
117
+
118
+ export interface CreatedSecret {
119
+ originalName: string;
120
+ prefixedName: string;
121
+ value: string;
122
+ description?: string;
123
+ }
124
+
125
+ export class LocalStackHelper {
126
+ private readonly endpoint: string;
127
+ private readonly region: string;
128
+ private readonly accessKey: string;
129
+ private readonly secretKey: string;
130
+ private readonly runId: string;
131
+
132
+ constructor() {
133
+ this.endpoint = process.env.LOCALSTACK_URL || 'http://localhost:4566';
134
+ this.region = process.env.AWS_DEFAULT_REGION || 'us-east-1';
135
+ this.accessKey = process.env.AWS_ACCESS_KEY_ID || 'test';
136
+ this.secretKey = process.env.AWS_SECRET_ACCESS_KEY || 'test';
137
+ this.runId = generateUniqueRunId();
138
+ }
139
+
140
+ private getEnvironment(): Record<string, string> {
141
+ return {
142
+ AWS_ENDPOINT_URL: this.endpoint,
143
+ AWS_ACCESS_KEY_ID: this.accessKey,
144
+ AWS_SECRET_ACCESS_KEY: this.secretKey,
145
+ AWS_DEFAULT_REGION: this.region,
146
+ AWS_REGION: this.region
147
+ };
148
+ }
149
+
150
+ async createSecret(
151
+ secret: TestSecret,
152
+ region?: string
153
+ ): Promise<CreatedSecret> {
154
+ // Try to parse as JSON, if it fails, treat as plain string
155
+ let secretValue: string;
156
+ try {
157
+ JSON.parse(secret.value);
158
+ secretValue = secret.value; // Already valid JSON
159
+ } catch {
160
+ secretValue = JSON.stringify(secret.value); // Wrap plain string in quotes
161
+ }
162
+
163
+ // Prepend unique run ID to secret name
164
+ const prefixedSecretName = `${this.runId}-${secret.name}`;
165
+
166
+ const secretRegion = region || this.region;
167
+ const command = `awslocal secretsmanager create-secret --name "${prefixedSecretName}" --secret-string '${secretValue}' --description "${
168
+ secret.description || 'Test secret'
169
+ }" --region ${secretRegion}`;
170
+
171
+ try {
172
+ await execAwslocalCommand(command, this.getEnvironment());
173
+ debugLog(`Secret created successfully: ${prefixedSecretName}`);
174
+
175
+ return {
176
+ originalName: secret.name,
177
+ prefixedName: prefixedSecretName,
178
+ value: secret.value,
179
+ description: secret.description
180
+ };
181
+ } catch (error) {
182
+ if (error instanceof AwslocalCommandError) {
183
+ debugError(`Failed to create secret ${secret.name}:`);
184
+ debugError(`Command: ${error.command}`);
185
+ debugError(`Exit Code: ${error.exitCode}`);
186
+ debugError(`Stdout: ${error.stdout}`);
187
+ debugError(`Stderr: ${error.stderr}`);
188
+ debugError(
189
+ `Environment: ${JSON.stringify(error.environment, null, 2)}`
190
+ );
191
+
192
+ // Provide helpful debugging information
193
+ if (error.stderr.includes('already exists')) {
194
+ debugLog(
195
+ `Hint: Secret '${prefixedSecretName}' may already exist. Try deleting it first or use a different name.`
196
+ );
197
+ } else if (
198
+ error.stderr.includes('connection') ||
199
+ error.stderr.includes('timeout')
200
+ ) {
201
+ debugLog(`Hint: Check if LocalStack is running at ${this.endpoint}`);
202
+ debugLog(`Hint: Try running: awslocal sts get-caller-identity`);
203
+ } else if (
204
+ error.stderr.includes('not found') ||
205
+ error.stderr.includes('command not found')
206
+ ) {
207
+ debugLog(
208
+ `Hint: Make sure awslocal is installed: pip install awscli-local`
209
+ );
210
+ }
211
+ } else {
212
+ debugLog(
213
+ `Unexpected error creating secret ${prefixedSecretName}:`,
214
+ error
215
+ );
216
+ }
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ async deleteSecret(secretName: string): Promise<void> {
222
+ // Prepend unique run ID to secret name if not already prefixed
223
+ const prefixedSecretName = secretName.startsWith(`${this.runId}-`)
224
+ ? secretName
225
+ : `${this.runId}-${secretName}`;
226
+
227
+ const command = `awslocal secretsmanager delete-secret --secret-id "${prefixedSecretName}" --force-delete-without-recovery --region ${this.region}`;
228
+
229
+ try {
230
+ await execAwslocalCommand(command, this.getEnvironment());
231
+ debugLog(`Secret deleted successfully: ${prefixedSecretName}`);
232
+ } catch (error) {
233
+ if (error instanceof AwslocalCommandError) {
234
+ // For cleanup operations, we log warnings but don't throw unless it's critical
235
+ debugWarn(`Failed to delete secret ${prefixedSecretName}:`);
236
+ debugWarn(`Command: ${error.command}`);
237
+ debugWarn(`Exit Code: ${error.exitCode}`);
238
+ debugWarn(`Stdout: ${error.stdout}`);
239
+ debugWarn(`Stderr: ${error.stderr}`);
240
+
241
+ // Only throw for critical errors (not "not found" errors)
242
+ if (
243
+ !error.stderr.includes('not found') &&
244
+ !error.stderr.includes('does not exist')
245
+ ) {
246
+ debugLog(`Critical error during cleanup - rethrowing`);
247
+ throw error;
248
+ }
249
+ } else {
250
+ debugLog(
251
+ `Unexpected error deleting secret ${prefixedSecretName}:`,
252
+ error
253
+ );
254
+ }
255
+ }
256
+ }
257
+
258
+ async cleanupRunSecrets(): Promise<void> {
259
+ debugLog(`Cleaning up secrets with prefix: ${this.runId}`);
260
+
261
+ try {
262
+ const allSecrets = await this.listSecrets();
263
+ const runSecrets = allSecrets.filter((secretName) =>
264
+ secretName.startsWith(`${this.runId}-`)
265
+ );
266
+
267
+ debugLog(
268
+ `Found ${runSecrets.length} secrets to cleanup for run ${this.runId}`
269
+ );
270
+
271
+ // Delete all secrets with the run prefix
272
+ for (const secretName of runSecrets) {
273
+ try {
274
+ await this.deleteSecret(secretName);
275
+ } catch (error) {
276
+ debugWarn(`Failed to cleanup secret ${secretName}:`, error);
277
+ // Continue with other secrets even if one fails
278
+ }
279
+ }
280
+
281
+ debugLog(`Cleanup completed for run ${this.runId}`);
282
+ } catch (error) {
283
+ debugWarn(`Failed to cleanup secrets for run ${this.runId}:`, error);
284
+ // Don't throw - cleanup failures shouldn't break tests
285
+ }
286
+ }
287
+
288
+ async listSecrets(): Promise<string[]> {
289
+ const command = `awslocal secretsmanager list-secrets --region ${this.region}`;
290
+
291
+ try {
292
+ const result = await execAwslocalCommand(command, this.getEnvironment());
293
+ const parsedResult = JSON.parse(result.stdout);
294
+ return (
295
+ parsedResult.SecretList?.map(
296
+ (secret: { Name: string }) => secret.Name
297
+ ) || []
298
+ );
299
+ } catch (error) {
300
+ if (error instanceof AwslocalCommandError) {
301
+ debugError('Failed to list secrets:');
302
+ debugError(`Command: ${error.command}`);
303
+ debugError(`Exit Code: ${error.exitCode}`);
304
+ debugError(`Stdout: ${error.stdout}`);
305
+ debugError(`Stderr: ${error.stderr}`);
306
+ debugError(
307
+ `Environment: ${JSON.stringify(error.environment, null, 2)}`
308
+ );
309
+
310
+ // Provide helpful debugging information
311
+ if (
312
+ error.stderr.includes('connection') ||
313
+ error.stderr.includes('timeout')
314
+ ) {
315
+ debugLog(`Hint: Check if LocalStack is running at ${this.endpoint}`);
316
+ debugLog(`Hint: Try running: awslocal sts get-caller-identity`);
317
+ } else if (
318
+ error.stderr.includes('not found') ||
319
+ error.stderr.includes('command not found')
320
+ ) {
321
+ debugLog(
322
+ `Hint: Make sure awslocal is installed: pip install awscli-local`
323
+ );
324
+ }
325
+ } else {
326
+ debugLog('Unexpected error listing secrets:', error);
327
+ }
328
+
329
+ // Return empty array for non-critical errors to allow tests to continue
330
+ return [];
331
+ }
332
+ }
333
+
334
+ async waitForLocalStack(): Promise<void> {
335
+ const maxRetries = 30;
336
+ const retryDelay = 1000; // 1 second
337
+
338
+ debugLog(`Waiting for LocalStack at ${this.endpoint}...`);
339
+
340
+ for (let i = 0; i < maxRetries; i++) {
341
+ try {
342
+ const command = `awslocal sts get-caller-identity --region ${this.region}`;
343
+ debugLog(`Testing LocalStack connectivity with: ${command}`);
344
+
345
+ const result = await execAwslocalCommand(
346
+ command,
347
+ this.getEnvironment()
348
+ );
349
+ debugLog('LocalStack is ready');
350
+ if (result.stdout) {
351
+ debugLog(`LocalStack response: ${result.stdout}`);
352
+ }
353
+ return;
354
+ } catch (error) {
355
+ if (i === maxRetries - 1) {
356
+ debugError('LocalStack failed to start within timeout');
357
+
358
+ if (error instanceof AwslocalCommandError) {
359
+ debugError(`Last command: ${error.command}`);
360
+ debugError(`Last exit code: ${error.exitCode}`);
361
+ debugError(`Last stdout: ${error.stdout}`);
362
+ debugError(`Last stderr: ${error.stderr}`);
363
+ debugError(
364
+ `Environment: ${JSON.stringify(error.environment, null, 2)}`
365
+ );
366
+
367
+ // Provide comprehensive debugging information
368
+ debugLog('\n=== LocalStack Debugging Information ===');
369
+ debugLog(`Endpoint: ${this.endpoint}`);
370
+ debugLog(`Region: ${this.region}`);
371
+ debugLog(`Access Key: ${this.accessKey}`);
372
+ debugLog(`Secret Key: ${this.secretKey}`);
373
+ debugLog('\n=== Troubleshooting Steps ===');
374
+ debugLog('1. Check if LocalStack is running:');
375
+ debugLog(' docker-compose ps');
376
+ debugLog('2. Check LocalStack logs:');
377
+ debugLog(' docker-compose logs localstack');
378
+ debugLog('3. Test awslocal installation:');
379
+ debugLog(' awslocal --version');
380
+ debugLog('4. Test direct connectivity:');
381
+ debugLog(` curl ${this.endpoint}/health`);
382
+ debugLog('5. Check if port 4566 is accessible:');
383
+ debugLog(' netstat -tlnp | grep 4566');
384
+ } else {
385
+ debugLog(`Last error: ${error.message}`);
386
+ }
387
+
388
+ throw new Error('LocalStack failed to start within timeout');
389
+ }
390
+
391
+ debugLog(`Waiting for LocalStack... (${i + 1}/${maxRetries})`);
392
+ if (error instanceof AwslocalCommandError && error.stderr) {
393
+ debugLog(`Error: ${error.stderr}`);
394
+ }
395
+
396
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
397
+ }
398
+ }
399
+ }
400
+ }
401
+
402
+ export async function cli(args: string[], cwd = '.'): Promise<CliResult> {
403
+ return new Promise((resolve) => {
404
+ // Clean environment by removing AWS variables that might interfere
405
+ const cleanEnv = { ...process.env };
406
+ delete cleanEnv.AWS_PROFILE;
407
+ delete cleanEnv.AWS_DEFAULT_PROFILE;
408
+ delete cleanEnv.AWS_SESSION_TOKEN;
409
+ delete cleanEnv.AWS_SECURITY_TOKEN;
410
+ delete cleanEnv.AWS_ROLE_ARN;
411
+ delete cleanEnv.AWS_ROLE_SESSION_NAME;
412
+ delete cleanEnv.AWS_WEB_IDENTITY_TOKEN_FILE;
413
+ delete cleanEnv.AWS_WEB_IDENTITY_TOKEN;
414
+
415
+ // Default environment variables for LocalStack
416
+ const defaultEnv = {
417
+ AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
418
+ AWS_ACCESS_KEY_ID: 'test',
419
+ AWS_SECRET_ACCESS_KEY: 'test',
420
+ AWS_DEFAULT_REGION: 'us-east-1',
421
+ AWS_REGION: 'us-east-1',
422
+ NODE_ENV: 'test'
423
+ };
424
+
425
+ const envVars = { ...cleanEnv, ...defaultEnv };
426
+ const command = `node ${path.resolve('./dist/index')} ${args.join(' ')}`;
427
+
428
+ debugLog(`Running CLI command: ${command}`);
429
+ debugLog(
430
+ `Environment: AWS_ENDPOINT_URL=${envVars.AWS_ENDPOINT_URL}, AWS_DEFAULT_REGION=${envVars.AWS_DEFAULT_REGION}`
431
+ );
432
+
433
+ exec(command, { cwd, env: envVars }, (error, stdout, stderr) => {
434
+ const result = {
435
+ code: error && error.code ? error.code : 0,
436
+ error: error || null,
437
+ stdout,
438
+ stderr
439
+ };
440
+
441
+ if (result.code !== 0) {
442
+ debugError(`CLI command failed with code ${result.code}`);
443
+ debugError(`Command: ${command}`);
444
+ debugError(`Stdout: ${result.stdout}`);
445
+ debugError(`Stderr: ${result.stderr}`);
446
+ }
447
+
448
+ resolve(result);
449
+ });
450
+ });
451
+ }
452
+
453
+ export async function cliWithEnv(
454
+ args: string[],
455
+ env: Record<string, string>,
456
+ cwd = '.'
457
+ ): Promise<CliResult> {
458
+ return new Promise((resolve) => {
459
+ // Clean environment by removing AWS variables that might interfere
460
+ const cleanEnv = { ...process.env };
461
+ delete cleanEnv.AWS_PROFILE;
462
+ delete cleanEnv.AWS_DEFAULT_PROFILE;
463
+ delete cleanEnv.AWS_SESSION_TOKEN;
464
+ delete cleanEnv.AWS_SECURITY_TOKEN;
465
+ delete cleanEnv.AWS_ROLE_ARN;
466
+ delete cleanEnv.AWS_ROLE_SESSION_NAME;
467
+ delete cleanEnv.AWS_WEB_IDENTITY_TOKEN_FILE;
468
+ delete cleanEnv.AWS_WEB_IDENTITY_TOKEN;
469
+
470
+ // Default environment variables for LocalStack
471
+ const defaultEnv = {
472
+ AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
473
+ AWS_ACCESS_KEY_ID: 'test',
474
+ AWS_SECRET_ACCESS_KEY: 'test',
475
+ AWS_DEFAULT_REGION: 'us-east-1',
476
+ AWS_REGION: 'us-east-1',
477
+ NODE_ENV: 'test'
478
+ };
479
+
480
+ const envVars = { ...cleanEnv, ...defaultEnv, ...env };
481
+ const command = `node ${path.resolve('./dist/index')} ${args.join(' ')}`;
482
+
483
+ debugLog(`Running CLI command with custom env: ${command}`);
484
+ debugLog(
485
+ `Environment: AWS_ENDPOINT_URL=${envVars.AWS_ENDPOINT_URL}, AWS_DEFAULT_REGION=${envVars.AWS_DEFAULT_REGION}`
486
+ );
487
+ debugLog(`Custom env vars:`, Object.keys(env));
488
+
489
+ exec(command, { cwd, env: envVars }, (error, stdout, stderr) => {
490
+ const result = {
491
+ code: error && error.code ? error.code : 0,
492
+ error: error || null,
493
+ stdout,
494
+ stderr
495
+ };
496
+
497
+ if (result.code !== 0) {
498
+ debugError(`CLI command failed with code ${result.code}`);
499
+ debugError(`Command: ${command}`);
500
+ debugError(`Stdout: ${result.stdout}`);
501
+ debugError(`Stderr: ${result.stderr}`);
502
+ }
503
+
504
+ resolve(result);
505
+ });
506
+ });
507
+ }
508
+
509
+ export function createTempFile(content: string) {
510
+ const tempDir = os.tmpdir();
511
+ const tempFile = path.join(tempDir, `env-secrets-test-${Date.now()}.env`);
512
+ fs.writeFileSync(tempFile, content);
513
+ return tempFile;
514
+ }
515
+
516
+ export function cleanupTempFile(filePath: string): void {
517
+ try {
518
+ if (fs.existsSync(filePath)) {
519
+ fs.unlinkSync(filePath);
520
+ }
521
+ } catch (error) {
522
+ debugWarn(`Failed to cleanup temp file ${filePath}:`, error);
523
+ }
524
+ }
525
+
526
+ export function createTestProfile() {
527
+ const homeDir = os.homedir();
528
+ const awsDir = path.join(homeDir, '.aws');
529
+ const credentialsFile = path.join(awsDir, 'credentials');
530
+ const configFile = path.join(awsDir, 'config');
531
+
532
+ // Create .aws directory if it doesn't exist
533
+ if (!fs.existsSync(awsDir)) {
534
+ fs.mkdirSync(awsDir, { mode: 0o700 });
535
+ }
536
+
537
+ // Backup existing files if they exist
538
+ const backupCredentials = credentialsFile + '.backup';
539
+ const backupConfig = configFile + '.backup';
540
+
541
+ if (fs.existsSync(credentialsFile)) {
542
+ fs.copyFileSync(credentialsFile, backupCredentials);
543
+ }
544
+ if (fs.existsSync(configFile)) {
545
+ fs.copyFileSync(configFile, backupConfig);
546
+ }
547
+
548
+ // Create test profile
549
+ const credentialsContent = `[default]
550
+ aws_access_key_id = test
551
+ aws_secret_access_key = test
552
+
553
+ [env-secrets-test]
554
+ aws_access_key_id = test
555
+ aws_secret_access_key = test
556
+ `;
557
+
558
+ const configContent = `[default]
559
+ region = us-east-1
560
+
561
+ [profile env-secrets-test]
562
+ region = us-east-1
563
+ `;
564
+
565
+ fs.writeFileSync(credentialsFile, credentialsContent, { mode: 0o600 });
566
+ fs.writeFileSync(configFile, configContent, { mode: 0o600 });
567
+
568
+ return awsDir;
569
+ }
570
+
571
+ export function restoreTestProfile(awsDir: string | undefined): void {
572
+ if (!awsDir) {
573
+ debugWarn('No AWS directory provided for profile restoration');
574
+ return;
575
+ }
576
+
577
+ const credentialsFile = path.join(awsDir, 'credentials');
578
+ const configFile = path.join(awsDir, 'config');
579
+ const backupCredentials = credentialsFile + '.backup';
580
+ const backupConfig = configFile + '.backup';
581
+
582
+ try {
583
+ if (fs.existsSync(backupCredentials)) {
584
+ fs.copyFileSync(backupCredentials, credentialsFile);
585
+ fs.unlinkSync(backupCredentials);
586
+ } else if (fs.existsSync(credentialsFile)) {
587
+ fs.unlinkSync(credentialsFile);
588
+ }
589
+
590
+ if (fs.existsSync(backupConfig)) {
591
+ fs.copyFileSync(backupConfig, configFile);
592
+ fs.unlinkSync(backupConfig);
593
+ } else if (fs.existsSync(configFile)) {
594
+ fs.unlinkSync(configFile);
595
+ }
596
+ } catch (error) {
597
+ debugWarn('Failed to restore AWS profile:', error);
598
+ }
599
+ }
600
+
601
+ export async function checkAwslocalInstalled(): Promise<void> {
602
+ try {
603
+ await execAwslocalCommand('awslocal --version', {});
604
+ debugLog('awslocal is installed and available');
605
+ } catch (error) {
606
+ debugLog('awslocal is not installed or not available in PATH');
607
+ debugLog('');
608
+
609
+ if (error instanceof AwslocalCommandError) {
610
+ debugError(`Command: ${error.command}`);
611
+ debugError(`Exit Code: ${error.exitCode}`);
612
+ debugError(`Stdout: ${error.stdout}`);
613
+ debugError(`Stderr: ${error.stderr}`);
614
+ } else {
615
+ debugError(`Error: ${error.message}`);
616
+ }
617
+
618
+ debugLog('');
619
+ debugLog('Please install awslocal by running:');
620
+ debugLog(' pip install awscli-local');
621
+ debugLog('');
622
+ debugLog('Or using npm:');
623
+ debugLog(' npm install -g awscli-local');
624
+ debugLog('');
625
+ debugLog(
626
+ 'For more information, visit: https://github.com/localstack/awscli-local'
627
+ );
628
+ debugLog('');
629
+ debugLog('=== Troubleshooting Steps ===');
630
+ debugLog('1. Verify Python/pip is installed:');
631
+ debugLog(' python --version');
632
+ debugLog(' pip --version');
633
+ debugLog('2. Try installing with sudo if needed:');
634
+ debugLog(' sudo pip install awscli-local');
635
+ debugLog('3. Check if awslocal is in your PATH:');
636
+ debugLog(' which awslocal');
637
+ debugLog(' echo $PATH');
638
+ debugLog('4. Try using the full path:');
639
+ debugLog(' /usr/local/bin/awslocal --version');
640
+
641
+ throw new Error(
642
+ 'awslocal is required for end-to-end tests but is not installed'
643
+ );
644
+ }
645
+ }