@chriscode/hush 1.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chris Hasson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # @chriscode/hush
2
+
3
+ > SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.
4
+
5
+ Hush manages secrets across your monorepo using [SOPS](https://github.com/getsops/sops) with [age](https://github.com/FiloSottile/age) encryption. It automatically detects your packages and generates the right env files for each.
6
+
7
+ ## Features
8
+
9
+ - **Single encrypted file** - One `.env.encrypted` committed to git
10
+ - **Environment prefixes** - `DEV__` and `PROD__` for env-specific values
11
+ - **Auto-detection** - Finds packages, detects Wrangler vs standard
12
+ - **Smart routing** - `EXPO_PUBLIC_*` to apps, other vars to APIs
13
+ - **Cloudflare integration** - Push secrets to Workers with one command
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm add -D @chriscode/hush
19
+ # or
20
+ npm install -D @chriscode/hush
21
+ ```
22
+
23
+ ### Prerequisites
24
+
25
+ ```bash
26
+ brew install sops age
27
+ ```
28
+
29
+ Set up your age key:
30
+
31
+ ```bash
32
+ mkdir -p ~/.config/sops/age
33
+ age-keygen -o ~/.config/sops/age/key.txt
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### 1. Create `.sops.yaml` in your repo root
39
+
40
+ ```yaml
41
+ creation_rules:
42
+ - encrypted_regex: '.*'
43
+ age: YOUR_AGE_PUBLIC_KEY
44
+ ```
45
+
46
+ Get your public key from `~/.config/sops/age/key.txt`.
47
+
48
+ ### 2. Create `.env` with your secrets
49
+
50
+ ```bash
51
+ # Shared across all environments
52
+ DATABASE_URL=postgres://localhost/mydb
53
+ EXPO_PUBLIC_API_KEY=pk_xxx
54
+
55
+ # Development only (prefix stripped on decrypt)
56
+ DEV__EXPO_PUBLIC_API_URL=http://localhost:8787
57
+
58
+ # Production only
59
+ PROD__EXPO_PUBLIC_API_URL=https://api.example.com
60
+ ```
61
+
62
+ ### 3. Encrypt and use
63
+
64
+ ```bash
65
+ # Encrypt your secrets
66
+ npx hush encrypt
67
+
68
+ # Decrypt for local development
69
+ npx hush decrypt
70
+
71
+ # Check your setup
72
+ npx hush status
73
+ ```
74
+
75
+ ## Commands
76
+
77
+ | Command | Description |
78
+ |---------|-------------|
79
+ | `hush decrypt` | Decrypt and generate env files for all packages |
80
+ | `hush decrypt --env prod` | Decrypt with production values |
81
+ | `hush encrypt` | Encrypt `.env` to `.env.encrypted` |
82
+ | `hush edit` | Edit encrypted file in `$EDITOR` |
83
+ | `hush push` | Push production secrets to Cloudflare Workers |
84
+ | `hush push --dry-run` | Preview what would be pushed |
85
+ | `hush status` | Show setup status and discovered packages |
86
+
87
+ ## Package Scripts
88
+
89
+ Add to your `package.json`:
90
+
91
+ ```json
92
+ {
93
+ "scripts": {
94
+ "secrets": "hush",
95
+ "secrets:decrypt": "hush decrypt",
96
+ "secrets:encrypt": "hush encrypt",
97
+ "secrets:edit": "hush edit",
98
+ "secrets:push": "hush push",
99
+ "secrets:status": "hush status"
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## How It Works
105
+
106
+ ### Package Detection
107
+
108
+ Hush scans for `package.json` files and determines the package type:
109
+
110
+ | Detection | Type | Output |
111
+ |-----------|------|--------|
112
+ | `wrangler.toml` (Workers) | wrangler | `.dev.vars` |
113
+ | `wrangler.toml` with `pages_build_output_dir` | standard | `.env.development` |
114
+ | No wrangler.toml | standard | `.env.development` |
115
+
116
+ ### Variable Routing
117
+
118
+ - `EXPO_PUBLIC_*` → Standard packages only (apps, Pages)
119
+ - Other variables → Wrangler packages only (Workers APIs)
120
+
121
+ This ensures client-side vars go to your app and server secrets go to your API.
122
+
123
+ ### Environment Prefixes
124
+
125
+ ```bash
126
+ # No prefix = shared
127
+ API_KEY=xxx
128
+
129
+ # DEV__ prefix = development only (stripped to API_KEY)
130
+ DEV__API_KEY=dev_xxx
131
+
132
+ # PROD__ prefix = production only (stripped to API_KEY)
133
+ PROD__API_KEY=prod_xxx
134
+ ```
135
+
136
+ When you run `hush decrypt`:
137
+ - `--env dev` (default): Uses `DEV__` vars, ignores `PROD__`
138
+ - `--env prod`: Uses `PROD__` vars, ignores `DEV__`
139
+
140
+ ## Local Overrides
141
+
142
+ Create `.env.local` (gitignored) for personal overrides:
143
+
144
+ ```bash
145
+ # .env.local
146
+ MY_DEBUG_VAR=true
147
+ ```
148
+
149
+ Local overrides are merged last and take precedence.
150
+
151
+ ## Programmatic Usage
152
+
153
+ ```typescript
154
+ import {
155
+ discoverPackages,
156
+ decrypt,
157
+ parseEnvContent,
158
+ getVarsForEnvironment
159
+ } from '@chriscode/hush';
160
+
161
+ const packages = await discoverPackages('/path/to/monorepo');
162
+ const content = decrypt('/path/to/.env.encrypted');
163
+ const vars = parseEnvContent(content);
164
+ const devVars = getVarsForEnvironment(vars, 'dev');
165
+ ```
166
+
167
+ ## File Reference
168
+
169
+ | File | Committed | Purpose |
170
+ |------|-----------|---------|
171
+ | `.env.encrypted` | Yes | Encrypted secrets (source of truth) |
172
+ | `.sops.yaml` | Yes | SOPS config with public key |
173
+ | `.env` | No | Generated root env |
174
+ | `.env.local` | No | Personal overrides |
175
+ | `.env.development` | No | Generated dev env |
176
+ | `.env.production` | No | Generated prod env |
177
+ | `api/.dev.vars` | No | Generated Wrangler secrets |
178
+
179
+ ## Troubleshooting
180
+
181
+ ### "No identity matched"
182
+ Your age key doesn't match. Get the correct key from a team member.
183
+
184
+ ### "SOPS is not installed"
185
+ ```bash
186
+ brew install sops
187
+ ```
188
+
189
+ ### Package detected as wrong type
190
+ Check for unexpected `wrangler.toml` files. For Cloudflare Pages, ensure it has `pages_build_output_dir` to be treated as standard.
191
+
192
+ ## License
193
+
194
+ MIT
package/bin/hush.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli.js');
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { decryptCommand } from './commands/decrypt.js';
5
+ import { editCommand } from './commands/edit.js';
6
+ import { encryptCommand } from './commands/encrypt.js';
7
+ import { pushCommand } from './commands/push.js';
8
+ import { statusCommand } from './commands/status.js';
9
+ const HELP = `
10
+ ${pc.bold('hush')} - SOPS-based secrets management for monorepos
11
+
12
+ ${pc.bold('Usage:')}
13
+ hush <command> [options]
14
+
15
+ ${pc.bold('Commands:')}
16
+ decrypt Decrypt .env.encrypted and generate env files for all packages
17
+ encrypt Encrypt .env to .env.encrypted
18
+ push Push production secrets to Cloudflare Workers
19
+ edit Open .env.encrypted in editor (SOPS inline edit)
20
+ status Show discovered packages and their styles
21
+ help Show this help message
22
+
23
+ ${pc.bold('Options:')}
24
+ --env <dev|prod> Target environment (default: dev)
25
+ --dry-run Don't make changes, just show what would happen
26
+ --root <path> Monorepo root directory (default: cwd)
27
+
28
+ ${pc.bold('Examples:')}
29
+ hush decrypt Decrypt for development
30
+ hush decrypt --env prod Decrypt for production
31
+ hush encrypt Encrypt .env file
32
+ hush push Push prod secrets to Wrangler
33
+ hush push --dry-run Preview what would be pushed
34
+ hush edit Edit encrypted file in $EDITOR
35
+ hush status Show package detection info
36
+ `;
37
+ function parseArgs(args) {
38
+ const result = {
39
+ command: 'help',
40
+ env: 'dev',
41
+ dryRun: false,
42
+ root: process.cwd(),
43
+ };
44
+ for (let i = 0; i < args.length; i++) {
45
+ const arg = args[i];
46
+ if (arg === '--env' && args[i + 1]) {
47
+ const envArg = args[i + 1];
48
+ if (envArg === 'dev' || envArg === 'prod') {
49
+ result.env = envArg;
50
+ }
51
+ else {
52
+ console.error(pc.red(`Invalid environment: ${envArg}`));
53
+ console.error(pc.dim('Valid values: dev, prod'));
54
+ process.exit(1);
55
+ }
56
+ i++;
57
+ }
58
+ else if (arg === '--dry-run') {
59
+ result.dryRun = true;
60
+ }
61
+ else if (arg === '--root' && args[i + 1]) {
62
+ result.root = resolve(args[i + 1]);
63
+ i++;
64
+ }
65
+ else if (!arg.startsWith('-') && !result.command) {
66
+ result.command = arg;
67
+ }
68
+ else if (!arg.startsWith('-')) {
69
+ result.command = arg;
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+ async function main() {
75
+ const args = process.argv.slice(2);
76
+ const parsed = parseArgs(args);
77
+ switch (parsed.command) {
78
+ case 'decrypt':
79
+ await decryptCommand({ root: parsed.root, env: parsed.env });
80
+ break;
81
+ case 'encrypt':
82
+ await encryptCommand({ root: parsed.root });
83
+ break;
84
+ case 'push':
85
+ await pushCommand({ root: parsed.root, dryRun: parsed.dryRun });
86
+ break;
87
+ case 'edit':
88
+ await editCommand({ root: parsed.root });
89
+ break;
90
+ case 'status':
91
+ await statusCommand({ root: parsed.root });
92
+ break;
93
+ case 'help':
94
+ case '--help':
95
+ case '-h':
96
+ console.log(HELP);
97
+ break;
98
+ default:
99
+ console.error(pc.red(`Unknown command: ${parsed.command}`));
100
+ console.log(HELP);
101
+ process.exit(1);
102
+ }
103
+ }
104
+ main().catch((error) => {
105
+ console.error(pc.red('Fatal error:'), error.message);
106
+ process.exit(1);
107
+ });
@@ -0,0 +1,11 @@
1
+ import type { Environment } from '../types.js';
2
+ interface DecryptOptions {
3
+ root: string;
4
+ env?: Environment;
5
+ }
6
+ /**
7
+ * Decrypt command - decrypt .env.encrypted and generate env files for all packages
8
+ */
9
+ export declare function decryptCommand(options: DecryptOptions): Promise<void>;
10
+ export {};
11
+ //# sourceMappingURL=decrypt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../src/commands/decrypt.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAmB,MAAM,aAAa,CAAC;AAEhE,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,WAAW,CAAC;CACnB;AAqCD;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAwF3E"}
@@ -0,0 +1,105 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { discoverPackages } from '../core/discover.js';
5
+ import { expandVariables, formatEnvFile, getVarsForEnvironment, mergeEnvVars, parseEnvContent, parseEnvFile, } from '../core/parse.js';
6
+ import { decrypt as sopsDecrypt } from '../core/sops.js';
7
+ /**
8
+ * Get output files for a package based on its style and environment
9
+ */
10
+ function getOutputFiles(pkg, env) {
11
+ if (pkg.style === 'wrangler') {
12
+ // Wrangler always uses .dev.vars (even for "prod" we generate dev vars for local testing)
13
+ return [{ path: '.dev.vars', env }];
14
+ }
15
+ // Standard style outputs environment-specific files
16
+ if (env === 'dev') {
17
+ return [{ path: '.env.development', env: 'dev' }];
18
+ }
19
+ else {
20
+ return [{ path: '.env.production', env: 'prod' }];
21
+ }
22
+ }
23
+ /**
24
+ * Filter variables for a package based on naming conventions
25
+ * - EXPO_PUBLIC_* vars only go to non-wrangler packages
26
+ * - Other vars go to wrangler packages
27
+ */
28
+ function filterVarsForPackage(vars, pkg) {
29
+ if (pkg.style === 'wrangler') {
30
+ // Wrangler gets everything EXCEPT EXPO_PUBLIC_* vars
31
+ return vars.filter((v) => !v.key.startsWith('EXPO_PUBLIC_'));
32
+ }
33
+ // Standard packages get all vars (including EXPO_PUBLIC_*)
34
+ return vars;
35
+ }
36
+ /**
37
+ * Decrypt command - decrypt .env.encrypted and generate env files for all packages
38
+ */
39
+ export async function decryptCommand(options) {
40
+ const { root, env = 'dev' } = options;
41
+ const encryptedPath = join(root, '.env.encrypted');
42
+ const localPath = join(root, '.env.local');
43
+ // Check encrypted file exists
44
+ if (!existsSync(encryptedPath)) {
45
+ console.error(pc.red(`Error: ${encryptedPath} not found`));
46
+ console.error(pc.dim('Create it with: pnpm secrets encrypt (after creating .env)'));
47
+ process.exit(1);
48
+ }
49
+ console.log(pc.blue('Decrypting secrets...'));
50
+ // Decrypt the encrypted file
51
+ let decryptedContent;
52
+ try {
53
+ decryptedContent = sopsDecrypt(encryptedPath);
54
+ }
55
+ catch (error) {
56
+ console.error(pc.red(error.message));
57
+ process.exit(1);
58
+ }
59
+ // Parse decrypted content
60
+ const allVars = parseEnvContent(decryptedContent);
61
+ console.log(pc.dim(` Parsed ${allVars.length} variables from .env.encrypted`));
62
+ // Get vars for the target environment
63
+ const envVars = getVarsForEnvironment(allVars, env);
64
+ console.log(pc.dim(` ${envVars.length} variables for ${env} environment`));
65
+ // Load local overrides if they exist
66
+ const localVars = parseEnvFile(localPath);
67
+ if (localVars.length > 0) {
68
+ console.log(pc.dim(` ${localVars.length} local overrides from .env.local`));
69
+ }
70
+ // Merge and expand
71
+ const mergedVars = mergeEnvVars(envVars, localVars);
72
+ const expandedVars = expandVariables(mergedVars);
73
+ // Discover packages
74
+ const packages = await discoverPackages(root);
75
+ console.log(pc.blue(`\nDiscovered ${packages.length} packages:`));
76
+ // Generate output files for each package
77
+ for (const pkg of packages) {
78
+ const pkgDir = pkg.path ? join(root, pkg.path) : root;
79
+ const outputFiles = getOutputFiles(pkg, env);
80
+ const pkgVars = filterVarsForPackage(expandedVars, pkg);
81
+ if (pkgVars.length === 0) {
82
+ console.log(pc.dim(` ${pkg.path || '.'} (${pkg.style}) - no applicable vars, skipped`));
83
+ continue;
84
+ }
85
+ for (const output of outputFiles) {
86
+ const outputPath = join(pkgDir, output.path);
87
+ // Ensure directory exists
88
+ const dir = dirname(outputPath);
89
+ if (!existsSync(dir)) {
90
+ mkdirSync(dir, { recursive: true });
91
+ }
92
+ // Write the file
93
+ const content = formatEnvFile(pkgVars);
94
+ writeFileSync(outputPath, content, 'utf-8');
95
+ const relativePath = pkg.path ? `${pkg.path}/${output.path}` : output.path;
96
+ console.log(pc.green(` ${relativePath}`) +
97
+ pc.dim(` (${pkg.style}, ${pkgVars.length} vars)`));
98
+ }
99
+ }
100
+ // Also write root .env with shared vars (useful for scripts)
101
+ const rootEnvPath = join(root, '.env');
102
+ writeFileSync(rootEnvPath, formatEnvFile(expandedVars), 'utf-8');
103
+ console.log(pc.green(` .env`) + pc.dim(` (root, ${expandedVars.length} vars)`));
104
+ console.log(pc.green('\nDecryption complete'));
105
+ }
@@ -0,0 +1,9 @@
1
+ interface EditOptions {
2
+ root: string;
3
+ }
4
+ /**
5
+ * Edit command - open .env.encrypted in editor via SOPS
6
+ */
7
+ export declare function editCommand(options: EditOptions): Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=edit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../src/commands/edit.ts"],"names":[],"mappings":"AAKA,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBrE"}
@@ -0,0 +1,28 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { edit as sopsEdit } from '../core/sops.js';
5
+ /**
6
+ * Edit command - open .env.encrypted in editor via SOPS
7
+ */
8
+ export async function editCommand(options) {
9
+ const { root } = options;
10
+ const encryptedPath = join(root, '.env.encrypted');
11
+ // Check encrypted file exists
12
+ if (!existsSync(encryptedPath)) {
13
+ console.error(pc.red(`Error: ${encryptedPath} not found`));
14
+ console.error(pc.dim('Create it with: pnpm secrets encrypt (after creating .env)'));
15
+ process.exit(1);
16
+ }
17
+ console.log(pc.blue('Opening encrypted file in editor...'));
18
+ console.log(pc.dim(' (Changes will be encrypted on save)'));
19
+ try {
20
+ sopsEdit(encryptedPath);
21
+ console.log(pc.green('\nEdit complete'));
22
+ console.log(pc.dim(' Run "pnpm secrets decrypt" to update local env files.'));
23
+ }
24
+ catch (error) {
25
+ console.error(pc.red(error.message));
26
+ process.exit(1);
27
+ }
28
+ }
@@ -0,0 +1,9 @@
1
+ interface EncryptOptions {
2
+ root: string;
3
+ }
4
+ /**
5
+ * Encrypt command - encrypt .env to .env.encrypted
6
+ */
7
+ export declare function encryptCommand(options: EncryptOptions): Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=encrypt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../src/commands/encrypt.ts"],"names":[],"mappings":"AAKA,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3E"}
@@ -0,0 +1,30 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { encrypt as sopsEncrypt } from '../core/sops.js';
5
+ /**
6
+ * Encrypt command - encrypt .env to .env.encrypted
7
+ */
8
+ export async function encryptCommand(options) {
9
+ const { root } = options;
10
+ const inputPath = join(root, '.env');
11
+ const outputPath = join(root, '.env.encrypted');
12
+ // Check input file exists
13
+ if (!existsSync(inputPath)) {
14
+ console.error(pc.red(`Error: ${inputPath} not found`));
15
+ console.error(pc.dim('Create a .env file first, then run encrypt.'));
16
+ process.exit(1);
17
+ }
18
+ console.log(pc.blue('Encrypting secrets...'));
19
+ console.log(pc.dim(` Input: .env`));
20
+ console.log(pc.dim(` Output: .env.encrypted`));
21
+ try {
22
+ sopsEncrypt(inputPath, outputPath);
23
+ console.log(pc.green('\nEncryption complete'));
24
+ console.log(pc.dim(' You can now commit .env.encrypted to git.'));
25
+ }
26
+ catch (error) {
27
+ console.error(pc.red(error.message));
28
+ process.exit(1);
29
+ }
30
+ }
@@ -0,0 +1,10 @@
1
+ interface PushOptions {
2
+ root: string;
3
+ dryRun?: boolean;
4
+ }
5
+ /**
6
+ * Push command - push production secrets to Cloudflare Workers
7
+ */
8
+ export declare function pushCommand(options: PushOptions): Promise<void>;
9
+ export {};
10
+ //# sourceMappingURL=push.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/commands/push.ts"],"names":[],"mappings":"AAaA,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA0CD;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqErE"}
@@ -0,0 +1,101 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import pc from 'picocolors';
5
+ import { discoverPackages } from '../core/discover.js';
6
+ import { expandVariables, getVarsForEnvironment, parseEnvContent, } from '../core/parse.js';
7
+ import { decrypt as sopsDecrypt } from '../core/sops.js';
8
+ /**
9
+ * Filter variables that should be pushed to Wrangler
10
+ * Excludes EXPO_PUBLIC_* vars
11
+ */
12
+ function getWranglerVars(vars) {
13
+ return vars.filter((v) => !v.key.startsWith('EXPO_PUBLIC_'));
14
+ }
15
+ /**
16
+ * Push a secret to Wrangler
17
+ */
18
+ function pushSecret(key, value, wranglerDir, dryRun) {
19
+ if (dryRun) {
20
+ console.log(pc.dim(` [dry-run] Would push: ${key}`));
21
+ return true;
22
+ }
23
+ try {
24
+ // Use echo to pipe the value to wrangler secret put
25
+ // This avoids the interactive prompt
26
+ execSync(`echo "${value}" | wrangler secret put ${key}`, {
27
+ cwd: wranglerDir,
28
+ stdio: ['pipe', 'pipe', 'pipe'],
29
+ shell: '/bin/bash',
30
+ });
31
+ return true;
32
+ }
33
+ catch (error) {
34
+ const err = error;
35
+ console.error(pc.red(` Failed to push ${key}: ${err.stderr || err.message}`));
36
+ return false;
37
+ }
38
+ }
39
+ /**
40
+ * Push command - push production secrets to Cloudflare Workers
41
+ */
42
+ export async function pushCommand(options) {
43
+ const { root, dryRun = false } = options;
44
+ const encryptedPath = join(root, '.env.encrypted');
45
+ // Check encrypted file exists
46
+ if (!existsSync(encryptedPath)) {
47
+ console.error(pc.red(`Error: ${encryptedPath} not found`));
48
+ process.exit(1);
49
+ }
50
+ // Find wrangler packages
51
+ const packages = await discoverPackages(root);
52
+ const wranglerPackages = packages.filter((p) => p.style === 'wrangler');
53
+ if (wranglerPackages.length === 0) {
54
+ console.error(pc.red('Error: No Wrangler packages found'));
55
+ console.error(pc.dim('A Wrangler package must have a wrangler.toml file.'));
56
+ process.exit(1);
57
+ }
58
+ console.log(pc.blue('Pushing secrets to Cloudflare Workers...'));
59
+ if (dryRun) {
60
+ console.log(pc.yellow(' (dry-run mode - no changes will be made)'));
61
+ }
62
+ // Decrypt and get production vars
63
+ let decryptedContent;
64
+ try {
65
+ decryptedContent = sopsDecrypt(encryptedPath);
66
+ }
67
+ catch (error) {
68
+ console.error(pc.red(error.message));
69
+ process.exit(1);
70
+ }
71
+ const allVars = parseEnvContent(decryptedContent);
72
+ const prodVars = getVarsForEnvironment(allVars, 'prod');
73
+ const expandedVars = expandVariables(prodVars);
74
+ const wranglerVars = getWranglerVars(expandedVars);
75
+ console.log(pc.dim(` ${wranglerVars.length} secrets to push`));
76
+ // Push to each wrangler package
77
+ for (const pkg of wranglerPackages) {
78
+ const pkgDir = pkg.path ? join(root, pkg.path) : root;
79
+ console.log(pc.blue(`\n${pkg.path || '.'} (${pkg.name}):`));
80
+ let successCount = 0;
81
+ let failCount = 0;
82
+ for (const v of wranglerVars) {
83
+ const success = pushSecret(v.key, v.value, pkgDir, dryRun);
84
+ if (success) {
85
+ console.log(pc.green(` ${v.key}`));
86
+ successCount++;
87
+ }
88
+ else {
89
+ failCount++;
90
+ }
91
+ }
92
+ console.log(pc.dim(` ${successCount} pushed, ${failCount} failed`));
93
+ }
94
+ if (dryRun) {
95
+ console.log(pc.yellow('\n[dry-run] No secrets were actually pushed.'));
96
+ console.log(pc.dim('Run without --dry-run to push secrets.'));
97
+ }
98
+ else {
99
+ console.log(pc.green('\nPush complete'));
100
+ }
101
+ }
@@ -0,0 +1,9 @@
1
+ interface StatusOptions {
2
+ root: string;
3
+ }
4
+ /**
5
+ * Status command - show discovered packages and their styles
6
+ */
7
+ export declare function statusCommand(options: StatusOptions): Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAMA,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6EzE"}
@@ -0,0 +1,58 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { discoverPackages } from '../core/discover.js';
5
+ import { isSopsInstalled } from '../core/sops.js';
6
+ /**
7
+ * Status command - show discovered packages and their styles
8
+ */
9
+ export async function statusCommand(options) {
10
+ const { root } = options;
11
+ console.log(pc.blue('Secrets Status\n'));
12
+ // Check prerequisites
13
+ console.log(pc.bold('Prerequisites:'));
14
+ const sopsInstalled = isSopsInstalled();
15
+ console.log(sopsInstalled
16
+ ? pc.green(' SOPS installed')
17
+ : pc.red(' SOPS not installed (brew install sops)'));
18
+ const ageKeyPath = join(process.env.HOME || '~', '.config/sops/age/key.txt');
19
+ const ageKeyExists = existsSync(ageKeyPath);
20
+ console.log(ageKeyExists
21
+ ? pc.green(' age key found')
22
+ : pc.yellow(' age key not found at ~/.config/sops/age/key.txt'));
23
+ // Check files
24
+ console.log(pc.bold('\nFiles:'));
25
+ const encryptedPath = join(root, '.env.encrypted');
26
+ const encryptedExists = existsSync(encryptedPath);
27
+ console.log(encryptedExists
28
+ ? pc.green(' .env.encrypted exists')
29
+ : pc.yellow(' .env.encrypted not found'));
30
+ const localPath = join(root, '.env.local');
31
+ const localExists = existsSync(localPath);
32
+ console.log(localExists
33
+ ? pc.green(' .env.local exists (local overrides)')
34
+ : pc.dim(' - .env.local not found (optional)'));
35
+ const sopsConfigPath = join(root, '.sops.yaml');
36
+ const sopsConfigExists = existsSync(sopsConfigPath);
37
+ console.log(sopsConfigExists
38
+ ? pc.green(' .sops.yaml exists')
39
+ : pc.yellow(' .sops.yaml not found (SOPS config)'));
40
+ // Discover packages
41
+ console.log(pc.bold('\nDiscovered Packages:'));
42
+ const packages = await discoverPackages(root);
43
+ if (packages.length === 0) {
44
+ console.log(pc.dim(' No packages found'));
45
+ }
46
+ else {
47
+ for (const pkg of packages) {
48
+ const styleColor = pkg.style === 'wrangler' ? pc.cyan : pc.magenta;
49
+ const output = pkg.style === 'wrangler'
50
+ ? '.dev.vars'
51
+ : '.env.development, .env.production';
52
+ console.log(` ${pkg.path || '.'} ` +
53
+ styleColor(`(${pkg.style})`) +
54
+ pc.dim(` -> ${output}`));
55
+ }
56
+ }
57
+ console.log('');
58
+ }
@@ -0,0 +1,6 @@
1
+ import type { Package } from '../types.js';
2
+ /**
3
+ * Discover all packages in the monorepo
4
+ */
5
+ export declare function discoverPackages(root: string): Promise<Package[]>;
6
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/core/discover.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,aAAa,CAAC;AAkDzD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAmCvE"}
@@ -0,0 +1,81 @@
1
+ import { glob } from 'glob';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ /**
5
+ * Patterns to ignore when discovering packages
6
+ */
7
+ const IGNORE_PATTERNS = [
8
+ '**/node_modules/**',
9
+ '**/.git/**',
10
+ '**/dist/**',
11
+ '**/build/**',
12
+ '**/.turbo/**',
13
+ '**/.next/**',
14
+ '**/coverage/**',
15
+ '**/.expo/**',
16
+ ];
17
+ /**
18
+ * Detect the style for a package directory
19
+ * - If wrangler.toml exists -> wrangler
20
+ * - Otherwise -> standard
21
+ */
22
+ function detectStyle(packageDir) {
23
+ const wranglerPath = join(packageDir, 'wrangler.toml');
24
+ const wranglerDevPath = join(packageDir, 'wrangler.dev.toml');
25
+ if (existsSync(wranglerPath) || existsSync(wranglerDevPath)) {
26
+ const configPath = existsSync(wranglerPath) ? wranglerPath : wranglerDevPath;
27
+ const content = readFileSync(configPath, 'utf-8');
28
+ if (content.includes('pages_build_output_dir')) {
29
+ return 'standard';
30
+ }
31
+ return 'wrangler';
32
+ }
33
+ return 'standard';
34
+ }
35
+ /**
36
+ * Read package name from package.json
37
+ */
38
+ function readPackageName(packageJsonPath) {
39
+ try {
40
+ const content = readFileSync(packageJsonPath, 'utf-8');
41
+ const pkg = JSON.parse(content);
42
+ return pkg.name || dirname(packageJsonPath);
43
+ }
44
+ catch {
45
+ return dirname(packageJsonPath);
46
+ }
47
+ }
48
+ /**
49
+ * Discover all packages in the monorepo
50
+ */
51
+ export async function discoverPackages(root) {
52
+ // Find all package.json files
53
+ const packageJsonPaths = await glob('**/package.json', {
54
+ cwd: root,
55
+ ignore: IGNORE_PATTERNS,
56
+ });
57
+ const packages = [];
58
+ for (const pkgPath of packageJsonPaths) {
59
+ const dir = dirname(pkgPath);
60
+ const fullDir = join(root, dir);
61
+ const style = detectStyle(fullDir);
62
+ const name = readPackageName(join(root, pkgPath));
63
+ if (name === '@chriscode/hush') {
64
+ continue;
65
+ }
66
+ packages.push({
67
+ name,
68
+ path: dir === '.' ? '' : dir,
69
+ style,
70
+ });
71
+ }
72
+ // Sort by path depth (root first, then alphabetically)
73
+ packages.sort((a, b) => {
74
+ const depthA = a.path.split('/').length;
75
+ const depthB = b.path.split('/').length;
76
+ if (depthA !== depthB)
77
+ return depthA - depthB;
78
+ return a.path.localeCompare(b.path);
79
+ });
80
+ return packages;
81
+ }
@@ -0,0 +1,30 @@
1
+ import type { EnvVar, Environment } from '../types.js';
2
+ /**
3
+ * Parse a .env file content into key-value pairs
4
+ */
5
+ export declare function parseEnvContent(content: string): EnvVar[];
6
+ /**
7
+ * Parse a .env file from disk
8
+ */
9
+ export declare function parseEnvFile(filePath: string): EnvVar[];
10
+ /**
11
+ * Filter and transform variables for a specific environment
12
+ * - Includes shared vars (no prefix)
13
+ * - Includes vars with matching prefix (stripped)
14
+ * - Excludes vars with other prefix
15
+ */
16
+ export declare function getVarsForEnvironment(vars: EnvVar[], env: Environment): EnvVar[];
17
+ /**
18
+ * Expand variable references in env vars
19
+ * e.g., ${OTHER_VAR} gets replaced with its value
20
+ */
21
+ export declare function expandVariables(vars: EnvVar[]): EnvVar[];
22
+ /**
23
+ * Merge multiple env var arrays, later arrays override earlier ones
24
+ */
25
+ export declare function mergeEnvVars(...varArrays: EnvVar[][]): EnvVar[];
26
+ /**
27
+ * Format env vars as .env file content
28
+ */
29
+ export declare function formatEnvFile(vars: EnvVar[]): string;
30
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/core/parse.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAGvD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA+BzD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAOvD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,WAAW,GACf,MAAM,EAAE,CA0BV;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAkBxD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,SAAS,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,CAU/D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,108 @@
1
+ import { expand } from 'dotenv-expand';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { ENV_PREFIXES } from '../types.js';
4
+ /**
5
+ * Parse a .env file content into key-value pairs
6
+ */
7
+ export function parseEnvContent(content) {
8
+ const vars = [];
9
+ const lines = content.split('\n');
10
+ for (const line of lines) {
11
+ const trimmed = line.trim();
12
+ // Skip empty lines and comments
13
+ if (!trimmed || trimmed.startsWith('#')) {
14
+ continue;
15
+ }
16
+ // Find the first = sign
17
+ const eqIndex = trimmed.indexOf('=');
18
+ if (eqIndex === -1)
19
+ continue;
20
+ const key = trimmed.slice(0, eqIndex).trim();
21
+ let value = trimmed.slice(eqIndex + 1).trim();
22
+ // Remove surrounding quotes if present
23
+ if ((value.startsWith('"') && value.endsWith('"')) ||
24
+ (value.startsWith("'") && value.endsWith("'"))) {
25
+ value = value.slice(1, -1);
26
+ }
27
+ vars.push({ key, value });
28
+ }
29
+ return vars;
30
+ }
31
+ /**
32
+ * Parse a .env file from disk
33
+ */
34
+ export function parseEnvFile(filePath) {
35
+ if (!existsSync(filePath)) {
36
+ return [];
37
+ }
38
+ const content = readFileSync(filePath, 'utf-8');
39
+ return parseEnvContent(content);
40
+ }
41
+ /**
42
+ * Filter and transform variables for a specific environment
43
+ * - Includes shared vars (no prefix)
44
+ * - Includes vars with matching prefix (stripped)
45
+ * - Excludes vars with other prefix
46
+ */
47
+ export function getVarsForEnvironment(vars, env) {
48
+ const prefix = ENV_PREFIXES[env];
49
+ const otherPrefix = env === 'dev' ? ENV_PREFIXES.prod : ENV_PREFIXES.dev;
50
+ const result = [];
51
+ for (const v of vars) {
52
+ // Skip vars with other environment's prefix
53
+ if (v.key.startsWith(otherPrefix)) {
54
+ continue;
55
+ }
56
+ // Strip prefix if it matches this environment
57
+ if (v.key.startsWith(prefix)) {
58
+ result.push({
59
+ key: v.key.slice(prefix.length),
60
+ value: v.value,
61
+ originalKey: v.key,
62
+ });
63
+ }
64
+ else {
65
+ // Shared var (no prefix)
66
+ result.push(v);
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+ /**
72
+ * Expand variable references in env vars
73
+ * e.g., ${OTHER_VAR} gets replaced with its value
74
+ */
75
+ export function expandVariables(vars) {
76
+ // Create a process.env-like object for expansion
77
+ const envObject = {};
78
+ for (const v of vars) {
79
+ envObject[v.key] = v.value;
80
+ }
81
+ // Use dotenv-expand with processEnv set to empty object to avoid mixing with process.env
82
+ const expanded = expand({ parsed: envObject, processEnv: {} });
83
+ if (!expanded.parsed) {
84
+ return vars;
85
+ }
86
+ return vars.map((v) => ({
87
+ ...v,
88
+ value: expanded.parsed[v.key] ?? v.value,
89
+ }));
90
+ }
91
+ /**
92
+ * Merge multiple env var arrays, later arrays override earlier ones
93
+ */
94
+ export function mergeEnvVars(...varArrays) {
95
+ const merged = new Map();
96
+ for (const vars of varArrays) {
97
+ for (const v of vars) {
98
+ merged.set(v.key, v);
99
+ }
100
+ }
101
+ return Array.from(merged.values());
102
+ }
103
+ /**
104
+ * Format env vars as .env file content
105
+ */
106
+ export function formatEnvFile(vars) {
107
+ return vars.map((v) => `${v.key}=${v.value}`).join('\n') + '\n';
108
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Check if SOPS is installed
3
+ */
4
+ export declare function isSopsInstalled(): boolean;
5
+ /**
6
+ * Decrypt a SOPS-encrypted file and return the content
7
+ */
8
+ export declare function decrypt(filePath: string): string;
9
+ /**
10
+ * Encrypt content to a SOPS-encrypted file
11
+ */
12
+ export declare function encrypt(inputPath: string, outputPath: string): void;
13
+ /**
14
+ * Open encrypted file in editor (SOPS inline edit)
15
+ */
16
+ export declare function edit(filePath: string): void;
17
+ //# sourceMappingURL=sops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sops.d.ts","sourceRoot":"","sources":["../../src/core/sops.ts"],"names":[],"mappings":"AAgCA;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAOzC;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+BhD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAwBnE;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAuB3C"}
@@ -0,0 +1,112 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Get the SOPS age key file path
6
+ * Checks SOPS_AGE_KEY_FILE env var, then falls back to default location
7
+ */
8
+ function getAgeKeyFile() {
9
+ if (process.env.SOPS_AGE_KEY_FILE) {
10
+ return process.env.SOPS_AGE_KEY_FILE;
11
+ }
12
+ const defaultPath = join(process.env.HOME || '~', '.config/sops/age/key.txt');
13
+ if (existsSync(defaultPath)) {
14
+ return defaultPath;
15
+ }
16
+ return undefined;
17
+ }
18
+ /**
19
+ * Get environment variables for SOPS commands
20
+ */
21
+ function getSopsEnv() {
22
+ const ageKeyFile = getAgeKeyFile();
23
+ if (ageKeyFile) {
24
+ return { ...process.env, SOPS_AGE_KEY_FILE: ageKeyFile };
25
+ }
26
+ return process.env;
27
+ }
28
+ /**
29
+ * Check if SOPS is installed
30
+ */
31
+ export function isSopsInstalled() {
32
+ try {
33
+ execSync('which sops', { stdio: 'ignore' });
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * Decrypt a SOPS-encrypted file and return the content
42
+ */
43
+ export function decrypt(filePath) {
44
+ if (!existsSync(filePath)) {
45
+ throw new Error(`Encrypted file not found: ${filePath}`);
46
+ }
47
+ if (!isSopsInstalled()) {
48
+ throw new Error('SOPS is not installed. Install with: brew install sops');
49
+ }
50
+ try {
51
+ // Use --input-type dotenv to handle .env format files
52
+ const result = execSync(`sops --input-type dotenv --output-type dotenv --decrypt "${filePath}"`, {
53
+ encoding: 'utf-8',
54
+ stdio: ['pipe', 'pipe', 'pipe'],
55
+ env: getSopsEnv(),
56
+ });
57
+ return result;
58
+ }
59
+ catch (error) {
60
+ const err = error;
61
+ if (err.stderr?.includes('No identity matched')) {
62
+ throw new Error('SOPS decryption failed: No matching age key found.\n' +
63
+ 'Ensure your age key is at ~/.config/sops/age/key.txt\n' +
64
+ 'Or set SOPS_AGE_KEY_FILE environment variable.');
65
+ }
66
+ throw new Error(`SOPS decryption failed: ${err.stderr || err.message}`);
67
+ }
68
+ }
69
+ /**
70
+ * Encrypt content to a SOPS-encrypted file
71
+ */
72
+ export function encrypt(inputPath, outputPath) {
73
+ if (!existsSync(inputPath)) {
74
+ throw new Error(`Input file not found: ${inputPath}`);
75
+ }
76
+ if (!isSopsInstalled()) {
77
+ throw new Error('SOPS is not installed. Install with: brew install sops');
78
+ }
79
+ try {
80
+ // Use --input-type dotenv to handle .env format files
81
+ execSync(`sops --input-type dotenv --output-type dotenv --encrypt "${inputPath}" > "${outputPath}"`, {
82
+ encoding: 'utf-8',
83
+ shell: '/bin/bash',
84
+ stdio: ['pipe', 'pipe', 'pipe'],
85
+ env: getSopsEnv(),
86
+ });
87
+ }
88
+ catch (error) {
89
+ const err = error;
90
+ throw new Error(`SOPS encryption failed: ${err.stderr || err.message}`);
91
+ }
92
+ }
93
+ /**
94
+ * Open encrypted file in editor (SOPS inline edit)
95
+ */
96
+ export function edit(filePath) {
97
+ if (!existsSync(filePath)) {
98
+ throw new Error(`Encrypted file not found: ${filePath}`);
99
+ }
100
+ if (!isSopsInstalled()) {
101
+ throw new Error('SOPS is not installed. Install with: brew install sops');
102
+ }
103
+ // Use spawnSync with inherit to allow interactive editing
104
+ // Specify input/output type for dotenv format
105
+ const result = spawnSync('sops', ['--input-type', 'dotenv', '--output-type', 'dotenv', filePath], {
106
+ stdio: 'inherit',
107
+ env: getSopsEnv(),
108
+ });
109
+ if (result.status !== 0) {
110
+ throw new Error(`SOPS edit failed with exit code ${result.status}`);
111
+ }
112
+ }
@@ -0,0 +1,11 @@
1
+ export { discoverPackages } from './core/discover.js';
2
+ export { expandVariables, formatEnvFile, getVarsForEnvironment, mergeEnvVars, parseEnvContent, parseEnvFile, } from './core/parse.js';
3
+ export { decrypt, edit, encrypt, isSopsInstalled } from './core/sops.js';
4
+ export { ENV_PREFIXES } from './types.js';
5
+ export type { EnvVar, Environment, Package, PackageStyle } from './types.js';
6
+ export { decryptCommand } from './commands/decrypt.js';
7
+ export { editCommand } from './commands/edit.js';
8
+ export { encryptCommand } from './commands/encrypt.js';
9
+ export { pushCommand } from './commands/push.js';
10
+ export { statusCommand } from './commands/status.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,eAAe,EACf,aAAa,EACb,qBAAqB,EACrB,YAAY,EACZ,eAAe,EACf,YAAY,GACb,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAGzE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG7E,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // Core exports for programmatic usage
2
+ export { discoverPackages } from './core/discover.js';
3
+ export { expandVariables, formatEnvFile, getVarsForEnvironment, mergeEnvVars, parseEnvContent, parseEnvFile, } from './core/parse.js';
4
+ export { decrypt, edit, encrypt, isSopsInstalled } from './core/sops.js';
5
+ // Types
6
+ export { ENV_PREFIXES } from './types.js';
7
+ // Commands (for programmatic usage)
8
+ export { decryptCommand } from './commands/decrypt.js';
9
+ export { editCommand } from './commands/edit.js';
10
+ export { encryptCommand } from './commands/encrypt.js';
11
+ export { pushCommand } from './commands/push.js';
12
+ export { statusCommand } from './commands/status.js';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Package style for env file output
3
+ * - wrangler: outputs .dev.vars (for Cloudflare Workers)
4
+ * - standard: outputs .env, .env.development, .env.production
5
+ */
6
+ export type PackageStyle = 'wrangler' | 'standard';
7
+ /**
8
+ * Discovered package in the monorepo
9
+ */
10
+ export interface Package {
11
+ /** Package name from package.json */
12
+ name: string;
13
+ /** Relative path from monorepo root */
14
+ path: string;
15
+ /** Detected style */
16
+ style: PackageStyle;
17
+ }
18
+ /**
19
+ * Parsed environment variable
20
+ */
21
+ export interface EnvVar {
22
+ key: string;
23
+ value: string;
24
+ /** Original key before prefix stripping */
25
+ originalKey?: string;
26
+ }
27
+ /**
28
+ * Environment type for prefix handling
29
+ */
30
+ export type Environment = 'dev' | 'prod';
31
+ /**
32
+ * Prefix constants
33
+ */
34
+ export declare const ENV_PREFIXES: {
35
+ readonly dev: "DEV__";
36
+ readonly prod: "PROD__";
37
+ };
38
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,UAAU,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,YAAY,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,CAAC;AAEzC;;GAEG;AACH,eAAO,MAAM,YAAY;;;CAGf,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Prefix constants
3
+ */
4
+ export const ENV_PREFIXES = {
5
+ dev: 'DEV__',
6
+ prod: 'PROD__',
7
+ };
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@chriscode/hush",
3
+ "version": "1.0.0",
4
+ "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
+ "type": "module",
6
+ "bin": {
7
+ "hush": "./bin/hush.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsc --watch",
18
+ "prepublishOnly": "pnpm build",
19
+ "type-check": "tsc --noEmit"
20
+ },
21
+ "keywords": [
22
+ "secrets",
23
+ "sops",
24
+ "age",
25
+ "encryption",
26
+ "env",
27
+ "dotenv",
28
+ "monorepo",
29
+ "cloudflare",
30
+ "wrangler",
31
+ "expo"
32
+ ],
33
+ "author": "Chris Hasson",
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/hassoncs/hush.git"
38
+ },
39
+ "bugs": {
40
+ "url": "https://github.com/hassoncs/hush/issues"
41
+ },
42
+ "homepage": "https://github.com/hassoncs/hush#readme",
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "dependencies": {
47
+ "dotenv": "^16.4.5",
48
+ "dotenv-expand": "^11.0.6",
49
+ "glob": "^10.3.10",
50
+ "picocolors": "^1.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.11.0",
54
+ "typescript": "^5.8.3"
55
+ },
56
+ "files": [
57
+ "dist",
58
+ "bin"
59
+ ],
60
+ "publishConfig": {
61
+ "access": "public"
62
+ }
63
+ }