@chriscode/hush 4.0.0 → 4.1.1

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/dist/cli.js CHANGED
@@ -72,12 +72,26 @@ ${pc.bold('Options:')}
72
72
  -h, --help Show this help message
73
73
  -v, --version Show version number
74
74
 
75
+ ${pc.bold('Variable Expansion (v4+):')}
76
+ Subdirectory .env files can reference root secrets:
77
+
78
+ \${VAR} Pull VAR from root secrets
79
+ \${VAR:-default} Pull VAR, use default if missing
80
+ \${env:VAR} Read from system environment (CI, etc.)
81
+
82
+ Example subdirectory template (apps/mobile/.env):
83
+ EXPO_PUBLIC_API_URL=\${API_URL}
84
+ PORT=\${PORT:-3000}
85
+
86
+ Subdirectory templates are safe to commit - they contain no secrets.
87
+
75
88
  ${pc.bold('Examples:')}
76
89
  hush init Initialize config + generate keys
77
90
  hush encrypt Encrypt .env files
78
91
  hush run -- npm start Run with secrets in memory (AI-safe!)
79
92
  hush run -e prod -- npm build Run with production secrets
80
93
  hush run -t api -- wrangler dev Run filtered for 'api' target
94
+ cd apps/mobile && hush run -- expo start Run from subdirectory with templates
81
95
  hush set DATABASE_URL Set a secret interactively (AI-safe)
82
96
  hush set API_KEY --gui Set secret via macOS dialog (for AI agents)
83
97
  hush set API_KEY -e prod Set a production secret
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AA4C/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFnE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAkD/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFnE"}
@@ -30,7 +30,11 @@ function getDecryptedSecrets(projectRoot, env, config) {
30
30
  varSources.push(parseEnvContent(content));
31
31
  }
32
32
  if (varSources.length === 0) {
33
- throw new Error(`No encrypted files found. Expected: ${sharedEncrypted}`);
33
+ throw new Error(`No encrypted files found at project root.\n` +
34
+ ` Expected: ${sharedEncrypted}\n` +
35
+ ` Project root: ${projectRoot}\n\n` +
36
+ ` If you haven't encrypted yet, run: npx hush encrypt\n` +
37
+ ` If running from a subdirectory, ensure hush.yaml exists at the project root.`);
34
38
  }
35
39
  const merged = mergeVars(...varSources);
36
40
  return interpolateVars(merged);
