@chriscode/hush 4.1.1 → 4.2.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/dist/cli.js CHANGED
@@ -40,7 +40,7 @@ ${pc.bold('Commands:')}
40
40
  inspect List all variables (masked values, AI-safe)
41
41
  has <key> Check if a secret exists (exit 0 if set, 1 if not)
42
42
  check Verify secrets are encrypted (for pre-commit hooks)
43
- push Push secrets to Cloudflare Workers
43
+ push Push secrets to Cloudflare (Workers and Pages)
44
44
  status Show configuration and status
45
45
  skill Install Claude Code / OpenCode skill
46
46
  keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
@@ -57,7 +57,7 @@ ${pc.bold('Advanced Commands:')}
57
57
  ${pc.bold('Options:')}
58
58
  -e, --env <env> Environment: development or production (default: development)
59
59
  -r, --root <dir> Root directory (default: current directory)
60
- -t, --target <t> Target name from hush.yaml (run/resolve only)
60
+ -t, --target <t> Target name from hush.yaml (run/resolve/push)
61
61
  -q, --quiet Suppress output (has/check commands)
62
62
  --dry-run Preview changes without applying (push only)
63
63
  --verbose Show detailed output (push --dry-run only)
@@ -79,6 +79,11 @@ ${pc.bold('Variable Expansion (v4+):')}
79
79
  \${VAR:-default} Pull VAR, use default if missing
80
80
  \${env:VAR} Read from system environment (CI, etc.)
81
81
 
82
+ Behavior:
83
+ - Template vars are merged with target filters (additive)
84
+ - Template vars take precedence over target filters
85
+ - Subdirectory templates are safe to commit
86
+
82
87
  Example subdirectory template (apps/mobile/.env):
83
88
  EXPO_PUBLIC_API_URL=\${API_URL}
84
89
  PORT=\${PORT:-3000}
@@ -90,8 +95,8 @@ ${pc.bold('Examples:')}
90
95
  hush encrypt Encrypt .env files
91
96
  hush run -- npm start Run with secrets in memory (AI-safe!)
92
97
  hush run -e prod -- npm build Run with production secrets
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
98
+ hush run -t api -- wrangler dev Run filtered for 'api' target (root secrets only)
99
+ cd apps/mobile && hush run -- expo start Run from subdirectory (applies template + target filters)
95
100
  hush set DATABASE_URL Set a secret interactively (AI-safe)
96
101
  hush set API_KEY --gui Set secret via macOS dialog (for AI agents)
97
102
  hush set API_KEY -e prod Set a production secret
@@ -105,6 +110,7 @@ ${pc.bold('Examples:')}
105
110
  hush has API_KEY -q && echo "API_KEY is configured"
106
111
  hush check Verify secrets are encrypted
107
112
  hush push --dry-run Preview push to Cloudflare
113
+ hush push -t app Push only the 'app' target
108
114
  hush status Show current status
109
115
  hush skill Install Claude skill (interactive)
110
116
  `);
@@ -359,7 +365,7 @@ async function main() {
359
365
  await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
360
366
  break;
361
367
  case 'push':
362
- await pushCommand({ root, dryRun, verbose });
368
+ await pushCommand({ root, dryRun, verbose, target });
363
369
  break;
364
370
  case 'status':
365
371
  await statusCommand({ root });
@@ -1 +1 @@
1
- {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/commands/push.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;AA0BvD,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAwErE"}
1
+ {"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/commands/push.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAU,WAAW,EAAqC,MAAM,aAAa,CAAC;AAgH1F,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6GrE"}
@@ -1,5 +1,5 @@
1
1
  import { execSync } from 'node:child_process';
2
- import { existsSync } from 'node:fs';
2
+ import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import pc from 'picocolors';
5
5
  import { loadConfig } from '../config/loader.js';
@@ -8,7 +8,8 @@ import { interpolateVars } from '../core/interpolate.js';
8
8
  import { mergeVars } from '../core/merge.js';
9
9
  import { parseEnvContent } from '../core/parse.js';
10
10
  import { decrypt as sopsDecrypt } from '../core/sops.js';
11
- function pushSecret(key, value, targetDir, dryRun, verbose) {
11
+ import { loadLocalTemplates, resolveTemplateVars } from '../core/template.js';
12
+ function pushWorkerSecret(key, value, targetDir, dryRun, verbose) {
12
13
  if (dryRun) {
13
14
  if (verbose) {
14
15
  console.log(pc.green(` + ${key}`));
@@ -32,10 +33,80 @@ function pushSecret(key, value, targetDir, dryRun, verbose) {
32
33
  return false;
33
34
  }
34
35
  }
36
+ function pushPagesSecrets(vars, projectName, targetDir, dryRun, verbose) {
37
+ if (dryRun) {
38
+ for (const { key } of vars) {
39
+ if (verbose) {
40
+ console.log(pc.green(` + ${key}`));
41
+ }
42
+ else {
43
+ console.log(pc.dim(` [dry-run] ${key}`));
44
+ }
45
+ }
46
+ return { success: vars.length, failed: 0 };
47
+ }
48
+ const secretsJson = {};
49
+ for (const { key, value } of vars) {
50
+ secretsJson[key] = value;
51
+ }
52
+ const tempFile = join(targetDir, '.hush-secrets-temp.json');
53
+ try {
54
+ writeFileSync(tempFile, JSON.stringify(secretsJson, null, 2));
55
+ execSync(`wrangler pages secret bulk "${tempFile}" --project-name "${projectName}"`, {
56
+ cwd: targetDir,
57
+ stdio: ['pipe', 'pipe', 'pipe'],
58
+ shell: '/bin/bash',
59
+ });
60
+ for (const { key } of vars) {
61
+ console.log(pc.green(` ${key}`));
62
+ }
63
+ return { success: vars.length, failed: 0 };
64
+ }
65
+ catch (error) {
66
+ const err = error;
67
+ const stderrStr = err.stderr instanceof Buffer ? err.stderr.toString() : (err.stderr || err.message || 'Unknown error');
68
+ console.error(pc.red(` Failed to push secrets: ${stderrStr}`));
69
+ return { success: 0, failed: vars.length };
70
+ }
71
+ finally {
72
+ if (existsSync(tempFile)) {
73
+ unlinkSync(tempFile);
74
+ }
75
+ }
76
+ }
77
+ function getTargetsWithPush(config, targetFilter) {
78
+ const pushableTargets = config.targets.filter(t => {
79
+ const hasPushConfig = t.push_to !== undefined;
80
+ const isWranglerFormat = t.format === 'wrangler';
81
+ return hasPushConfig || isWranglerFormat;
82
+ });
83
+ if (targetFilter) {
84
+ const filtered = pushableTargets.filter(t => t.name === targetFilter);
85
+ if (filtered.length === 0) {
86
+ const availableTargets = pushableTargets.map(t => t.name).join(', ');
87
+ throw new Error(`Target "${targetFilter}" not found or has no push configuration.\n` +
88
+ `Available pushable targets: ${availableTargets || '(none)'}`);
89
+ }
90
+ return filtered;
91
+ }
92
+ return pushableTargets;
93
+ }
94
+ function getPushType(target) {
95
+ if (target.push_to) {
96
+ return target.push_to.type;
97
+ }
98
+ return 'cloudflare-workers';
99
+ }
100
+ function getPagesProject(target) {
101
+ if (target.push_to?.type === 'cloudflare-pages') {
102
+ return target.push_to.project;
103
+ }
104
+ throw new Error(`Target "${target.name}" is not configured for Cloudflare Pages`);
105
+ }
35
106
  export async function pushCommand(options) {
36
- const { root, dryRun, verbose } = options;
107
+ const { root, dryRun, verbose, target: targetFilter } = options;
37
108
  const config = loadConfig(root);
38
- console.log(pc.blue('Pushing production secrets to Cloudflare Workers...'));
109
+ console.log(pc.blue('Pushing production secrets to Cloudflare...'));
39
110
  if (dryRun) {
40
111
  console.log(pc.yellow('(dry-run mode)'));
41
112
  if (verbose) {
@@ -59,30 +130,61 @@ export async function pushCommand(options) {
59
130
  }
60
131
  const merged = mergeVars(...varSources);
61
132
  const interpolated = interpolateVars(merged);
62
- const wranglerTargets = config.targets.filter(t => t.format === 'wrangler');
63
- if (wranglerTargets.length === 0) {
64
- console.error(pc.red('No wrangler targets configured'));
133
+ const rootSecretsRecord = {};
134
+ for (const { key, value } of interpolated) {
135
+ rootSecretsRecord[key] = value;
136
+ }
137
+ let pushableTargets;
138
+ try {
139
+ pushableTargets = getTargetsWithPush(config, targetFilter);
140
+ }
141
+ catch (error) {
142
+ console.error(pc.red(error.message));
143
+ process.exit(1);
144
+ }
145
+ if (pushableTargets.length === 0) {
146
+ console.error(pc.red('No targets configured for push'));
147
+ console.error(pc.dim('Add format: wrangler or push_to: { type: cloudflare-pages, project: ... } to a target'));
65
148
  process.exit(1);
66
149
  }
67
- for (const target of wranglerTargets) {
150
+ for (const target of pushableTargets) {
68
151
  const targetDir = join(root, target.path);
69
- const filtered = filterVarsForTarget(interpolated, target);
152
+ const pushType = getPushType(target);
153
+ let filtered = filterVarsForTarget(interpolated, target);
154
+ const localTemplate = loadLocalTemplates(targetDir, 'production');
155
+ if (localTemplate.hasTemplate) {
156
+ const templateVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
157
+ filtered = mergeVars(filtered, templateVars);
158
+ }
159
+ if (filtered.length === 0) {
160
+ console.log(pc.dim(`\n${target.name} - no matching vars, skipped`));
161
+ continue;
162
+ }
163
+ const typeLabel = pushType === 'cloudflare-pages' ? 'Pages' : 'Workers';
70
164
  if (dryRun && verbose) {
71
- console.log(pc.blue(`\n[DRY RUN] Would push to ${target.name} (${target.path}/):`));
165
+ console.log(pc.blue(`\n[DRY RUN] Would push to ${target.name} (${typeLabel}, ${target.path}/):`));
72
166
  }
73
167
  else {
74
- console.log(pc.blue(`\n${target.name} (${target.path}/)`));
168
+ console.log(pc.blue(`\n${target.name} (${typeLabel}, ${target.path}/)`));
75
169
  }
76
170
  let success = 0;
77
171
  let failed = 0;
78
- for (const { key, value } of filtered) {
79
- if (pushSecret(key, value, targetDir, dryRun, verbose)) {
80
- if (!dryRun)
81
- console.log(pc.green(` ${key}`));
82
- success++;
83
- }
84
- else {
85
- failed++;
172
+ if (pushType === 'cloudflare-pages') {
173
+ const projectName = getPagesProject(target);
174
+ const result = pushPagesSecrets(filtered, projectName, targetDir, dryRun, verbose);
175
+ success = result.success;
176
+ failed = result.failed;
177
+ }
178
+ else {
179
+ for (const { key, value } of filtered) {
180
+ if (pushWorkerSecret(key, value, targetDir, dryRun, verbose)) {
181
+ if (!dryRun)
182
+ console.log(pc.green(` ${key}`));
183
+ success++;
184
+ }
185
+ else {
186
+ failed++;
187
+ }
86
188
  }
87
189
  }
88
190
  console.log(pc.dim(` ${success} pushed, ${failed} failed`));
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,WAAW,EAAU,MAAM,aAAa,CAAC;AAG/D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AA6BD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmG3E"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAU,WAAW,EAAU,MAAM,aAAa,CAAC;AAG/D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AA6BD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmI3E"}
@@ -6,6 +6,7 @@ import { interpolateVars } from '../core/interpolate.js';
6
6
  import { mergeVars } from '../core/merge.js';
7
7
  import { parseEnvContent } from '../core/parse.js';
8
8
  import { decrypt as sopsDecrypt } from '../core/sops.js';
9
+ import { loadLocalTemplates } from '../core/template.js';
9
10
  import { FORMAT_OUTPUT_FILES } from '../types.js';
10
11
  function matchesPattern(key, pattern) {
11
12
  const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
@@ -87,7 +88,7 @@ export async function resolveCommand(options) {
87
88
  console.log(`Path: ${pc.dim(target.path + '/')}`);
88
89
  console.log(`Format: ${pc.dim(target.format)} ${pc.dim(`(${outputFile})`)}`);
89
90
  console.log(`Environment: ${pc.dim(env)}`);
90
- console.log(pc.green(`\n✅ INCLUDED VARIABLES (${included.length}):`));
91
+ console.log(pc.green(`\n✅ ROOT SECRETS (Matched Filters) (${included.length}):`));
91
92
  if (included.length === 0) {
92
93
  console.log(pc.dim(' (none)'));
93
94
  }
@@ -107,5 +108,33 @@ export async function resolveCommand(options) {
107
108
  console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(matches: ${v.pattern})`)}`);
108
109
  }
109
110
  }
111
+ const targetAbsPath = join(root, target.path);
112
+ const localTemplate = loadLocalTemplates(targetAbsPath, env);
113
+ if (localTemplate.hasTemplate) {
114
+ console.log(pc.blue(`\n📄 TEMPLATE EXPANSIONS (${pc.dim(join(target.path, '.env'))}):`));
115
+ const maxKeyLen = Math.max(...localTemplate.vars.map(v => v.key.length));
116
+ for (const v of localTemplate.vars) {
117
+ console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`← ${v.value}`)}`);
118
+ }
119
+ // Calculate final merged list for clarity
120
+ const finalKeys = new Set([
121
+ ...included.map(v => v.key),
122
+ ...localTemplate.vars.map(v => v.key)
123
+ ]);
124
+ console.log(pc.magenta(`\n📦 FINAL INJECTION (${finalKeys.size} total):`));
125
+ const sortedKeys = Array.from(finalKeys).sort();
126
+ for (const key of sortedKeys) {
127
+ const isTemplate = localTemplate.vars.some(v => v.key === key);
128
+ const isRoot = included.some(v => v.key === key);
129
+ let sourceInfo = '';
130
+ if (isTemplate && isRoot)
131
+ sourceInfo = pc.dim('(template overrides root)');
132
+ else if (isTemplate)
133
+ sourceInfo = pc.dim('(template)');
134
+ else if (isRoot)
135
+ sourceInfo = pc.dim('(root)');
136
+ console.log(` ${key} ${sourceInfo}`);
137
+ }
138
+ }
110
139
  console.log('');
111
140
  }
@@ -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;AAkD/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,CA8GnE"}
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, resolve } from 'node:path';
4
4
  import pc from 'picocolors';
5
5
  import { loadConfig, findProjectRoot } from '../config/loader.js';
6
6
  import { filterVarsForTarget } from '../core/filter.js';
@@ -67,20 +67,26 @@ export async function runCommand(options) {
67
67
  const rootSecrets = getDecryptedSecrets(projectRoot, env, config);
68
68
  const rootSecretsRecord = getRootSecretsAsRecord(rootSecrets);
69
69
  const localTemplate = loadLocalTemplates(contextDir, env);
70
- let vars;
70
+ // 1. Resolve Template Vars
71
+ let templateVars = [];
71
72
  if (localTemplate.hasTemplate) {
72
- vars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
73
+ templateVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
73
74
  }
74
- else if (target) {
75
- const targetConfig = config.targets.find(t => t.name === target);
76
- if (!targetConfig) {
77
- console.error(pc.red(`Target "${target}" not found in hush.yaml`));
78
- console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
79
- process.exit(1);
80
- }
81
- vars = filterVarsForTarget(rootSecrets, targetConfig);
75
+ // 2. Resolve Target Vars
76
+ let targetVars = [];
77
+ // Find target config: either explicit by name, or implicit by directory matching
78
+ const targetConfig = target
79
+ ? config.targets.find(t => t.name === target)
80
+ : config.targets.find(t => resolve(projectRoot, t.path) === resolve(contextDir));
81
+ if (target && !targetConfig) {
82
+ console.error(pc.red(`Target "${target}" not found in hush.yaml`));
83
+ console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
84
+ process.exit(1);
85
+ }
86
+ if (targetConfig) {
87
+ targetVars = filterVarsForTarget(rootSecrets, targetConfig);
82
88
  if (targetConfig.format === 'wrangler') {
83
- vars.push({ key: 'CLOUDFLARE_INCLUDE_PROCESS_ENV', value: 'true' });
89
+ targetVars.push({ key: 'CLOUDFLARE_INCLUDE_PROCESS_ENV', value: 'true' });
84
90
  const devVarsPath = join(targetConfig.path, '.dev.vars');
85
91
  const absDevVarsPath = join(projectRoot, devVarsPath);
86
92
  if (existsSync(absDevVarsPath)) {
@@ -91,8 +97,21 @@ export async function runCommand(options) {
91
97
  }
92
98
  }
93
99
  }
100
+ else if (!localTemplate.hasTemplate && !target) {
101
+ // If no template and no target matched (and not running explicit target), fallback to all secrets
102
+ // This maintains backward compatibility for running in root or non-target dirs without templates
103
+ targetVars = rootSecrets;
104
+ }
105
+ // 3. Merge (Template overrides Target)
106
+ let vars;
107
+ if (localTemplate.hasTemplate) {
108
+ // Merge target vars with template vars.
109
+ // Template vars take precedence over target vars.
110
+ // This allows "additive" behavior: get target vars + template vars.
111
+ vars = mergeVars(targetVars, templateVars);
112
+ }
94
113
  else {
95
- vars = rootSecrets;
114
+ vars = targetVars;
96
115
  }
97
116
  const unresolved = getUnresolvedVars(vars);
98
117
  if (unresolved.length > 0) {
@@ -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;AAu+ChD,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;AA8hDhD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
@@ -155,7 +155,9 @@ Hush automatically:
155
155
  2. Decrypts root secrets
156
156
  3. Loads the local \`.env\` template
157
157
  4. Resolves \`\${VAR}\` references against root secrets
158
- 5. Injects the result into your command
158
+ 5. **Filters root secrets based on target config (include/exclude)**
159
+ 6. **Merges them (Template overrides Target)**
160
+ 7. Injects the result into your command
159
161
 
160
162
  ### Variable Expansion Syntax
161
163
 
@@ -721,14 +723,25 @@ hush status
721
723
 
722
724
  ### hush push
723
725
 
724
- Push production secrets to Cloudflare Workers.
726
+ Push production secrets to Cloudflare (Workers and Pages).
725
727
 
726
728
  \`\`\`bash
727
- hush push # Push secrets
729
+ hush push # Push all targets
730
+ hush push -t api # Push specific target
728
731
  hush push --dry-run # Preview without pushing
729
732
  hush push --dry-run --verbose # Detailed preview of what would be pushed
730
733
  \`\`\`
731
734
 
735
+ **For Cloudflare Pages:** Add \`push_to\` configuration to your target:
736
+ \`\`\`yaml
737
+ targets:
738
+ - name: app
739
+ format: dotenv
740
+ push_to:
741
+ type: cloudflare-pages
742
+ project: my-pages-project
743
+ \`\`\`
744
+
732
745
  ---
733
746
 
734
747
  ## Debugging Commands
@@ -893,10 +906,11 @@ hush has DB_URL -q && hush has API_KEY -q && echo "All set"
893
906
 
894
907
  ### hush push
895
908
 
896
- Push production secrets to Cloudflare Workers.
909
+ Push production secrets to Cloudflare (Workers and Pages).
897
910
 
898
911
  \`\`\`bash
899
- hush push # Push secrets
912
+ hush push # Push all targets
913
+ hush push -t api # Push specific target
900
914
  hush push --dry-run # Preview without pushing
901
915
  \`\`\`
902
916
 
@@ -1230,14 +1244,35 @@ Look at the 🚫 EXCLUDED section to see which pattern is filtering out your var
1230
1244
 
1231
1245
  ### "Wrangler dev not seeing secrets"
1232
1246
 
1233
- If you are using \`hush run -- wrangler dev\` and secrets are missing, Wrangler is likely being blocked by a local file.
1247
+ If you are using \`hush run -- wrangler dev\` and secrets are missing:
1234
1248
 
1235
- **The Fix:**
1236
- 1. **Delete .dev.vars**: Run \`rm .dev.vars\` inside your worker directory.
1237
- 2. **Run normally**: \`hush run -- wrangler dev\`
1249
+ **Step 1: Check for blocking files**
1250
+ \`\`\`bash
1251
+ ls -la .dev.vars # If this exists, it blocks Hush secrets
1252
+ \`\`\`
1238
1253
 
1239
- **Explanation:**
1240
- Wrangler completely ignores environment variables if a \`.dev.vars\` file exists. Hush automatically handles the necessary environment configuration (\`CLOUDFLARE_INCLUDE_PROCESS_ENV=true\`) for you, but you MUST ensure the conflicting file is removed.
1254
+ **Step 2: Delete the blocking file**
1255
+ \`\`\`bash
1256
+ rm .dev.vars
1257
+ \`\`\`
1258
+
1259
+ **Step 3: Run normally**
1260
+ \`\`\`bash
1261
+ npx hush run -t api -- wrangler dev
1262
+ \`\`\`
1263
+
1264
+ **Step 4: If still not working, update Wrangler**
1265
+ \`\`\`bash
1266
+ npm update wrangler
1267
+ \`\`\`
1268
+
1269
+ **Why this happens:**
1270
+ - Wrangler has a strict rule: if \`.dev.vars\` exists (even empty!), it ignores ALL environment variables
1271
+ - Hush automatically sets \`CLOUDFLARE_INCLUDE_PROCESS_ENV=true\` for you
1272
+ - But Wrangler only respects this when no \`.dev.vars\` file exists
1273
+ - Older Wrangler versions may not support \`CLOUDFLARE_INCLUDE_PROCESS_ENV\` at all
1274
+
1275
+ **Prevention tip:** Never use \`hush decrypt\` for Wrangler targets—always use \`hush run\`.
1241
1276
 
1242
1277
  ### "Variable appears in wrong places"
1243
1278
 
@@ -1290,6 +1325,26 @@ npx hush inspect # See what's new
1290
1325
  \`\`\`bash
1291
1326
  npx hush push --dry-run # Preview first
1292
1327
  npx hush push # Actually push
1328
+ npx hush push -t api # Push specific target
1329
+ \`\`\`
1330
+
1331
+ ### "Push to Cloudflare Pages"
1332
+
1333
+ First, add \`push_to\` to your target in \`hush.yaml\`:
1334
+ \`\`\`yaml
1335
+ targets:
1336
+ - name: app
1337
+ path: ./app
1338
+ format: dotenv
1339
+ push_to:
1340
+ type: cloudflare-pages
1341
+ project: my-pages-project
1342
+ \`\`\`
1343
+
1344
+ Then push:
1345
+ \`\`\`bash
1346
+ npx hush push -t app --dry-run # Preview first
1347
+ npx hush push -t app # Actually push
1293
1348
  \`\`\`
1294
1349
 
1295
1350
  ### "Build and deploy"
@@ -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,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"}
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,CA6C3D"}
@@ -55,6 +55,7 @@ export function checkSchemaVersion(config) {
55
55
  export function validateConfig(config) {
56
56
  const errors = [];
57
57
  const validFormats = ['dotenv', 'wrangler', 'json', 'shell', 'yaml'];
58
+ const validPushTypes = ['cloudflare-workers', 'cloudflare-pages'];
58
59
  if (!config.sources.shared) {
59
60
  errors.push('sources.shared is required');
60
61
  }
@@ -76,6 +77,21 @@ export function validateConfig(config) {
76
77
  else if (!validFormats.includes(target.format)) {
77
78
  errors.push(`${prefix}: invalid format "${target.format}" (must be one of: ${validFormats.join(', ')})`);
78
79
  }
80
+ // Validate push_to configuration
81
+ if (target.push_to) {
82
+ if (!target.push_to.type) {
83
+ errors.push(`${prefix}: push_to.type is required (one of: ${validPushTypes.join(', ')})`);
84
+ }
85
+ else if (!validPushTypes.includes(target.push_to.type)) {
86
+ errors.push(`${prefix}: invalid push_to.type "${target.push_to.type}" (must be one of: ${validPushTypes.join(', ')})`);
87
+ }
88
+ else if (target.push_to.type === 'cloudflare-pages') {
89
+ const pagesConfig = target.push_to;
90
+ if (!pagesConfig.project) {
91
+ errors.push(`${prefix}: push_to.project is required for cloudflare-pages`);
92
+ }
93
+ }
94
+ }
79
95
  }
80
96
  return errors;
81
97
  }
package/dist/types.d.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  export type OutputFormat = 'dotenv' | 'wrangler' | 'json' | 'shell' | 'yaml';
2
2
  export type Environment = 'development' | 'production';
3
+ export type PushDestinationType = 'cloudflare-workers' | 'cloudflare-pages';
4
+ export interface CloudflareWorkersPushConfig {
5
+ type: 'cloudflare-workers';
6
+ }
7
+ export interface CloudflarePagesPushConfig {
8
+ type: 'cloudflare-pages';
9
+ project: string;
10
+ }
11
+ export type PushConfig = CloudflareWorkersPushConfig | CloudflarePagesPushConfig;
3
12
  export interface Target {
4
13
  name: string;
5
14
  path: string;
6
15
  format: OutputFormat;
7
16
  include?: string[];
8
17
  exclude?: string[];
18
+ push_to?: PushConfig;
9
19
  }
10
20
  export interface SourceFiles {
11
21
  shared: string;
@@ -52,6 +62,7 @@ export interface PushOptions {
52
62
  root: string;
53
63
  dryRun: boolean;
54
64
  verbose: boolean;
65
+ target?: string;
55
66
  }
56
67
  export interface StatusOptions {
57
68
  root: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "4.1.1",
3
+ "version": "4.2.0",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {