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,7 +39,7 @@ jobs:
39
39
  run: yarn test:unit:coverage
40
40
 
41
41
  - name: Upload coverage reports to Codecov
42
- uses: codecov/codecov-action@v5
42
+ uses: codecov/codecov-action@v6
43
43
  with:
44
44
  token: ${{ secrets.CODECOV_TOKEN }}
45
45
  directory: ./coverage
package/.rulesrc.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "targets": ["claude", "codex"],
3
+ "agents": [
4
+ "local-dev",
5
+ "docs",
6
+ "cicd",
7
+ "observability",
8
+ "publishing",
9
+ "git-hooks",
10
+ "linting",
11
+ "logging",
12
+ "testing"
13
+ ],
14
+ "skills": ["github-health-check"],
15
+ "ballastVersion": "5.9.2",
16
+ "languages": ["typescript"],
17
+ "paths": {
18
+ "typescript": ["."]
19
+ }
20
+ }
package/AGENTS.md CHANGED
@@ -156,3 +156,37 @@ yarn install
156
156
  brew install awscli-local
157
157
  yarn build
158
158
  ```
159
+
160
+ ## Installed agent rules
161
+
162
+ Created by [Ballast](https://github.com/everydaydevopsio/ballast) v5.9.2. Do not edit this section.
163
+
164
+ Read and follow these rule files in `.codex/rules/` when they apply:
165
+
166
+ - `.codex/rules/local-dev-badges.md` — Add standard badges (CI, Release, License, GitHub Release, npm) to the top of README.md
167
+ - `.codex/rules/local-dev-env.md` — Local development environment specialist - reproducible dev setup, DX, and documentation
168
+ - `.codex/rules/local-dev-license.md` — License setup - ensure LICENSE file, package.json license field, and README reference (default MIT; overridable in AGENTS.md/CLAUDE.md)
169
+ - `.codex/rules/local-dev-mcp.md` — Optional: use GitHub MCP and issues MCP (Jira/Linear/GitHub) for local-dev context
170
+ - `.codex/rules/docs.md` — Documentation specialist - GitHub Markdown docs by default, or maintain existing Docusaurus sites with publish-docs automation
171
+ - `.codex/rules/cicd.md` — CI/CD specialist - pipeline design, quality gates, and deployment
172
+ - `.codex/rules/observability.md` — Observability specialist - logging, tracing, metrics, and SLOs
173
+ - `.codex/rules/publishing-api.md` — REST API publishing specialist - Docker CD with Kubernetes health probes and Helm chart update
174
+ - `.codex/rules/publishing-apps.md` — App publishing specialist - npmjs for Node apps, PyPI for Python apps, GitHub Releases for Go apps
175
+ - `.codex/rules/publishing-apt.md` — APT/deb package publishing specialist - GoReleaser nfpms and GitHub Releases
176
+ - `.codex/rules/publishing-brew.md` — Homebrew tap publishing specialist - GoReleaser brews block and tap repo setup
177
+ - `.codex/rules/publishing-cli.md` — CLI publishing specialist - GoReleaser for Go, npmjs for Node, PyPI for Python
178
+ - `.codex/rules/publishing-libraries.md` — Library publishing specialist - npmjs for TypeScript, PyPI for Python, GitHub tags/releases for Go
179
+ - `.codex/rules/publishing-sdks.md` — SDK publishing specialist - npmjs for TypeScript SDKs, PyPI for Python SDKs, GitHub tags/releases for Go SDKs
180
+ - `.codex/rules/publishing-web.md` — Web app publishing specialist - Docker to GHCR/Docker Hub with Helm chart CD on push to main
181
+ - `.codex/rules/git-hooks.md` — Git hook specialist - configure pre-commit, pre-push, and Husky workflows that match the repository layout
182
+ - `.codex/rules/typescript-linting.md` — TypeScript linting specialist - implements comprehensive linting and code formatting for TypeScript/JavaScript projects
183
+ - `.codex/rules/typescript-logging.md` — Centralized logging specialist - configures Pino with Fluentd for Node/Next.js, and pino-browser to /api/logs
184
+ - `.codex/rules/typescript-testing.md` — Testing specialist - sets up Jest (default) or Vitest for Vite projects, 50% coverage, and test step in build GitHub Action
185
+
186
+ ## Installed skills
187
+
188
+ Created by [Ballast](https://github.com/everydaydevopsio/ballast) v5.9.2. Do not edit this section.
189
+
190
+ Read and use these skill files in `.codex/rules/` when they are relevant:
191
+
192
+ - `.codex/rules/github-health-check.md` — Run a comprehensive GitHub repository health check. Use this skill whenever the user asks to: check GitHub health, audit the repo, check CI status, review open PRs, merge Dependabot PRs, check code coverage, check GitHub Code Quality, check GitHub security feature enablement, check security advisories, check Dependabot alerts, check code scanning alerts, check secret scanning alerts, check Snyk integration, keep GitHub in good shape, or any variation of "how is the repo doing". Also trigger for: "check dependabot PRs", "any PRs to merge", "check branch status", "repo health", "GitHub status check", "what needs attention in GitHub", "tidy up GitHub".
package/CLAUDE.md ADDED
@@ -0,0 +1,58 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code for working in this repository.
4
+
5
+ ## Repository Facts
6
+
7
+ Use this section for durable repo-specific facts that agents repeatedly need. Prefer facts stored here over re-deriving them with shell commands on every task.
8
+
9
+ Keep only stable, reviewable metadata here. Do not store secrets, credentials, or ephemeral runtime state.
10
+
11
+ Suggested facts to record:
12
+
13
+ - Canonical GitHub repo: `<OWNER/REPO>`
14
+ - Default branch: `<main>`
15
+ - Primary package manager: `<pnpm | npm | yarn | uv | go>`
16
+ - Version-file locations agents should check first: `<.nvmrc, packageManager, pyproject.toml, go.mod, etc.>`
17
+ - Canonical config files: `<paths agents should read before falling back to discovery>`
18
+ - Primary CI workflows: `<workflow filenames>`
19
+ - Primary release/publish workflows: `<workflow filenames>`
20
+ - Preferred build/test/lint/format/coverage commands: `<commands>`
21
+ - Coverage threshold: `<value>`
22
+ - Generated or protected paths agents should avoid editing directly: `<paths>`
23
+
24
+ Update this section when those facts change. If live runtime state is required, discover it separately instead of treating it as a durable repo fact.
25
+
26
+ ## Installed agent rules
27
+
28
+ Created by [Ballast](https://github.com/everydaydevopsio/ballast) v5.9.2. Do not edit this section.
29
+
30
+ Read and follow these rule files in `.claude/rules/` when they apply:
31
+
32
+ - `.claude/rules/local-dev-badges.md` — Add standard badges (CI, Release, License, GitHub Release, npm) to the top of README.md
33
+ - `.claude/rules/local-dev-env.md` — Local development environment specialist - reproducible dev setup, DX, and documentation
34
+ - `.claude/rules/local-dev-license.md` — License setup - ensure LICENSE file, package.json license field, and README reference (default MIT; overridable in AGENTS.md/CLAUDE.md)
35
+ - `.claude/rules/local-dev-mcp.md` — Optional: use GitHub MCP and issues MCP (Jira/Linear/GitHub) for local-dev context
36
+ - `.claude/rules/docs.md` — Documentation specialist - GitHub Markdown docs by default, or maintain existing Docusaurus sites with publish-docs automation
37
+ - `.claude/rules/cicd.md` — CI/CD specialist - pipeline design, quality gates, and deployment
38
+ - `.claude/rules/observability.md` — Observability specialist - logging, tracing, metrics, and SLOs
39
+ - `.claude/rules/publishing-api.md` — REST API publishing specialist - Docker CD with Kubernetes health probes and Helm chart update
40
+ - `.claude/rules/publishing-apps.md` — App publishing specialist - npmjs for Node apps, PyPI for Python apps, GitHub Releases for Go apps
41
+ - `.claude/rules/publishing-apt.md` — APT/deb package publishing specialist - GoReleaser nfpms and GitHub Releases
42
+ - `.claude/rules/publishing-brew.md` — Homebrew tap publishing specialist - GoReleaser brews block and tap repo setup
43
+ - `.claude/rules/publishing-cli.md` — CLI publishing specialist - GoReleaser for Go, npmjs for Node, PyPI for Python
44
+ - `.claude/rules/publishing-libraries.md` — Library publishing specialist - npmjs for TypeScript, PyPI for Python, GitHub tags/releases for Go
45
+ - `.claude/rules/publishing-sdks.md` — SDK publishing specialist - npmjs for TypeScript SDKs, PyPI for Python SDKs, GitHub tags/releases for Go SDKs
46
+ - `.claude/rules/publishing-web.md` — Web app publishing specialist - Docker to GHCR/Docker Hub with Helm chart CD on push to main
47
+ - `.claude/rules/git-hooks.md` — Git hook specialist - configure pre-commit, pre-push, and Husky workflows that match the repository layout
48
+ - `.claude/rules/typescript-linting.md` — TypeScript linting specialist - implements comprehensive linting and code formatting for TypeScript/JavaScript projects
49
+ - `.claude/rules/typescript-logging.md` — Centralized logging specialist - configures Pino with Fluentd for Node/Next.js, and pino-browser to /api/logs
50
+ - `.claude/rules/typescript-testing.md` — Testing specialist - sets up Jest (default) or Vitest for Vite projects, 50% coverage, and test step in build GitHub Action
51
+
52
+ ## Installed skills
53
+
54
+ Created by [Ballast](https://github.com/everydaydevopsio/ballast) v5.9.2. Do not edit this section.
55
+
56
+ Read and use these skill files in `.claude/skills/` when they are relevant:
57
+
58
+ - `.claude/skills/github-health-check.skill` — Run a comprehensive GitHub repository health check. Use this skill whenever the user asks to: check GitHub health, audit the repo, check CI status, review open PRs, merge Dependabot PRs, check code coverage, check GitHub Code Quality, check GitHub security feature enablement, check security advisories, check Dependabot alerts, check code scanning alerts, check secret scanning alerts, check Snyk integration, keep GitHub in good shape, or any variation of "how is the repo doing". Also trigger for: "check dependabot PRs", "any PRs to merge", "check branch status", "repo health", "GitHub status check", "what needs attention in GitHub", "tidy up GitHub".
package/README.md CHANGED
@@ -101,9 +101,10 @@ env-secrets aws -s my-app-secrets -r us-east-1 -- node app.js
101
101
  - `-r, --region <region>` (optional): AWS region where the secret is stored. If not provided, uses `AWS_DEFAULT_REGION` environment variable
102
102
  - `-p, --profile <profile>` (optional): Local AWS profile to use. If not provided, uses `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables
103
103
  - `-o, --output <file>` (optional): Output secrets to a file instead of injecting into environment variables. File will be created with 0400 permissions and will not overwrite existing files
104
+ - `--no-shell` (optional): Run the program directly without a shell wrapper. Disables shell expansion — use when you do not need `$VAR` interpolation in arguments
104
105
  - `-- <program-to-run>`: The program to run with the injected environment variables (only used when `-o` is not specified)
105
106
 
106
- For `aws secret` management subcommands (`create`, `update`, `append`, `remove`, `upsert`/`import`, `list`, `get`, `delete`), use:
107
+ For `aws secret` management subcommands (`create`, `update`, `upsert`/`import`, `append`, `remove`, `list`, `get`, `value`, `delete`), use:
107
108
 
108
109
  - `-r, --region <region>` to target a specific region
109
110
  - `-p, --profile <profile>` to select credentials profile
@@ -111,8 +112,8 @@ For `aws secret` management subcommands (`create`, `update`, `append`, `remove`,
111
112
 
112
113
  These options are honored consistently on `aws secret` subcommands.
113
114
 
114
- `env-secrets aws -s` is for fetching/injecting secret values into a child process.
115
- `env-secrets aws secret ...` is for lifecycle management commands (`create`, `update`, `append`, `remove`, `upsert`/`import`, `list`, `get`, `delete`).
115
+ `env-secrets aws -s` is for fetching/injecting secret values into a child process. Use `--no-shell` to run the program directly without a shell wrapper (disables shell expansion).
116
+ `env-secrets aws secret ...` is for lifecycle management commands (`create`, `update`, `upsert`/`import`, `append`, `remove`, `list`, `get`, `value`, `delete`).
116
117
 
117
118
  #### Examples
118
119
 
@@ -262,6 +263,19 @@ env-secrets aws secret append -n app/dev --key JIRA_EMAIL_TOKEN -v blah --output
262
263
  env-secrets aws secret remove -n app/dev --key API_KEY --key OLD_TOKEN --output json
263
264
  ```
264
265
 
266
+ 13. **View the values of a secret:**
267
+
268
+ ```bash
269
+ # Table output — values masked as **** by default
270
+ env-secrets aws secret value -n app/dev -r us-east-1
271
+
272
+ # Reveal actual values (warning printed to stderr)
273
+ env-secrets aws secret value -n app/dev -r us-east-1 --reveal
274
+
275
+ # JSON output — full values, suitable for scripting (warns if stdout is a terminal)
276
+ env-secrets aws secret value -n app/dev -r us-east-1 --output json
277
+ ```
278
+
265
279
  ## Security Considerations
266
280
 
267
281
  - 🔐 **Credential Management**: The tool respects AWS credential precedence (environment variables, IAM roles, profiles)
@@ -1,4 +1,4 @@
1
- import { cliWithEnv } from './utils/test-utils';
1
+ import { cliWithEnv, cliWithRealSpawn } from './utils/test-utils';
2
2
  import { registerAwsE2eContext } from './utils/aws-e2e-context';
3
3
 
4
4
  describe('AWS Program Execution CLI Args', () => {
@@ -42,3 +42,99 @@ describe('AWS Program Execution CLI Args', () => {
42
42
  expect(envVars.API_KEY).toBe('secret123');
43
43
  });
44
44
  });
45
+
46
+ describe('AWS Real Spawn Execution (no NODE_ENV=test)', () => {
47
+ const { createTestSecret, getLocalStackEnv } = registerAwsE2eContext();
48
+
49
+ test('injected env vars are visible to the spawned child process (shell mode)', async () => {
50
+ const secret = await createTestSecret({
51
+ name: `test-secret-realspawn-${Date.now()}`,
52
+ value: '{"INJECTED_KEY": "injected_value"}',
53
+ description: 'Real spawn env injection test'
54
+ });
55
+
56
+ // printenv avoids any shell-quoting complexity in the test command string
57
+ const result = await cliWithRealSpawn(
58
+ ['aws', '-s', secret.prefixedName, '--', 'printenv', 'INJECTED_KEY'],
59
+ getLocalStackEnv()
60
+ );
61
+
62
+ expect(result.code).toBe(0);
63
+ expect(result.stdout.trim()).toBe('injected_value');
64
+ });
65
+
66
+ test('exit code of child process is propagated on success', async () => {
67
+ const secret = await createTestSecret({
68
+ name: `test-secret-exitcode-ok-${Date.now()}`,
69
+ value: '{"DUMMY": "1"}',
70
+ description: 'Exit code propagation test (success)'
71
+ });
72
+
73
+ // Use --no-shell so node -e args are passed directly without shell re-parsing
74
+ const result = await cliWithRealSpawn(
75
+ [
76
+ 'aws',
77
+ '-s',
78
+ secret.prefixedName,
79
+ '--no-shell',
80
+ '--',
81
+ 'node',
82
+ '-e',
83
+ 'process.exit(0)'
84
+ ],
85
+ getLocalStackEnv()
86
+ );
87
+
88
+ expect(result.code).toBe(0);
89
+ });
90
+
91
+ test('exit code of child process is propagated on failure', async () => {
92
+ const secret = await createTestSecret({
93
+ name: `test-secret-exitcode-fail-${Date.now()}`,
94
+ value: '{"DUMMY": "1"}',
95
+ description: 'Exit code propagation test (failure)'
96
+ });
97
+
98
+ // Use --no-shell so node -e args are passed directly without shell re-parsing
99
+ const result = await cliWithRealSpawn(
100
+ [
101
+ 'aws',
102
+ '-s',
103
+ secret.prefixedName,
104
+ '--no-shell',
105
+ '--',
106
+ 'node',
107
+ '-e',
108
+ 'process.exit(42)'
109
+ ],
110
+ getLocalStackEnv()
111
+ );
112
+
113
+ expect(result.code).toBe(42);
114
+ });
115
+
116
+ test('--no-shell passes args directly and env is injected', async () => {
117
+ const secret = await createTestSecret({
118
+ name: `test-secret-noshell-${Date.now()}`,
119
+ value: '{"INJECTED_KEY": "direct_value"}',
120
+ description: 'No-shell spawn test'
121
+ });
122
+
123
+ // printenv avoids any shell-quoting complexity in the test command string
124
+ const result = await cliWithRealSpawn(
125
+ [
126
+ 'aws',
127
+ '-s',
128
+ secret.prefixedName,
129
+ '--no-shell',
130
+ '--',
131
+ 'printenv',
132
+ 'INJECTED_KEY'
133
+ ],
134
+ getLocalStackEnv()
135
+ );
136
+
137
+ expect(result.code).toBe(0);
138
+ expect(result.stdout.trim()).toBe('direct_value');
139
+ });
140
+ });
@@ -0,0 +1,142 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+
5
+ import { cliWithEnv, cleanupTempFile } from './utils/test-utils';
6
+ import { registerAwsE2eContext } from './utils/aws-e2e-context';
7
+
8
+ describe('AWS Secret Value CLI Args', () => {
9
+ const { getLocalStackEnv } = registerAwsE2eContext();
10
+
11
+ test('should show masked values by default (table output)', async () => {
12
+ const secretName = `e2e-value-masked-${Date.now()}`;
13
+ const tempFile = path.join(
14
+ os.tmpdir(),
15
+ `env-secrets-value-${Date.now()}.env`
16
+ );
17
+ fs.writeFileSync(tempFile, 'DB_PASSWORD=super-secret\nAPI_KEY=abc123');
18
+
19
+ const createResult = await cliWithEnv(
20
+ ['aws', 'secret', 'upsert', '--file', tempFile, '--name', secretName],
21
+ getLocalStackEnv()
22
+ );
23
+ expect(createResult.code).toBe(0);
24
+
25
+ const valueResult = await cliWithEnv(
26
+ ['aws', 'secret', 'value', '-n', secretName],
27
+ getLocalStackEnv()
28
+ );
29
+ expect(valueResult.code).toBe(0);
30
+ expect(valueResult.stdout).toContain('DB_PASSWORD');
31
+ expect(valueResult.stdout).toContain('API_KEY');
32
+ expect(valueResult.stdout).toContain('****');
33
+ expect(valueResult.stdout).not.toContain('super-secret');
34
+ expect(valueResult.stdout).not.toContain('abc123');
35
+
36
+ const deleteResult1 = await cliWithEnv(
37
+ [
38
+ 'aws',
39
+ 'secret',
40
+ 'delete',
41
+ '-n',
42
+ secretName,
43
+ '--force-delete-without-recovery',
44
+ '--yes'
45
+ ],
46
+ getLocalStackEnv()
47
+ );
48
+ expect(deleteResult1.code).toBe(0);
49
+ cleanupTempFile(tempFile);
50
+ });
51
+
52
+ test('should reveal values with --reveal flag and warn on stderr', async () => {
53
+ const secretName = `e2e-value-reveal-${Date.now()}`;
54
+ const tempFile = path.join(
55
+ os.tmpdir(),
56
+ `env-secrets-reveal-${Date.now()}.env`
57
+ );
58
+ fs.writeFileSync(tempFile, 'DB_PASSWORD=super-secret\nAPI_KEY=abc123');
59
+
60
+ const createResult = await cliWithEnv(
61
+ ['aws', 'secret', 'upsert', '--file', tempFile, '--name', secretName],
62
+ getLocalStackEnv()
63
+ );
64
+ expect(createResult.code).toBe(0);
65
+
66
+ const valueResult = await cliWithEnv(
67
+ ['aws', 'secret', 'value', '-n', secretName, '--reveal'],
68
+ getLocalStackEnv()
69
+ );
70
+ expect(valueResult.code).toBe(0);
71
+ expect(valueResult.stdout).toContain('DB_PASSWORD');
72
+ expect(valueResult.stdout).toContain('super-secret');
73
+ expect(valueResult.stdout).toContain('API_KEY');
74
+ expect(valueResult.stdout).toContain('abc123');
75
+ expect(valueResult.stderr).toContain(
76
+ 'Warning: displaying sensitive secret values.'
77
+ );
78
+
79
+ const deleteResult2 = await cliWithEnv(
80
+ [
81
+ 'aws',
82
+ 'secret',
83
+ 'delete',
84
+ '-n',
85
+ secretName,
86
+ '--force-delete-without-recovery',
87
+ '--yes'
88
+ ],
89
+ getLocalStackEnv()
90
+ );
91
+ expect(deleteResult2.code).toBe(0);
92
+ cleanupTempFile(tempFile);
93
+ });
94
+
95
+ test('should output full values as JSON without --reveal', async () => {
96
+ const secretName = `e2e-value-json-${Date.now()}`;
97
+ const tempFile = path.join(
98
+ os.tmpdir(),
99
+ `env-secrets-json-${Date.now()}.env`
100
+ );
101
+ fs.writeFileSync(tempFile, 'DB_PASSWORD=super-secret\nAPI_KEY=abc123');
102
+
103
+ const createResult = await cliWithEnv(
104
+ ['aws', 'secret', 'upsert', '--file', tempFile, '--name', secretName],
105
+ getLocalStackEnv()
106
+ );
107
+ expect(createResult.code).toBe(0);
108
+
109
+ const valueResult = await cliWithEnv(
110
+ ['aws', 'secret', 'value', '-n', secretName, '--output', 'json'],
111
+ getLocalStackEnv()
112
+ );
113
+ expect(valueResult.code).toBe(0);
114
+ const parsed = JSON.parse(valueResult.stdout) as Record<string, string>;
115
+ expect(parsed.DB_PASSWORD).toBe('super-secret');
116
+ expect(parsed.API_KEY).toBe('abc123');
117
+
118
+ const deleteResult3 = await cliWithEnv(
119
+ [
120
+ 'aws',
121
+ 'secret',
122
+ 'delete',
123
+ '-n',
124
+ secretName,
125
+ '--force-delete-without-recovery',
126
+ '--yes'
127
+ ],
128
+ getLocalStackEnv()
129
+ );
130
+ expect(deleteResult3.code).toBe(0);
131
+ cleanupTempFile(tempFile);
132
+ });
133
+
134
+ test('should fail for a non-existent secret', async () => {
135
+ const valueResult = await cliWithEnv(
136
+ ['aws', 'secret', 'value', '-n', 'does-not-exist-e2e'],
137
+ getLocalStackEnv()
138
+ );
139
+ expect(valueResult.code).not.toBe(0);
140
+ expect(valueResult.stderr).toContain('not found');
141
+ });
142
+ });
@@ -654,6 +654,84 @@ export function restoreTestProfile(
654
654
  }
655
655
  }
656
656
 
657
+ /**
658
+ * Like cliWithEnv but does NOT set NODE_ENV=test, so the real spawn path in
659
+ * src/index.ts is exercised instead of the early-return test branch.
660
+ */
661
+ export async function cliWithRealSpawn(
662
+ args: string[],
663
+ env: Record<string, string>,
664
+ cwd = '.'
665
+ ): Promise<CliResult> {
666
+ return new Promise((resolve) => {
667
+ const cleanEnv = { ...process.env };
668
+ delete cleanEnv.AWS_PROFILE;
669
+ delete cleanEnv.AWS_DEFAULT_PROFILE;
670
+ delete cleanEnv.AWS_SESSION_TOKEN;
671
+ delete cleanEnv.AWS_SECURITY_TOKEN;
672
+ delete cleanEnv.AWS_ROLE_ARN;
673
+ delete cleanEnv.AWS_ROLE_SESSION_NAME;
674
+ delete cleanEnv.AWS_WEB_IDENTITY_TOKEN_FILE;
675
+ delete cleanEnv.AWS_WEB_IDENTITY_TOKEN;
676
+ // Deliberately omit NODE_ENV=test so real spawn is used
677
+ delete cleanEnv.NODE_ENV;
678
+
679
+ const defaultEnv = {
680
+ AWS_ENDPOINT_URL: process.env.LOCALSTACK_URL || 'http://localhost:4566',
681
+ AWS_ACCESS_KEY_ID: 'test',
682
+ AWS_SECRET_ACCESS_KEY: 'test',
683
+ AWS_DEFAULT_REGION: 'us-east-1',
684
+ AWS_REGION: 'us-east-1'
685
+ };
686
+
687
+ const envVars = { ...cleanEnv, ...defaultEnv, ...env };
688
+ const cliPath = path.resolve('./dist/index');
689
+ const spawnArgs = [cliPath, ...args];
690
+ const command = `node ${cliPath} ${args.join(' ')}`;
691
+
692
+ debugLog(`Running CLI command (real spawn): ${command}`);
693
+
694
+ const child = spawn('node', spawnArgs, { cwd, env: envVars });
695
+
696
+ let stdout = '';
697
+ let stderr = '';
698
+ child.stdout.on('data', (chunk: Buffer) => {
699
+ stdout += chunk.toString();
700
+ });
701
+ child.stderr.on('data', (chunk: Buffer) => {
702
+ stderr += chunk.toString();
703
+ });
704
+
705
+ child.on('close', (code: number | null, signal: NodeJS.Signals | null) => {
706
+ const exitCode = code ?? (signal ? 1 : 0);
707
+ const errorMessage = signal
708
+ ? `Process terminated by signal ${signal}`
709
+ : exitCode !== 0
710
+ ? `Process exited with code ${exitCode}`
711
+ : null;
712
+ const result = {
713
+ code: exitCode,
714
+ error: errorMessage ? new Error(errorMessage) : null,
715
+ stdout,
716
+ stderr
717
+ };
718
+
719
+ if (exitCode !== 0) {
720
+ debugError(
721
+ signal
722
+ ? `CLI command failed: terminated by signal ${signal}`
723
+ : `CLI command failed with code ${exitCode}`
724
+ );
725
+ debugError(`Command: ${command}`);
726
+ debugError(`Stdout: ${result.stdout}`);
727
+ debugError(`Stderr: ${result.stderr}`);
728
+ }
729
+
730
+ resolve(result);
731
+ });
732
+ });
733
+ }
734
+
657
735
  export async function checkAwslocalInstalled(): Promise<void> {
658
736
  try {
659
737
  await execAwslocalCommand('awslocal --version', {});
@@ -11,6 +11,7 @@ import {
11
11
  readStdin,
12
12
  renderTable,
13
13
  resolveAwsScope,
14
+ resolveOutputFormat,
14
15
  resolveSecretValue
15
16
  } from '../../src/cli/helpers';
16
17
 
@@ -214,4 +215,38 @@ describe('cli/helpers', () => {
214
215
  region: 'us-west-2'
215
216
  });
216
217
  });
218
+
219
+ describe('resolveOutputFormat', () => {
220
+ it('uses explicit local output option', () => {
221
+ expect(resolveOutputFormat({ output: 'json' })).toBe('json');
222
+ expect(resolveOutputFormat({ output: 'table' })).toBe('table');
223
+ });
224
+
225
+ it('throws for invalid local output option', () => {
226
+ expect(() => resolveOutputFormat({ output: 'xml' })).toThrow(
227
+ 'Invalid output format'
228
+ );
229
+ });
230
+
231
+ it('inherits json or table from global options', () => {
232
+ const jsonCmd = { optsWithGlobals: () => ({ output: 'json' }) };
233
+ const tableCmd = { optsWithGlobals: () => ({ output: 'table' }) };
234
+ expect(resolveOutputFormat({}, jsonCmd)).toBe('json');
235
+ expect(resolveOutputFormat({}, tableCmd)).toBe('table');
236
+ });
237
+
238
+ it('ignores a file path in global output and defaults to table', () => {
239
+ const command = { optsWithGlobals: () => ({ output: 'secrets.env' }) };
240
+ expect(resolveOutputFormat({}, command)).toBe('table');
241
+ });
242
+
243
+ it('defaults to table when no options are provided', () => {
244
+ expect(resolveOutputFormat({})).toBe('table');
245
+ });
246
+
247
+ it('prefers local option over global option', () => {
248
+ const command = { optsWithGlobals: () => ({ output: 'json' }) };
249
+ expect(resolveOutputFormat({ output: 'table' }, command)).toBe('table');
250
+ });
251
+ });
217
252
  });