@@ -1 +1 @@
1
- {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAwK9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2M9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
@@ -1,10 +1,38 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, fstatSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { platform } from 'node:os';
5
5
  import pc from 'picocolors';
6
6
  import { loadConfig } from '../config/loader.js';
7
7
  import { setKey } from '../core/sops.js';
8
+ const STDIN_FD = 0;
9
+ function hasStdinPipe() {
10
+ try {
11
+ if (process.stdin.isTTY) {
12
+ return false;
13
+ }
14
+ const stat = fstatSync(STDIN_FD);
15
+ return stat.isFIFO() || stat.isFile();
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ function readFromStdinPipe() {
22
+ return new Promise((resolve, reject) => {
23
+ let data = '';
24
+ process.stdin.setEncoding('utf8');
25
+ process.stdin.on('data', (chunk) => {
26
+ data += chunk;
27
+ });
28
+ process.stdin.on('end', () => {
29
+ const trimTrailingNewlines = /\n+$/;
30
+ resolve(data.replace(trimTrailingNewlines, ''));
31
+ });
32
+ process.stdin.on('error', reject);
33
+ process.stdin.resume();
34
+ });
35
+ }
8
36
  function promptViaMacOSDialog(key) {
9
37
  try {
10
38
  const script = `display dialog "Enter value for ${key}:" default answer "" with hidden answer with title "Hush - Set Secret"`;
@@ -133,6 +161,9 @@ function promptViaTTY(key) {
133
161
  });
134
162
  }
135
163
  async function promptForValue(key, forceGui) {
164
+ if (hasStdinPipe()) {
165
+ return readFromStdinPipe();
166
+ }
136
167
  if (process.stdin.isTTY && !forceGui) {
137
168
  return promptViaTTY(key);
138
169
  }
@@ -1 +1 @@
1
- {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAqrChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
1
+ {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAu+ChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
@@ -12,9 +12,9 @@ allowed-tools: Bash(hush:*), Bash(npx hush:*), Bash(brew:*), Bash(npm:*), Bash(p
12
12
 
13
13
  # Hush - AI-Native Secrets Management
14
14
 
15
- **CRITICAL: NEVER read .env files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
15
+ **CRITICAL: NEVER read root .env files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
16
16
 
17
- Hush keeps secrets **encrypted at rest**. When properly set up, all secrets are stored in \`.env.encrypted\` files and plaintext \`.env\` files should NOT exist.
17
+ Hush keeps secrets **encrypted at rest** at the project root. Subdirectory \`.env\` files are **templates** (safe to commit and read) that reference root secrets via \`\${VAR}\` syntax.
18
18
 
19
19
  ## First Step: Investigate Current State
20
20
 
@@ -35,12 +35,13 @@ This tells you:
35
35
 
36
36
  | You See | What It Means | Action |
37
37
  |---------|---------------|--------|
38
- | \`SECURITY WARNING: Unencrypted .env files\` | Plaintext secrets exist! | Run \`npx hush encrypt\` immediately |
38
+ | \`SECURITY WARNING: Unencrypted .env files\` | Plaintext secrets at project root! | Run \`npx hush encrypt\` immediately |
39
39
  | \`No hush.yaml found\` | Hush not initialized | Run \`npx hush init\` |
40
40
  | \`SOPS not installed\` | Missing prerequisite | \`brew install sops\` |
41
41
  | \`age key not found\` | Missing encryption key | \`npx hush keys setup\` |
42
42
  | \`Project: not set\` | Key management limited | Add \`project:\` to hush.yaml |
43
- | \`1Password backup: not synced\` | Key not backed up | \`npx hush keys push\` |
43
+
44
+ **Note:** Security warnings only apply to root-level \`.env\` files. Subdirectory \`.env\` files are templates (safe to commit).
44
45
 
45
46
  ## Decision Tree: What Do I Do?
46
47
 
@@ -85,6 +86,122 @@ npx hush run -e production -- npm build # Production
85
86
 
86
87
  ---
87
88
 
89
+ ## Monorepo Architecture: Push vs Pull
90
+
91
+ Hush supports two ways to distribute secrets in monorepos. **Choose based on the use case:**
92
+
93
+ | Need | Use | Example |
94
+ |------|-----|---------|
95
+ | Pattern-based filtering | **Push** | "All \`NEXT_PUBLIC_*\` vars → web app" |
96
+ | Auto-flow new vars | **Push** | Add var at root, it flows automatically |
97
+ | Rename variables | **Pull** | \`API_URL\` → \`EXPO_PUBLIC_API_URL\` |
98
+ | Default values | **Pull** | \`PORT=\${PORT:-3000}\` |
99
+ | Combine variables | **Pull** | \`URL=\${HOST}:\${PORT}\` |
100
+
101
+ ### Push (include/exclude in hush.yaml)
102
+
103
+ Best for simple filtering where new vars should auto-flow:
104
+
105
+ \`\`\`yaml
106
+ # hush.yaml
107
+ targets:
108
+ - name: web
109
+ path: ./apps/web
110
+ include: [NEXT_PUBLIC_*] # All matching vars auto-flow
111
+ \`\`\`
112
+
113
+ ### Pull (subdirectory .env templates)
114
+
115
+ Best for transformation, renaming, or explicit dependencies:
116
+
117
+ \`\`\`bash
118
+ # apps/mobile/.env (committed - it's just a template)
119
+ EXPO_PUBLIC_API_URL=\${API_URL} # Rename from root
120
+ PORT=\${PORT:-8081} # Default value
121
+ \`\`\`
122
+
123
+ **Decision rule:** Use push for "all X goes to Y" patterns. Use pull when you need to rename, transform, or add defaults.
124
+
125
+ ---
126
+
127
+ ## Subdirectory Templates (Pull-Based)
128
+
129
+ When a subdirectory needs to rename, transform, or add defaults to root secrets, create a \`.env\` template file in that subdirectory.
130
+
131
+ ### Step-by-Step Setup
132
+
133
+ **1. Ensure root secrets exist:**
134
+ \`\`\`bash
135
+ npx hush inspect # From repo root - verify secrets are configured
136
+ \`\`\`
137
+
138
+ **2. Create subdirectory template (this file is committed to git):**
139
+ \`\`\`bash
140
+ # apps/mobile/.env
141
+ EXPO_PUBLIC_API_URL=\${API_URL} # Pull API_URL from root, rename it
142
+ EXPO_PUBLIC_STRIPE_KEY=\${STRIPE_KEY} # Pull and rename
143
+ PORT=\${PORT:-8081} # Use root PORT, or default to 8081
144
+ DEBUG=\${DEBUG:-false} # Use root DEBUG, or default to false
145
+ \`\`\`
146
+
147
+ **3. Run from the subdirectory:**
148
+ \`\`\`bash
149
+ cd apps/mobile
150
+ npx hush run -- npm start
151
+ \`\`\`
152
+
153
+ Hush automatically:
154
+ 1. Finds the project root (where \`hush.yaml\` is)
155
+ 2. Decrypts root secrets
156
+ 3. Loads the local \`.env\` template
157
+ 4. Resolves \`\${VAR}\` references against root secrets
158
+ 5. Injects the result into your command
159
+
160
+ ### Variable Expansion Syntax
161
+
162
+ | Syntax | Meaning | Example |
163
+ |--------|---------|---------|
164
+ | \`\${VAR}\` | Pull VAR from root secrets | \`API_URL=\${API_URL}\` |
165
+ | \`\${VAR:-default}\` | Pull VAR, use default if missing/empty | \`PORT=\${PORT:-3000}\` |
166
+ | \`\${env:VAR}\` | Read from system environment (CI, etc.) | \`CI=\${env:CI}\` |
167
+
168
+ ### Common Patterns
169
+
170
+ **Expo/React Native app:**
171
+ \`\`\`bash
172
+ # apps/mobile/.env
173
+ EXPO_PUBLIC_API_URL=\${API_URL}
174
+ EXPO_PUBLIC_STRIPE_KEY=\${STRIPE_PUBLISHABLE_KEY}
175
+ EXPO_PUBLIC_ENV=\${ENV:-development}
176
+ \`\`\`
177
+
178
+ **Next.js app:**
179
+ \`\`\`bash
180
+ # apps/web/.env
181
+ NEXT_PUBLIC_API_URL=\${API_URL}
182
+ NEXT_PUBLIC_STRIPE_KEY=\${STRIPE_PUBLISHABLE_KEY}
183
+ DATABASE_URL=\${DATABASE_URL}
184
+ \`\`\`
185
+
186
+ **API server with defaults:**
187
+ \`\`\`bash
188
+ # apps/api/.env
189
+ DATABASE_URL=\${DATABASE_URL}
190
+ PORT=\${PORT:-8787}
191
+ LOG_LEVEL=\${LOG_LEVEL:-info}
192
+ \`\`\`
193
+
194
+ ### Important Notes
195
+
196
+ - **Subdirectory .env files ARE committed to git** - they're templates, not secrets
197
+ - **Can contain expansions AND constants** - \`APP_NAME=MyApp\` alongside \`API_URL=\${API_URL}\`
198
+ - **Run from the subdirectory** - \`hush run\` auto-detects the project root
199
+ - **Root secrets stay encrypted** - subdirectory templates just reference them
200
+ - **Self-reference works** - \`PORT=\${PORT:-3000}\` uses root PORT if set, else 3000
201
+ - **Security warnings only apply to root** - subdirectory .env files are always safe
202
+
203
+ ---
204
+
88
205
  ## Commands Quick Reference
89
206
 
90
207
  | Command | Purpose | When to Use |
@@ -196,6 +313,15 @@ npx hush set DEBUG --local # Set personal local override
196
313
  The user will be prompted to enter the value (hidden input).
197
314
  **You never see the actual secret - just invoke the command!**
198
315
 
316
+ ### Add a secret via pipe (for scripts/automation)
317
+
318
+ \`\`\`bash
319
+ echo "my-secret-value" | npx hush set MY_KEY
320
+ cat secret.txt | npx hush set CERT_CONTENT
321
+ \`\`\`
322
+
323
+ When stdin has piped data, Hush reads from it instead of prompting.
324
+
199
325
  ---
200
326
 
201
327
  ## Additional Resources
@@ -516,6 +642,12 @@ hush set DEBUG --local # Set personal local override
516
642
 
517
643
  User will be prompted with hidden input - the value is never visible.
518
644
 
645
+ **Pipe support:** You can also pipe values directly:
646
+ \`\`\`bash
647
+ echo "my-secret" | hush set MY_KEY
648
+ cat cert.pem | hush set CERTIFICATE
649
+ \`\`\`
650
+
519
651
  ---
520
652
 
521
653
  ### hush edit [file]
@@ -635,6 +767,43 @@ hush trace STRIPE_SECRET_KEY # Trace another variable
635
767
 
636
768
  ---
637
769
 
770
+ ### hush template
771
+
772
+ Show the resolved template for the current directory's \`.env\` file.
773
+
774
+ \`\`\`bash
775
+ cd apps/mobile
776
+ hush template # Show resolved expansions
777
+ hush template -e production # Show for production
778
+ \`\`\`
779
+
780
+ **Output shows:**
781
+ - Original template values (e.g., \`\${API_URL}\`)
782
+ - Resolved values from root secrets (masked)
783
+ - Any unresolved references
784
+
785
+ **Use when:** Debugging why a subdirectory template isn't resolving correctly
786
+
787
+ ---
788
+
789
+ ### hush expansions
790
+
791
+ Show the expansion graph across all subdirectories that have \`.env\` templates.
792
+
793
+ \`\`\`bash
794
+ hush expansions # Scan all subdirectories
795
+ hush expansions -e production # Show for production
796
+ \`\`\`
797
+
798
+ **Output shows:**
799
+ - Which subdirectories have \`.env\` templates
800
+ - What variables each template references from root
801
+ - Resolution status for each reference
802
+
803
+ **Use when:** Getting an overview of pull-based templates across a monorepo
804
+
805
+ ---
806
+
638
807
  ## Quick Reference
639
808
 
640
809
  | Command | Purpose |
@@ -645,7 +814,10 @@ hush trace STRIPE_SECRET_KEY # Trace another variable
645
814
  | \`hush inspect\` | See variables (masked) |
646
815
  | \`hush has <KEY>\` | Check if variable exists |
647
816
  | \`hush status\` | View configuration |
648
- | \`cat .env.encrypted\` | Read encrypted file (safe!) |
817
+ | \`hush resolve <target>\` | See what a target receives |
818
+ | \`hush trace <KEY>\` | Trace variable through targets |
819
+ | \`hush template\` | Show resolved subdirectory template |
820
+ | \`hush expansions\` | Show all subdirectory templates |
649
821
 
650
822
  ---
651
823
 
@@ -873,14 +1045,32 @@ DEBUG=\${DEBUG:-false}
873
1045
  PORT=\${PORT:-3000}
874
1046
 
875
1047
  # System environment (explicit opt-in)
876
- PATH=\${env:HOME}/.local/bin
1048
+ CI=\${env:CI}
877
1049
 
878
1050
  # Pull from root (subdirectory .env can reference root secrets)
879
- # apps/web/.env.development:
880
- DATABASE_URL=\${DATABASE_URL} # Inherited from root .env
1051
+ # apps/mobile/.env:
1052
+ EXPO_PUBLIC_API_URL=\${API_URL} # Renamed from root
881
1053
  \`\`\`
882
1054
 
883
- **Resolution order:** Local value → Parent directories → System env (only with \`env:\` prefix)
1055
+ **Resolution order:** Local value → Root secrets → System env (only with \`env:\` prefix)
1056
+
1057
+ ### Push vs Pull Architecture
1058
+
1059
+ **Push (hush.yaml targets):** Pattern-based filtering, auto-flow
1060
+ \`\`\`yaml
1061
+ targets:
1062
+ - name: web
1063
+ include: [NEXT_PUBLIC_*] # All matching vars flow automatically
1064
+ \`\`\`
1065
+
1066
+ **Pull (subdirectory templates):** Transformation, renaming, defaults
1067
+ \`\`\`bash
1068
+ # apps/mobile/.env
1069
+ EXPO_PUBLIC_API_URL=\${API_URL} # Rename required
1070
+ PORT=\${PORT:-3000} # Default value
1071
+ \`\`\`
1072
+
1073
+ **Decision:** Use push for "all X → Y". Use pull for rename/transform/defaults.
884
1074
  `,
885
1075
  'examples/workflows.md': `# Hush Workflow Examples
886
1076
 
@@ -1110,6 +1300,122 @@ npx hush push
1110
1300
 
1111
1301
  ---
1112
1302
 
1303
+ ## Setting Up Subdirectory Templates (Pull-Based Secrets)
1304
+
1305
+ ### "Set up secrets for a subdirectory app (Expo, Next.js, etc.)"
1306
+
1307
+ **Use this when:** You need to rename, transform, or add defaults to root secrets for a specific package.
1308
+
1309
+ **Step 1: Verify root secrets exist**
1310
+ \`\`\`bash
1311
+ cd /path/to/repo/root
1312
+ npx hush inspect
1313
+ \`\`\`
1314
+
1315
+ **Step 2: Create the subdirectory template file**
1316
+
1317
+ Create a \`.env\` file in the subdirectory. This file is committed to git - it's just a template, not actual secrets.
1318
+
1319
+ \`\`\`bash
1320
+ # Example: apps/mobile/.env
1321
+ EXPO_PUBLIC_API_URL=\${API_URL} # Pulls API_URL from root, renames it
1322
+ EXPO_PUBLIC_STRIPE_KEY=\${STRIPE_KEY} # Pulls and renames
1323
+ PORT=\${PORT:-8081} # Uses root PORT if set, otherwise 8081
1324
+ DEBUG=\${DEBUG:-false} # Uses root DEBUG if set, otherwise false
1325
+ \`\`\`
1326
+
1327
+ **Step 3: Run from the subdirectory**
1328
+ \`\`\`bash
1329
+ cd apps/mobile
1330
+ npx hush run -- npm start
1331
+ \`\`\`
1332
+
1333
+ ### Variable Expansion Syntax Reference
1334
+
1335
+ | Syntax | What It Does | Example |
1336
+ |--------|--------------|---------|
1337
+ | \`\${VAR}\` | Pull VAR from root secrets | \`API_URL=\${API_URL}\` |
1338
+ | \`\${VAR:-default}\` | Pull VAR, use default if not set | \`PORT=\${PORT:-3000}\` |
1339
+ | \`\${env:VAR}\` | Read from system environment | \`CI=\${env:CI}\` |
1340
+
1341
+ ### Framework Examples
1342
+
1343
+ **Expo/React Native:**
1344
+ \`\`\`bash
1345
+ # apps/mobile/.env
1346
+ EXPO_PUBLIC_API_URL=\${API_URL}
1347
+ EXPO_PUBLIC_STRIPE_KEY=\${STRIPE_PUBLISHABLE_KEY}
1348
+ EXPO_PUBLIC_ENV=\${ENV:-development}
1349
+ \`\`\`
1350
+
1351
+ **Next.js:**
1352
+ \`\`\`bash
1353
+ # apps/web/.env
1354
+ NEXT_PUBLIC_API_URL=\${API_URL}
1355
+ NEXT_PUBLIC_STRIPE_KEY=\${STRIPE_PUBLISHABLE_KEY}
1356
+ DATABASE_URL=\${DATABASE_URL}
1357
+ \`\`\`
1358
+
1359
+ **Cloudflare Worker:**
1360
+ \`\`\`bash
1361
+ # apps/api/.env
1362
+ DATABASE_URL=\${DATABASE_URL}
1363
+ STRIPE_SECRET_KEY=\${STRIPE_SECRET_KEY}
1364
+ PORT=\${PORT:-8787}
1365
+ \`\`\`
1366
+
1367
+ ### Important Notes
1368
+
1369
+ - **Template files ARE committed** to git (they contain no secrets)
1370
+ - **Root secrets stay encrypted** - templates just reference them
1371
+ - **Run from subdirectory** - \`hush run\` finds the project root automatically
1372
+ - **Self-reference works** - \`PORT=\${PORT:-3000}\` uses root PORT if set
1373
+
1374
+ ---
1375
+
1376
+ ## Choosing Push vs Pull (Monorepos)
1377
+
1378
+ ### "How should I set up secrets for a new package?"
1379
+
1380
+ **Ask yourself:** Does this package need to rename variables or add defaults?
1381
+
1382
+ #### If NO (simple filtering) → Use Push
1383
+
1384
+ Edit \`hush.yaml\` to add a target:
1385
+ \`\`\`yaml
1386
+ targets:
1387
+ - name: new-package
1388
+ path: ./packages/new-package
1389
+ format: dotenv
1390
+ include:
1391
+ - NEXT_PUBLIC_* # Or whatever pattern fits
1392
+ \`\`\`
1393
+
1394
+ **Benefits:** New \`NEXT_PUBLIC_*\` vars at root auto-flow. Zero maintenance.
1395
+
1396
+ #### If YES (transformation needed) → Use Pull
1397
+
1398
+ Create a template \`.env\` in the package:
1399
+ \`\`\`bash
1400
+ # packages/mobile/.env (committed to git)
1401
+ EXPO_PUBLIC_API_URL=\${API_URL} # Rename from root
1402
+ EXPO_PUBLIC_DEBUG=\${DEBUG:-false} # With default
1403
+ PORT=\${PORT:-8081} # Local default
1404
+ \`\`\`
1405
+
1406
+ **Benefits:** Full control over naming and defaults. Explicit dependencies.
1407
+
1408
+ ### "When do I update templates vs hush.yaml?"
1409
+
1410
+ | Scenario | Update |
1411
+ |----------|--------|
1412
+ | New \`NEXT_PUBLIC_*\` var, web uses push | Nothing! Auto-flows |
1413
+ | New var mobile needs, mobile uses pull | \`packages/mobile/.env\` template |
1414
+ | New package needs secrets | \`hush.yaml\` (push) or new template (pull) |
1415
+ | Change var routing | \`hush.yaml\` include/exclude patterns |
1416
+
1417
+ ---
1418
+
1113
1419
  ## Understanding the Output
1114
1420
 
1115
1421
  ### npx hush status output explained
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA8DjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAyHzE"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwCjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHzE"}
@@ -1,45 +1,21 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import pc from 'picocolors';
4
4
  import { findConfigPath, loadConfig } from '../config/loader.js';
5
5
  import { describeFilter } from '../core/filter.js';
6
6
  import { isAgeKeyConfigured, isSopsInstalled } from '../core/sops.js';
7
7
  import { keyExists } from '../lib/age.js';
8
- import { opAvailable, opListKeys } from '../lib/onepassword.js';
8
+ import { opInstalled } from '../lib/onepassword.js';
9
9
  import { FORMAT_OUTPUT_FILES } from '../types.js';
10
- function findPlaintextEnvFiles(root) {
10
+ function findRootPlaintextEnvFiles(root) {
11
11
  const results = [];
12
12
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
13
- const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
14
- function scanDir(dir, relativePath = '') {
15
- let entries;
16
- try {
17
- entries = readdirSync(dir);
18
- }
19
- catch {
20
- return;
21
- }
22
- for (const entry of entries) {
23
- if (skipDirs.has(entry))
24
- continue;
25
- if (entry.endsWith('.encrypted'))
26
- continue;
27
- const fullPath = join(dir, entry);
28
- const relPath = relativePath ? `${relativePath}/${entry}` : entry;
29
- try {
30
- if (statSync(fullPath).isDirectory()) {
31
- scanDir(fullPath, relPath);
32
- }
33
- else if (plaintextPatterns.includes(entry)) {
34
- results.push(relPath);
35
- }
36
- }
37
- catch {
38
- continue;
39
- }
13
+ for (const pattern of plaintextPatterns) {
14
+ const filePath = join(root, pattern);
15
+ if (existsSync(filePath)) {
16
+ results.push(pattern);
40
17
  }
41
18
  }
42
- scanDir(root);
43
19
  return results;
44
20
  }
45
21
  function getProjectFromConfig(root) {
@@ -72,10 +48,10 @@ export async function statusCommand(options) {
72
48
  const config = loadConfig(root);
73
49
  const configPath = findConfigPath(root);
74
50
  console.log(pc.blue('Hush Status\n'));
75
- const plaintextFiles = findPlaintextEnvFiles(root);
51
+ const plaintextFiles = findRootPlaintextEnvFiles(root);
76
52
  if (plaintextFiles.length > 0) {
77
53
  console.log(pc.bgRed(pc.white(pc.bold(' SECURITY WARNING '))));
78
- console.log(pc.red(pc.bold('\nUnencrypted .env files detected!\n')));
54
+ console.log(pc.red(pc.bold('\nUnencrypted .env files detected at project root!\n')));
79
55
  for (const file of plaintextFiles) {
80
56
  console.log(pc.red(` ${file}`));
81
57
  }
@@ -112,21 +88,16 @@ export async function statusCommand(options) {
112
88
  : pc.yellow(' age key not found at ~/.config/sops/age/key.txt'));
113
89
  if (project) {
114
90
  const hasLocalKey = keyExists(project);
115
- const has1PasswordBackup = opAvailable() && opListKeys().includes(project);
116
91
  console.log(pc.bold('\nKey Status:'));
117
92
  console.log(hasLocalKey
118
93
  ? pc.green(` Local key: ~/.config/sops/age/keys/${project.replace(/\//g, '-')}.txt`)
119
94
  : pc.yellow(' Local key: not found'));
120
- if (opAvailable()) {
121
- console.log(has1PasswordBackup
122
- ? pc.green(' 1Password backup: synced')
123
- : pc.yellow(' 1Password backup: not synced'));
124
- if (!has1PasswordBackup && hasLocalKey) {
125
- console.log(pc.dim(' Run "npx hush keys push" to backup to 1Password'));
126
- }
95
+ if (opInstalled()) {
96
+ console.log(pc.dim(' 1Password CLI: installed'));
97
+ console.log(pc.dim(' Run "npx hush keys list" to check backup status'));
127
98
  }
128
99
  else {
129
- console.log(pc.dim(' 1Password CLI: not available'));
100
+ console.log(pc.dim(' 1Password CLI: not installed'));
130
101
  }
131
102
  if (!hasLocalKey) {
132
103
  console.log(pc.bold('\n To set up keys:'));
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK9C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ1D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAepG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAmBnD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAO5G;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CA8B3D"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK9C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ1D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAepG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAoBnD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAO5G;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CA8B3D"}
@@ -37,7 +37,8 @@ export function loadConfig(root) {
37
37
  const content = readFileSync(configPath, 'utf-8');
38
38
  const parsed = parseYaml(content);
39
39
  return {
40
- version: parsed.version,
40
+ // Support both 'version' and 'schema_version' (prefer schema_version)
41
+ version: parsed.schema_version ?? parsed.version,
41
42
  project: parsed.project,
42
43
  sources: { ...DEFAULT_SOURCES, ...parsed.sources },
43
44
  targets: parsed.targets ?? [{ name: 'root', path: '.', format: 'dotenv' }],
@@ -1,4 +1,5 @@
1
1
  export declare const OP_ITEM_PREFIX = "SOPS Key - hush/";
2
+ export declare function opInstalled(): boolean;
2
3
  export declare function opAvailable(): boolean;
3
4
  export declare function opGetKey(project: string, vault?: string): string | null;
4
5
  export declare function opStoreKey(project: string, privateKey: string, publicKey: string, vault?: string): void;
@@ -1 +1 @@
1
- {"version":3,"file":"onepassword.d.ts","sourceRoot":"","sources":["../../src/lib/onepassword.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,qBAAqB,CAAC;AAcjD,wBAAgB,WAAW,IAAI,OAAO,CAOrC;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASvE;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBvG;AAED,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAYnD"}
1
+ {"version":3,"file":"onepassword.d.ts","sourceRoot":"","sources":["../../src/lib/onepassword.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,qBAAqB,CAAC;AAUjD,wBAAgB,WAAW,IAAI,OAAO,CAOrC;AAED,wBAAgB,WAAW,IAAI,OAAO,CAOrC;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASvE;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBvG;AAED,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAYnD"}
@@ -1,9 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  export const OP_ITEM_PREFIX = 'SOPS Key - hush/';
3
- /**
4
- * 1Password CLI sessions don't persist across subprocesses, so we run
5
- * `op signin` before every command to trigger biometric auth.
6
- */
7
3
  function opExec(command) {
8
4
  return execSync(`op signin && ${command}`, {
9
5
  encoding: 'utf-8',
@@ -11,6 +7,15 @@ function opExec(command) {
11
7
  shell: '/bin/bash',
12
8
  });
13
9
  }
10
+ export function opInstalled() {
11
+ try {
12
+ execSync('which op', { encoding: 'utf-8', stdio: 'pipe' });
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
14
19
  export function opAvailable() {
15
20
  try {
16
21
  opExec('op whoami');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {