env-secrets 0.5.2 → 0.5.4

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 (56) hide show
  1. package/.claude/rules/cicd.md +189 -0
  2. package/.claude/rules/docs.md +96 -0
  3. package/.claude/rules/git-hooks.md +43 -0
  4. package/.claude/rules/local-dev-badges.md +91 -0
  5. package/.claude/rules/local-dev-env.md +382 -0
  6. package/.claude/rules/local-dev-license.md +104 -0
  7. package/.claude/rules/local-dev-mcp.md +70 -0
  8. package/.claude/rules/observability.md +23 -0
  9. package/.claude/rules/publishing-api.md +158 -0
  10. package/.claude/rules/publishing-apps.md +204 -0
  11. package/.claude/rules/publishing-apt.md +146 -0
  12. package/.claude/rules/publishing-brew.md +110 -0
  13. package/.claude/rules/publishing-cli.md +238 -0
  14. package/.claude/rules/publishing-libraries.md +115 -0
  15. package/.claude/rules/publishing-sdks.md +109 -0
  16. package/.claude/rules/publishing-web.md +185 -0
  17. package/.claude/rules/typescript-linting.md +141 -0
  18. package/.claude/rules/typescript-logging.md +356 -0
  19. package/.claude/rules/typescript-testing.md +185 -0
  20. package/.claude/settings.json +18 -0
  21. package/.claude/skills/github-health-check.skill +0 -0
  22. package/.codex/rules/cicd.md +21 -0
  23. package/.codex/rules/docs.md +98 -0
  24. package/.codex/rules/git-hooks.md +43 -0
  25. package/.codex/rules/github-health-check.md +440 -0
  26. package/.codex/rules/local-dev-env.md +47 -0
  27. package/.codex/rules/local-dev-license.md +3 -1
  28. package/.codex/rules/publishing-api.md +160 -0
  29. package/.codex/rules/publishing-apps.md +206 -0
  30. package/.codex/rules/publishing-apt.md +148 -0
  31. package/.codex/rules/publishing-brew.md +112 -0
  32. package/.codex/rules/publishing-cli.md +240 -0
  33. package/.codex/rules/publishing-libraries.md +117 -0
  34. package/.codex/rules/publishing-sdks.md +111 -0
  35. package/.codex/rules/publishing-web.md +187 -0
  36. package/.codex/rules/typescript-linting.md +143 -0
  37. package/.codex/rules/typescript-logging.md +358 -0
  38. package/.codex/rules/typescript-testing.md +187 -0
  39. package/.github/workflows/deploy-docs.yml +2 -2
  40. package/.github/workflows/unittests.yaml +1 -1
  41. package/.rulesrc.json +20 -0
  42. package/AGENTS.md +34 -0
  43. package/CLAUDE.md +58 -0
  44. package/README.md +17 -3
  45. package/__e2e__/aws-exec-args.test.ts +97 -1
  46. package/__e2e__/aws-secret-value-args.test.ts +142 -0
  47. package/__e2e__/utils/test-utils.ts +78 -0
  48. package/__tests__/cli/helpers.test.ts +35 -0
  49. package/__tests__/index.test.ts +208 -58
  50. package/dist/cli/helpers.js +13 -1
  51. package/dist/index.js +94 -44
  52. package/docker-compose.yaml +1 -1
  53. package/docs/AWS.md +42 -13
  54. package/package.json +6 -6
  55. package/src/cli/helpers.ts +16 -0
  56. package/src/index.ts +117 -52
@@ -39,6 +39,24 @@ const mockObjectToExport = objectToExport as jest.MockedFunction<
39
39
  typeof objectToExport
40
40
  >;
41
41
 
42
+ // Build a ChildProcess-like mock that records event handlers
43
+ function makeChildMock() {
44
+ const handlers: Record<string, ((...args: unknown[]) => void)[]> = {};
45
+ const child = {
46
+ stdio: 'inherit',
47
+ on: jest.fn((event: string, cb: (...args: unknown[]) => void) => {
48
+ if (!handlers[event]) handlers[event] = [];
49
+ handlers[event].push(cb);
50
+ return child;
51
+ }),
52
+ emit(event: string, ...args: unknown[]) {
53
+ (handlers[event] ?? []).forEach((cb) => cb(...args));
54
+ }
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ } as any;
57
+ return child;
58
+ }
59
+
42
60
  describe('index.ts CLI functionality', () => {
43
61
  beforeEach(() => {
44
62
  jest.clearAllMocks();
@@ -94,43 +112,67 @@ describe('index.ts CLI functionality', () => {
94
112
  );
95
113
  });
96
114
 
97
- it('should spawn a program when provided', async () => {
115
+ it('should spawn using shell (default) with joined command string', async () => {
98
116
  const mockEnv = { SECRET_KEY: 'secret_value' };
99
117
  mockSecretsmanager.mockResolvedValue(mockEnv);
100
118
 
101
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
- const mockChildProcess = {
103
- stdio: 'inherit'
104
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
- } as any;
106
- mockSpawn.mockReturnValue(mockChildProcess);
119
+ const child = makeChildMock();
120
+ mockSpawn.mockReturnValue(child);
107
121
 
108
122
  const program = ['node', 'script.js', 'arg1', 'arg2'];
109
- const options = { secret: 'my-secret' };
123
+ const options = { secret: 'my-secret', shell: true };
110
124
 
111
- // Simulate the action logic
112
125
  let env = await mockSecretsmanager(options);
113
126
  env = Object.assign({}, process.env, env);
114
127
 
115
128
  if (program && program.length > 0) {
116
- mockSpawn(program[0], program.slice(1), {
129
+ mockSpawn(program.join(' '), [], {
117
130
  stdio: 'inherit',
118
131
  shell: true,
119
132
  env
120
133
  });
121
134
  }
122
135
 
123
- expect(mockSpawn).toHaveBeenCalledWith(
124
- 'node',
125
- ['script.js', 'arg1', 'arg2'],
126
- {
127
- stdio: 'inherit',
128
- shell: true,
129
- env: expect.objectContaining({
130
- SECRET_KEY: 'secret_value'
131
- })
136
+ expect(mockSpawn).toHaveBeenCalledWith('node script.js arg1 arg2', [], {
137
+ stdio: 'inherit',
138
+ shell: true,
139
+ env: expect.objectContaining({
140
+ SECRET_KEY: 'secret_value'
141
+ })
142
+ });
143
+ });
144
+
145
+ it('should spawn without shell when --no-shell is passed', async () => {
146
+ const mockEnv = { SECRET_KEY: 'secret_value' };
147
+ mockSecretsmanager.mockResolvedValue(mockEnv);
148
+
149
+ const child = makeChildMock();
150
+ mockSpawn.mockReturnValue(child);
151
+
152
+ const program = ['node', 'script.js', 'arg1'];
153
+ const options = { secret: 'my-secret', shell: false };
154
+
155
+ let env = await mockSecretsmanager(options);
156
+ env = Object.assign({}, process.env, env);
157
+
158
+ if (program && program.length > 0) {
159
+ if (options.shell) {
160
+ mockSpawn(program.join(' '), [], {
161
+ stdio: 'inherit',
162
+ shell: true,
163
+ env
164
+ });
165
+ } else {
166
+ mockSpawn(program[0], program.slice(1), { stdio: 'inherit', env });
132
167
  }
133
- );
168
+ }
169
+
170
+ expect(mockSpawn).toHaveBeenCalledWith('node', ['script.js', 'arg1'], {
171
+ stdio: 'inherit',
172
+ env: expect.objectContaining({
173
+ SECRET_KEY: 'secret_value'
174
+ })
175
+ });
134
176
  });
135
177
 
136
178
  it('should not spawn a program when no program is provided', async () => {
@@ -143,7 +185,7 @@ describe('index.ts CLI functionality', () => {
143
185
 
144
186
  const program: string[] = [];
145
187
  if (program && program.length > 0) {
146
- mockSpawn(program[0], program.slice(1), {
188
+ mockSpawn(program.join(' '), [], {
147
189
  stdio: 'inherit',
148
190
  shell: true,
149
191
  env
@@ -163,7 +205,7 @@ describe('index.ts CLI functionality', () => {
163
205
 
164
206
  const program: string[] = [];
165
207
  if (program && program.length > 0) {
166
- mockSpawn(program[0], program.slice(1), {
208
+ mockSpawn(program.join(' '), [], {
167
209
  stdio: 'inherit',
168
210
  shell: true,
169
211
  env
@@ -173,26 +215,21 @@ describe('index.ts CLI functionality', () => {
173
215
  expect(mockSpawn).not.toHaveBeenCalled();
174
216
  });
175
217
 
176
- it('should handle single program argument', async () => {
218
+ it('should handle single program argument (shell mode)', async () => {
177
219
  const mockEnv = { SECRET_KEY: 'secret_value' };
178
220
  mockSecretsmanager.mockResolvedValue(mockEnv);
179
221
 
180
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
- const mockChildProcess = {
182
- stdio: 'inherit'
183
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
- } as any;
185
- mockSpawn.mockReturnValue(mockChildProcess);
222
+ const child = makeChildMock();
223
+ mockSpawn.mockReturnValue(child);
186
224
 
187
225
  const program = ['echo'];
188
- const options = { secret: 'my-secret' };
226
+ const options = { secret: 'my-secret', shell: true };
189
227
 
190
- // Simulate the action logic
191
228
  let env = await mockSecretsmanager(options);
192
229
  env = Object.assign({}, process.env, env);
193
230
 
194
231
  if (program && program.length > 0) {
195
- mockSpawn(program[0], program.slice(1), {
232
+ mockSpawn(program.join(' '), [], {
196
233
  stdio: 'inherit',
197
234
  shell: true,
198
235
  env
@@ -219,13 +256,12 @@ describe('index.ts CLI functionality', () => {
219
256
 
220
257
  mockSecretsmanager.mockResolvedValue(mockSecrets);
221
258
 
222
- // Simulate the action logic
223
259
  let env = await mockSecretsmanager({ secret: 'my-secret' });
224
260
  env = Object.assign({}, process.env, env);
225
261
 
226
262
  const program = ['echo'];
227
263
  if (program && program.length > 0) {
228
- mockSpawn(program[0], program.slice(1), {
264
+ mockSpawn(program.join(' '), [], {
229
265
  stdio: 'inherit',
230
266
  shell: true,
231
267
  env
@@ -247,13 +283,12 @@ describe('index.ts CLI functionality', () => {
247
283
  it('should handle secretsmanager returning empty object', async () => {
248
284
  mockSecretsmanager.mockResolvedValue({});
249
285
 
250
- // Simulate the action logic
251
286
  let env = await mockSecretsmanager({ secret: 'my-secret' });
252
287
  env = Object.assign({}, process.env, env);
253
288
 
254
289
  const program = ['echo'];
255
290
  if (program && program.length > 0) {
256
- mockSpawn(program[0], program.slice(1), {
291
+ mockSpawn(program.join(' '), [], {
257
292
  stdio: 'inherit',
258
293
  shell: true,
259
294
  env
@@ -276,6 +311,128 @@ describe('index.ts CLI functionality', () => {
276
311
  'AWS connection failed'
277
312
  );
278
313
  });
314
+
315
+ describe('ChildProcess error and exit handling', () => {
316
+ beforeEach(() => {
317
+ jest.spyOn(process, 'exit').mockImplementation(() => {
318
+ throw new Error('process.exit called');
319
+ });
320
+ jest.spyOn(console, 'error').mockImplementation(() => undefined);
321
+ });
322
+
323
+ afterEach(() => {
324
+ jest.restoreAllMocks();
325
+ });
326
+
327
+ it('should print error message and exit 1 on child process error', async () => {
328
+ const mockEnv = { SECRET_KEY: 'secret_value' };
329
+ mockSecretsmanager.mockResolvedValue(mockEnv);
330
+
331
+ const child = makeChildMock();
332
+ mockSpawn.mockReturnValue(child);
333
+
334
+ let env = await mockSecretsmanager({ secret: 'my-secret' });
335
+ env = Object.assign({}, process.env, env);
336
+
337
+ const program = ['nonexistent-cmd'];
338
+ mockSpawn(program.join(' '), [], {
339
+ stdio: 'inherit',
340
+ shell: true,
341
+ env
342
+ });
343
+
344
+ // Simulate attaching handlers as src/index.ts does
345
+ child.on('error', (err: Error) => {
346
+ // eslint-disable-next-line no-console
347
+ console.error(`Failed to start process: ${err.message}`);
348
+ process.exit(1);
349
+ });
350
+
351
+ expect(() => child.emit('error', new Error('spawn ENOENT'))).toThrow(
352
+ 'process.exit called'
353
+ );
354
+
355
+ // eslint-disable-next-line no-console
356
+ expect(console.error).toHaveBeenCalledWith(
357
+ 'Failed to start process: spawn ENOENT'
358
+ );
359
+ expect(process.exit).toHaveBeenCalledWith(1);
360
+ });
361
+
362
+ it('should propagate child exit code 0', async () => {
363
+ const mockEnv = { SECRET_KEY: 'secret_value' };
364
+ mockSecretsmanager.mockResolvedValue(mockEnv);
365
+
366
+ const child = makeChildMock();
367
+ mockSpawn.mockReturnValue(child);
368
+
369
+ let env = await mockSecretsmanager({ secret: 'my-secret' });
370
+ env = Object.assign({}, process.env, env);
371
+
372
+ mockSpawn(['node', '-e', '"process.exit(0)"'].join(' '), [], {
373
+ stdio: 'inherit',
374
+ shell: true,
375
+ env
376
+ });
377
+
378
+ child.on('exit', (code: number | null, signal: string | null) => {
379
+ process.exit(signal ? 1 : code ?? 0);
380
+ });
381
+
382
+ expect(() => child.emit('exit', 0, null)).toThrow(
383
+ 'process.exit called'
384
+ );
385
+ expect(process.exit).toHaveBeenCalledWith(0);
386
+ });
387
+
388
+ it('should propagate non-zero child exit code', async () => {
389
+ const mockEnv = { SECRET_KEY: 'secret_value' };
390
+ mockSecretsmanager.mockResolvedValue(mockEnv);
391
+
392
+ const child = makeChildMock();
393
+ mockSpawn.mockReturnValue(child);
394
+
395
+ let env = await mockSecretsmanager({ secret: 'my-secret' });
396
+ env = Object.assign({}, process.env, env);
397
+
398
+ mockSpawn(['node', '-e', '"process.exit(42)"'].join(' '), [], {
399
+ stdio: 'inherit',
400
+ shell: true,
401
+ env
402
+ });
403
+
404
+ child.on('exit', (code: number | null, signal: string | null) => {
405
+ process.exit(signal ? 1 : code ?? 0);
406
+ });
407
+
408
+ expect(() => child.emit('exit', 42, null)).toThrow(
409
+ 'process.exit called'
410
+ );
411
+ expect(process.exit).toHaveBeenCalledWith(42);
412
+ });
413
+
414
+ it('should exit 1 when child is killed by a signal', async () => {
415
+ const mockEnv = { SECRET_KEY: 'secret_value' };
416
+ mockSecretsmanager.mockResolvedValue(mockEnv);
417
+
418
+ const child = makeChildMock();
419
+ mockSpawn.mockReturnValue(child);
420
+
421
+ let env = await mockSecretsmanager({ secret: 'my-secret' });
422
+ env = Object.assign({}, process.env, env);
423
+
424
+ mockSpawn('sleep 10', [], { stdio: 'inherit', shell: true, env });
425
+
426
+ child.on('exit', (code: number | null, signal: string | null) => {
427
+ process.exit(signal ? 1 : code ?? 0);
428
+ });
429
+
430
+ expect(() => child.emit('exit', null, 'SIGTERM')).toThrow(
431
+ 'process.exit called'
432
+ );
433
+ expect(process.exit).toHaveBeenCalledWith(1);
434
+ });
435
+ });
279
436
  });
280
437
 
281
438
  describe('Debug logging', () => {
@@ -314,12 +471,8 @@ describe('index.ts CLI functionality', () => {
314
471
  const mockEnv = { SECRET_KEY: 'secret_value' };
315
472
  mockSecretsmanager.mockResolvedValue(mockEnv);
316
473
 
317
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
318
- const mockChildProcess = {
319
- stdio: 'inherit'
320
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
321
- } as any;
322
- mockSpawn.mockReturnValue(mockChildProcess);
474
+ const child = makeChildMock();
475
+ mockSpawn.mockReturnValue(child);
323
476
 
324
477
  const program = ['node', 'script.js', 'arg1'];
325
478
 
@@ -331,7 +484,7 @@ describe('index.ts CLI functionality', () => {
331
484
  // Simulate debug logging
332
485
  mockDebugInstance(`${program[0]} ${program.slice(1).join(' ')}`);
333
486
 
334
- mockSpawn(program[0], program.slice(1), {
487
+ mockSpawn(program.join(' '), [], {
335
488
  stdio: 'inherit',
336
489
  shell: true,
337
490
  env
@@ -438,16 +591,12 @@ describe('index.ts CLI functionality', () => {
438
591
  const mockSecrets = { SECRET_KEY: 'secret_value' };
439
592
  mockSecretsmanager.mockResolvedValue(mockSecrets);
440
593
 
441
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
442
- const mockChildProcess = {
443
- stdio: 'inherit'
444
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
445
- } as any;
446
- mockSpawn.mockReturnValue(mockChildProcess);
447
-
448
- const options: { secret: string; output?: string } = {
449
- secret: 'my-secret'
450
- // No output option
594
+ const child = makeChildMock();
595
+ mockSpawn.mockReturnValue(child);
596
+
597
+ const options: { secret: string; output?: string; shell: boolean } = {
598
+ secret: 'my-secret',
599
+ shell: true
451
600
  };
452
601
 
453
602
  const program = ['echo', 'hello'];
@@ -474,7 +623,7 @@ describe('index.ts CLI functionality', () => {
474
623
  const env = Object.assign({}, process.env, secrets);
475
624
 
476
625
  if (program && program.length > 0) {
477
- mockSpawn(program[0], program.slice(1), {
626
+ mockSpawn(program.join(' '), [], {
478
627
  stdio: 'inherit',
479
628
  shell: true,
480
629
  env
@@ -483,7 +632,7 @@ describe('index.ts CLI functionality', () => {
483
632
  }
484
633
 
485
634
  expect(mockSecretsmanager).toHaveBeenCalledWith(options);
486
- expect(mockSpawn).toHaveBeenCalledWith('echo', ['hello'], {
635
+ expect(mockSpawn).toHaveBeenCalledWith('echo hello', [], {
487
636
  stdio: 'inherit',
488
637
  shell: true,
489
638
  env: expect.objectContaining({
@@ -538,9 +687,10 @@ describe('index.ts CLI functionality', () => {
538
687
  mockObjectToExport.mockReturnValue(mockEnvContent);
539
688
  mockExistsSync.mockReturnValue(false);
540
689
 
541
- const options: { secret: string; output: string } = {
690
+ const options: { secret: string; output: string; shell: boolean } = {
542
691
  secret: 'my-secret',
543
- output: '/tmp/secrets.env'
692
+ output: '/tmp/secrets.env',
693
+ shell: true
544
694
  };
545
695
 
546
696
  const program = ['echo', 'hello'];
@@ -566,7 +716,7 @@ describe('index.ts CLI functionality', () => {
566
716
  const env = Object.assign({}, process.env, secrets);
567
717
 
568
718
  if (program && program.length > 0) {
569
- mockSpawn(program[0], program.slice(1), {
719
+ mockSpawn(program.join(' '), [], {
570
720
  stdio: 'inherit',
571
721
  shell: true,
572
722
  env
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.resolveAwsScope = exports.parseEnvSecretsFile = exports.parseEnvSecrets = exports.resolveSecretValue = exports.readStdin = exports.parseRecoveryDays = exports.printData = exports.renderTable = exports.asOutputFormat = void 0;
12
+ exports.resolveAwsScope = exports.resolveOutputFormat = exports.parseEnvSecretsFile = exports.parseEnvSecrets = exports.resolveSecretValue = exports.readStdin = exports.parseRecoveryDays = exports.printData = exports.renderTable = exports.asOutputFormat = void 0;
13
13
  const promises_1 = require("node:fs/promises");
14
14
  const asOutputFormat = (value) => {
15
15
  if (value !== 'json' && value !== 'table') {
@@ -156,6 +156,18 @@ const parseEnvSecretsFile = (path) => __awaiter(void 0, void 0, void 0, function
156
156
  return (0, exports.parseEnvSecrets)(content);
157
157
  });
158
158
  exports.parseEnvSecretsFile = parseEnvSecretsFile;
159
+ const resolveOutputFormat = (options, command) => {
160
+ var _a, _b;
161
+ if (options.output) {
162
+ return (0, exports.asOutputFormat)(options.output);
163
+ }
164
+ const globalOutput = (_b = (_a = command === null || command === void 0 ? void 0 : command.optsWithGlobals) === null || _a === void 0 ? void 0 : _a.call(command)) === null || _b === void 0 ? void 0 : _b.output;
165
+ if (globalOutput === 'json' || globalOutput === 'table') {
166
+ return globalOutput;
167
+ }
168
+ return 'table';
169
+ };
170
+ exports.resolveOutputFormat = resolveOutputFormat;
159
171
  const resolveAwsScope = (options, command) => {
160
172
  var _a;
161
173
  const globalOptions = ((_a = command === null || command === void 0 ? void 0 : command.optsWithGlobals) === null || _a === void 0 ? void 0 : _a.call(command)) || {};
package/dist/index.js CHANGED
@@ -91,6 +91,7 @@ const awsCommand = program
91
91
  .option('-p, --profile <profile>', 'profile to use')
92
92
  .option('-r, --region <region>', 'region to use')
93
93
  .option('-o, --output <file>', 'output secrets to file instead of environment variables')
94
+ .option('--no-shell', 'run program directly without a shell (disables shell expansion)')
94
95
  .action((program, options) => __awaiter(void 0, void 0, void 0, function* () {
95
96
  if (!options.secret) {
96
97
  exitWithError(new Error('Missing required option --secret for this command.'));
@@ -123,10 +124,20 @@ const awsCommand = program
123
124
  console.log(JSON.stringify(env));
124
125
  return;
125
126
  }
126
- (0, node_child_process_1.spawn)(program[0], program.slice(1), {
127
- stdio: 'inherit',
128
- shell: true,
129
- env
127
+ const child = options.shell
128
+ ? (0, node_child_process_1.spawn)(program.join(' '), [], {
129
+ stdio: 'inherit',
130
+ shell: true,
131
+ env
132
+ })
133
+ : (0, node_child_process_1.spawn)(program[0], program.slice(1), { stdio: 'inherit', env });
134
+ child.on('error', (err) => {
135
+ // eslint-disable-next-line no-console
136
+ console.error(`Failed to start process: ${err.message}`);
137
+ process.exit(1);
138
+ });
139
+ child.on('exit', (code, signal) => {
140
+ process.exit(signal ? 1 : code !== null && code !== void 0 ? code : 0);
130
141
  });
131
142
  }
132
143
  }
@@ -148,13 +159,9 @@ secretCommand
148
159
  .option('-r, --region <region>', 'region to use')
149
160
  .option('--output <format>', 'output format: json|table')
150
161
  .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
151
- var _a;
152
162
  try {
153
163
  const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
154
- const globalOptions = command.optsWithGlobals();
155
- const output = (_a = options.output) !== null && _a !== void 0 ? _a : (typeof globalOptions.output === 'string'
156
- ? globalOptions.output
157
- : 'table');
164
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
158
165
  const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
159
166
  if (!value) {
160
167
  throw new Error('Secret value is required. Provide --value, --value-stdin, or --file.');
@@ -192,13 +199,9 @@ secretCommand
192
199
  .option('-r, --region <region>', 'region to use')
193
200
  .option('--output <format>', 'output format: json|table')
194
201
  .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
195
- var _b;
196
202
  try {
197
203
  const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
198
- const globalOptions = command.optsWithGlobals();
199
- const output = (_b = options.output) !== null && _b !== void 0 ? _b : (typeof globalOptions.output === 'string'
200
- ? globalOptions.output
201
- : 'table');
204
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
202
205
  const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
203
206
  if (!value && !options.description && !options.kmsKeyId) {
204
207
  throw new Error('Nothing to update. Provide --value/--value-stdin/--file, --description, or --kms-key-id.');
@@ -234,13 +237,9 @@ secretCommand
234
237
  .option('-r, --region <region>', 'region to use')
235
238
  .option('--output <format>', 'output format: json|table')
236
239
  .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
237
- var _c;
238
240
  try {
239
241
  const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
240
- const globalOptions = command.optsWithGlobals();
241
- const output = (_c = options.output) !== null && _c !== void 0 ? _c : (typeof globalOptions.output === 'string'
242
- ? globalOptions.output
243
- : 'table');
242
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
244
243
  const parsed = yield (0, helpers_1.parseEnvSecretsFile)(options.file);
245
244
  if (parsed.entries.length === 0) {
246
245
  throw new Error('No env entries found. Include lines like KEY=value or export KEY=value.');
@@ -346,13 +345,9 @@ secretCommand
346
345
  .option('-r, --region <region>', 'region to use')
347
346
  .option('--output <format>', 'output format: json|table')
348
347
  .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
349
- var _d;
350
348
  try {
351
349
  const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
352
- const globalOptions = command.optsWithGlobals();
353
- const output = (_d = options.output) !== null && _d !== void 0 ? _d : (typeof globalOptions.output === 'string'
354
- ? globalOptions.output
355
- : 'table');
350
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
356
351
  const value = yield (0, helpers_1.resolveSecretValue)(options.value, options.valueStdin, options.file);
357
352
  if (!value) {
358
353
  throw new Error('Append value is required. Provide --value, --value-stdin, or --file.');
@@ -393,13 +388,9 @@ secretCommand
393
388
  .option('-r, --region <region>', 'region to use')
394
389
  .option('--output <format>', 'output format: json|table')
395
390
  .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
396
- var _e;
397
391
  try {
398
392
  const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
399
- const globalOptions = command.optsWithGlobals();
400
- const output = (_e = options.output) !== null && _e !== void 0 ? _e : (typeof globalOptions.output === 'string'
401
- ? globalOptions.output
402
- : 'table');
393
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
403
394
  const keys = options.key;
404
395
  const current = yield (0, secretsmanager_admin_1.getSecretString)({
405
396
  name: options.name,
@@ -450,13 +441,9 @@ secretCommand
450
441
  .option('-r, --region <region>', 'region to use')
451
442
  .option('--output <format>', 'output format: json|table')
452
443
  .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
453
- var _f;
454
444
  try {
455
445
  const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
456
- const globalOptions = command.optsWithGlobals();
457
- const output = (_f = options.output) !== null && _f !== void 0 ? _f : (typeof globalOptions.output === 'string'
458
- ? globalOptions.output
459
- : 'table');
446
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
460
447
  const result = yield (0, secretsmanager_admin_1.listSecrets)({
461
448
  prefix: options.prefix,
462
449
  tags: options.tag,
@@ -486,13 +473,9 @@ secretCommand
486
473
  .option('-r, --region <region>', 'region to use')
487
474
  .option('--output <format>', 'output format: json|table')
488
475
  .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
489
- var _g;
490
476
  try {
491
477
  const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
492
- const globalOptions = command.optsWithGlobals();
493
- const output = (_g = options.output) !== null && _g !== void 0 ? _g : (typeof globalOptions.output === 'string'
494
- ? globalOptions.output
495
- : 'table');
478
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
496
479
  const result = yield (0, secretsmanager_admin_1.getSecretMetadata)({
497
480
  name: options.name,
498
481
  profile,
@@ -521,6 +504,77 @@ secretCommand
521
504
  exitWithError(error);
522
505
  }
523
506
  }));
507
+ secretCommand
508
+ .command('value')
509
+ .description('get the values of a secret')
510
+ .requiredOption('-n, --name <name>', 'secret name')
511
+ .option('--reveal', 'reveal secret values in table output (values are masked by default)', false)
512
+ .option('-p, --profile <profile>', 'profile to use')
513
+ .option('-r, --region <region>', 'region to use')
514
+ .option('--output <format>', 'output format: json|table')
515
+ .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
516
+ try {
517
+ const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
518
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
519
+ let secretString;
520
+ try {
521
+ secretString = yield (0, secretsmanager_admin_1.getSecretString)({
522
+ name: options.name,
523
+ profile,
524
+ region
525
+ });
526
+ }
527
+ catch (error) {
528
+ const msg = error instanceof Error ? error.message : String(error);
529
+ if (msg.includes('cannot be edited with append/remove')) {
530
+ throw new Error(`Secret "${options.name}" is stored as binary and cannot be displayed as text.`);
531
+ }
532
+ throw error;
533
+ }
534
+ let jsonEntries;
535
+ try {
536
+ const parsed = JSON.parse(secretString);
537
+ if (parsed && !Array.isArray(parsed) && typeof parsed === 'object') {
538
+ jsonEntries = Object.entries(parsed).map(([key, value]) => ({ key, value }));
539
+ }
540
+ else {
541
+ jsonEntries = [{ key: options.name, value: secretString }];
542
+ }
543
+ }
544
+ catch (_a) {
545
+ jsonEntries = [{ key: options.name, value: secretString }];
546
+ }
547
+ if (output === 'json') {
548
+ if (process.stdout.isTTY) {
549
+ // eslint-disable-next-line no-console
550
+ console.error('Warning: displaying sensitive secret values.');
551
+ }
552
+ const result = Object.fromEntries(jsonEntries.map(({ key, value }) => [key, value]));
553
+ // eslint-disable-next-line no-console
554
+ console.log(JSON.stringify(result, null, 2));
555
+ return;
556
+ }
557
+ if (options.reveal) {
558
+ // eslint-disable-next-line no-console
559
+ console.error('Warning: displaying sensitive secret values.');
560
+ }
561
+ const rows = jsonEntries.map(({ key, value }) => ({
562
+ key,
563
+ value: options.reveal
564
+ ? typeof value === 'string'
565
+ ? value
566
+ : JSON.stringify(value)
567
+ : '****'
568
+ }));
569
+ (0, helpers_1.printData)((0, helpers_1.asOutputFormat)(output), [
570
+ { key: 'key', label: 'Key' },
571
+ { key: 'value', label: 'Value' }
572
+ ], rows);
573
+ }
574
+ catch (error) {
575
+ exitWithError(error);
576
+ }
577
+ }));
524
578
  secretCommand
525
579
  .command('delete')
526
580
  .description('delete a secret in AWS Secrets Manager')
@@ -532,13 +586,9 @@ secretCommand
532
586
  .option('-r, --region <region>', 'region to use')
533
587
  .option('--output <format>', 'output format: json|table')
534
588
  .action((options, command) => __awaiter(void 0, void 0, void 0, function* () {
535
- var _h;
536
589
  try {
537
590
  const { profile, region } = (0, helpers_1.resolveAwsScope)(options, command);
538
- const globalOptions = command.optsWithGlobals();
539
- const output = (_h = options.output) !== null && _h !== void 0 ? _h : (typeof globalOptions.output === 'string'
540
- ? globalOptions.output
541
- : 'table');
591
+ const output = (0, helpers_1.resolveOutputFormat)(options, command);
542
592
  if (!options.yes) {
543
593
  throw new Error('Delete requires --yes confirmation.');
544
594
  }
@@ -1,7 +1,7 @@
1
1
  services:
2
2
  localstack:
3
3
  container_name: 'localstack'
4
- image: localstack/localstack:latest
4
+ image: localstack/localstack:3
5
5
  ports:
6
6
  - '4566:4566' # LocalStack Gateway
7
7
  environment: