@chriscode/hush 4.1.2 → 5.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/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { resolveCommand } from './commands/resolve.js';
19
19
  import { traceCommand } from './commands/trace.js';
20
20
  import { templateCommand } from './commands/template.js';
21
21
  import { expansionsCommand } from './commands/expansions.js';
22
+ import { migrateCommand } from './commands/migrate.js';
22
23
  import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
23
24
  import { checkForUpdate } from './utils/version-check.js';
24
25
  const require = createRequire(import.meta.url);
@@ -32,7 +33,7 @@ ${pc.bold('Usage:')}
32
33
 
33
34
  ${pc.bold('Commands:')}
34
35
  init Initialize hush.yaml config
35
- encrypt Encrypt source .env files
36
+ encrypt Encrypt source .hush files
36
37
  run -- <cmd> Run command with secrets in memory (AI-safe)
37
38
  set <KEY> Set a single secret interactively (AI-safe)
38
39
  edit [file] Edit all secrets in $EDITOR
@@ -40,10 +41,11 @@ ${pc.bold('Commands:')}
40
41
  inspect List all variables (masked values, AI-safe)
41
42
  has <key> Check if a secret exists (exit 0 if set, 1 if not)
42
43
  check Verify secrets are encrypted (for pre-commit hooks)
43
- push Push secrets to Cloudflare Workers
44
+ push Push secrets to Cloudflare (Workers and Pages)
44
45
  status Show configuration and status
45
46
  skill Install Claude Code / OpenCode skill
46
47
  keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
48
+ migrate Migrate from v4 (.env.encrypted) to v5 (.hush.encrypted)
47
49
 
48
50
  ${pc.bold('Debugging Commands:')}
49
51
  resolve <target> Show what variables a target receives (AI-safe)
@@ -57,7 +59,7 @@ ${pc.bold('Advanced Commands:')}
57
59
  ${pc.bold('Options:')}
58
60
  -e, --env <env> Environment: development or production (default: development)
59
61
  -r, --root <dir> Root directory (default: current directory)
60
- -t, --target <t> Target name from hush.yaml (run/resolve only)
62
+ -t, --target <t> Target name from hush.yaml (run/resolve/push)
61
63
  -q, --quiet Suppress output (has/check commands)
62
64
  --dry-run Preview changes without applying (push only)
63
65
  --verbose Show detailed output (push --dry-run only)
@@ -72,7 +74,7 @@ ${pc.bold('Options:')}
72
74
  -h, --help Show this help message
73
75
  -v, --version Show version number
74
76
 
75
- ${pc.bold('Variable Expansion (v4+):')}
77
+ ${pc.bold('Variable Expansion (v5+):')}
76
78
  Subdirectory .env files can reference root secrets:
77
79
 
78
80
  \${VAR} Pull VAR from root secrets
@@ -90,9 +92,20 @@ ${pc.bold('Variable Expansion (v4+):')}
90
92
 
91
93
  Subdirectory templates are safe to commit - they contain no secrets.
92
94
 
95
+ ${pc.bold('File Naming (v5+):')}
96
+ Hush uses .hush files instead of .env to avoid conflicts with other tools:
97
+
98
+ .hush Shared secrets (source file)
99
+ .hush.development Development secrets (source file)
100
+ .hush.encrypted Encrypted shared secrets (committed)
101
+ .hush.development.encrypted Encrypted dev secrets (committed)
102
+
103
+ The .env files are reserved for other tools (Wrangler, Metro, etc.).
104
+
93
105
  ${pc.bold('Examples:')}
94
106
  hush init Initialize config + generate keys
95
- hush encrypt Encrypt .env files
107
+ hush migrate Migrate v4 .env.encrypted to v5 .hush.encrypted
108
+ hush encrypt Encrypt .hush files
96
109
  hush run -- npm start Run with secrets in memory (AI-safe!)
97
110
  hush run -e prod -- npm build Run with production secrets
98
111
  hush run -t api -- wrangler dev Run filtered for 'api' target (root secrets only)
@@ -110,6 +123,7 @@ ${pc.bold('Examples:')}
110
123
  hush has API_KEY -q && echo "API_KEY is configured"
111
124
  hush check Verify secrets are encrypted
112
125
  hush push --dry-run Preview push to Cloudflare
126
+ hush push -t app Push only the 'app' target
113
127
  hush status Show current status
114
128
  hush skill Install Claude skill (interactive)
115
129
  `);
@@ -281,7 +295,7 @@ function parseArgs(args) {
281
295
  return { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs };
282
296
  }
283
297
  function checkMigrationNeeded(root, command) {
284
- const skipCommands = ['', 'help', 'version', 'init', 'skill'];
298
+ const skipCommands = ['', 'help', 'version', 'init', 'skill', 'migrate'];
285
299
  if (skipCommands.includes(command))
286
300
  return;
287
301
  const configPath = findConfigPath(root);
@@ -364,7 +378,7 @@ async function main() {
364
378
  await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
365
379
  break;
366
380
  case 'push':
367
- await pushCommand({ root, dryRun, verbose });
381
+ await pushCommand({ root, dryRun, verbose, target });
368
382
  break;
369
383
  case 'status':
370
384
  await statusCommand({ root });
@@ -402,6 +416,9 @@ async function main() {
402
416
  case 'expansions':
403
417
  await expansionsCommand({ root, env });
404
418
  break;
419
+ case 'migrate':
420
+ await migrateCommand({ root, dryRun });
421
+ break;
405
422
  default:
406
423
  if (command) {
407
424
  console.error(pc.red(`Unknown command: ${command}`));
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,MAAM,aAAa,CAAC;AAkF/G,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAuMD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,MAAM,aAAa,CAAC;AAmF/G,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAuMD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
@@ -44,6 +44,7 @@ function getGitChangedFiles(root) {
44
44
  }
45
45
  function findPlaintextEnvFiles(root) {
46
46
  const results = [];
47
+ // Only warn about .env files (legacy/output files), NOT .hush files (Hush's source files)
47
48
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
48
49
  const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
49
50
  function scanDir(dir, relativePath = '') {
@@ -206,8 +207,8 @@ function formatTextOutput(result) {
206
207
  lines.push(pc.yellow('These files contain plaintext secrets that could be exposed to AI assistants.'));
207
208
  lines.push('');
208
209
  lines.push(pc.bold('To fix:'));
209
- lines.push(pc.dim(' 1. Run: hush encrypt'));
210
- lines.push(pc.dim(' 2. Delete the plaintext files (the .encrypted versions are your source of truth)'));
210
+ lines.push(pc.dim(' 1. Run: hush migrate (if upgrading from v4)'));
211
+ lines.push(pc.dim(' 2. Delete or gitignore these .env files'));
211
212
  lines.push(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars'));
212
213
  lines.push('');
213
214
  lines.push(pc.dim('To allow plaintext files (not recommended): --allow-plaintext'));
@@ -34,7 +34,7 @@ export async function encryptCommand(options) {
34
34
  }
35
35
  if (encryptedFiles.length === 0) {
36
36
  console.error(pc.red('\nNo source files found to encrypt'));
37
- console.error(pc.dim('Create at least .env with your secrets'));
37
+ console.error(pc.dim('Create at least .hush with your secrets'));
38
38
  process.exit(1);
39
39
  }
40
40
  console.log(pc.blue('\nVerifying encryption...'));
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;AAyJnE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAsErE"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;AAyKnE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFrE"}
@@ -126,6 +126,7 @@ function detectTargets(root) {
126
126
  return targets;
127
127
  }
128
128
  function findExistingPlaintextEnvFiles(root) {
129
+ // Look for legacy .env files that may need migration
129
130
  const patterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
130
131
  const found = [];
131
132
  for (const pattern of patterns) {
@@ -136,6 +137,18 @@ function findExistingPlaintextEnvFiles(root) {
136
137
  }
137
138
  return found;
138
139
  }
140
+ function findExistingEncryptedFiles(root) {
141
+ // Look for v4 encrypted files that need migration to v5 (.hush.encrypted)
142
+ const patterns = ['.env.encrypted', '.env.development.encrypted', '.env.production.encrypted', '.env.local.encrypted'];
143
+ const found = [];
144
+ for (const pattern of patterns) {
145
+ const filePath = join(root, pattern);
146
+ if (existsSync(filePath)) {
147
+ found.push(pattern);
148
+ }
149
+ }
150
+ return found;
151
+ }
139
152
  export async function initCommand(options) {
140
153
  const { root } = options;
141
154
  const existingConfig = findConfigPath(root);
@@ -144,14 +157,24 @@ export async function initCommand(options) {
144
157
  process.exit(1);
145
158
  }
146
159
  console.log(pc.blue('Initializing hush...\n'));
160
+ const existingEncryptedFiles = findExistingEncryptedFiles(root);
161
+ if (existingEncryptedFiles.length > 0) {
162
+ console.log(pc.bgYellow(pc.black(' V4 ENCRYPTED FILES DETECTED ')));
163
+ console.log(pc.yellow('\nFound existing v4 encrypted files:'));
164
+ for (const file of existingEncryptedFiles) {
165
+ console.log(pc.yellow(` ${file}`));
166
+ }
167
+ console.log(pc.dim('\nRun "npx hush migrate" to convert to v5 format (.hush.encrypted).\n'));
168
+ }
147
169
  const existingEnvFiles = findExistingPlaintextEnvFiles(root);
148
170
  if (existingEnvFiles.length > 0) {
149
- console.log(pc.bgYellow(pc.black(' EXISTING SECRETS DETECTED ')));
171
+ console.log(pc.bgYellow(pc.black(' PLAINTEXT .ENV FILES DETECTED ')));
150
172
  console.log(pc.yellow('\nFound existing .env files:'));
151
173
  for (const file of existingEnvFiles) {
152
174
  console.log(pc.yellow(` ${file}`));
153
175
  }
154
- console.log(pc.dim('\nThese will be encrypted after setup. Run "npx hush encrypt" when ready.\n'));
176
+ console.log(pc.dim('\nRename these to .hush files, then run "npx hush encrypt".\n'));
177
+ console.log(pc.dim('Example: mv .env .hush && mv .env.development .hush.development\n'));
155
178
  }
156
179
  const project = getProjectFromPackageJson(root);
157
180
  if (!project) {
@@ -179,14 +202,19 @@ export async function initCommand(options) {
179
202
  console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
180
203
  }
181
204
  console.log(pc.bold('\nNext steps:'));
182
- if (existingEnvFiles.length > 0) {
183
- console.log(pc.green(' 1. npx hush encrypt') + pc.dim(' # Encrypt existing .env files (deletes plaintext)'));
184
- console.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
185
- console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
205
+ if (existingEncryptedFiles.length > 0) {
206
+ console.log(pc.green(' 1. npx hush migrate') + pc.dim(' # Convert v4 .env.encrypted to v5 .hush.encrypted'));
207
+ console.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
208
+ console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
209
+ }
210
+ else if (existingEnvFiles.length > 0) {
211
+ console.log(pc.green(' 1. Rename .env files to .hush') + pc.dim(' # mv .env .hush'));
212
+ console.log(pc.dim(' 2. npx hush encrypt') + pc.dim(' # Encrypt .hush files'));
213
+ console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
186
214
  }
187
215
  else {
188
- console.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
189
- console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
216
+ console.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
217
+ console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
190
218
  }
191
219
  console.log(pc.dim('\nGit setup:'));
192
220
  console.log(pc.dim(' git add hush.yaml .sops.yaml'));
@@ -0,0 +1,6 @@
1
+ export interface MigrateOptions {
2
+ root: string;
3
+ dryRun: boolean;
4
+ }
5
+ export declare function migrateCommand(options: MigrateOptions): Promise<void>;
6
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AA8DD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA0E3E"}
@@ -0,0 +1,116 @@
1
+ import { existsSync, renameSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
5
+ import { findConfigPath } from '../config/loader.js';
6
+ const FILE_MIGRATIONS = [
7
+ { from: '.env.encrypted', to: '.hush.encrypted' },
8
+ { from: '.env.development.encrypted', to: '.hush.development.encrypted' },
9
+ { from: '.env.production.encrypted', to: '.hush.production.encrypted' },
10
+ { from: '.env.local.encrypted', to: '.hush.local.encrypted' },
11
+ ];
12
+ const SOURCE_MIGRATIONS = {
13
+ '.env': '.hush',
14
+ '.env.development': '.hush.development',
15
+ '.env.production': '.hush.production',
16
+ '.env.local': '.hush.local',
17
+ };
18
+ function getMigrationFiles(root) {
19
+ return FILE_MIGRATIONS.map(({ from, to }) => ({
20
+ from,
21
+ to,
22
+ exists: existsSync(join(root, from)),
23
+ }));
24
+ }
25
+ function migrateConfig(root, dryRun) {
26
+ const configPath = findConfigPath(root);
27
+ if (!configPath)
28
+ return false;
29
+ const content = readFileSync(configPath, 'utf-8');
30
+ const config = parseYaml(content);
31
+ let modified = false;
32
+ const sources = config.sources;
33
+ if (sources) {
34
+ for (const [oldValue, newValue] of Object.entries(SOURCE_MIGRATIONS)) {
35
+ for (const [key, value] of Object.entries(sources)) {
36
+ if (value === oldValue) {
37
+ if (!dryRun) {
38
+ sources[key] = newValue;
39
+ }
40
+ modified = true;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ if (modified && !dryRun) {
46
+ const schemaComment = content.startsWith('#') ? content.split('\n')[0] + '\n' : '';
47
+ const newContent = schemaComment + stringifyYaml(config, { indent: 2 });
48
+ writeFileSync(configPath, newContent, 'utf-8');
49
+ }
50
+ return modified;
51
+ }
52
+ export async function migrateCommand(options) {
53
+ const { root, dryRun } = options;
54
+ console.log(pc.blue('Hush v4 → v5 Migration\n'));
55
+ if (dryRun) {
56
+ console.log(pc.yellow('DRY RUN - no changes will be made\n'));
57
+ }
58
+ const migrations = getMigrationFiles(root);
59
+ const filesToMigrate = migrations.filter(m => m.exists);
60
+ if (filesToMigrate.length === 0) {
61
+ console.log(pc.dim('No v4 encrypted files found (.env.encrypted, etc.)'));
62
+ console.log(pc.dim('Already on v5 or no encrypted files exist.\n'));
63
+ const configNeedsMigration = migrateConfig(root, true);
64
+ if (configNeedsMigration) {
65
+ console.log(pc.yellow('hush.yaml contains v4 source paths that need updating.\n'));
66
+ if (!dryRun) {
67
+ migrateConfig(root, false);
68
+ console.log(pc.green('Updated hush.yaml source paths to v5 format.\n'));
69
+ }
70
+ }
71
+ return;
72
+ }
73
+ console.log(pc.bold('Files to migrate:'));
74
+ for (const { from, to, exists } of migrations) {
75
+ if (exists) {
76
+ console.log(` ${pc.yellow(from)} → ${pc.green(to)}`);
77
+ }
78
+ else {
79
+ console.log(pc.dim(` ${from} (not found, skipping)`));
80
+ }
81
+ }
82
+ console.log('');
83
+ if (dryRun) {
84
+ console.log(pc.dim('Run without --dry-run to apply changes.'));
85
+ return;
86
+ }
87
+ let migratedCount = 0;
88
+ for (const { from, to, exists } of migrations) {
89
+ if (!exists)
90
+ continue;
91
+ const fromPath = join(root, from);
92
+ const toPath = join(root, to);
93
+ if (existsSync(toPath)) {
94
+ console.log(pc.yellow(` Skipping ${from}: ${to} already exists`));
95
+ continue;
96
+ }
97
+ renameSync(fromPath, toPath);
98
+ console.log(pc.green(` Migrated ${from} → ${to}`));
99
+ migratedCount++;
100
+ }
101
+ const configUpdated = migrateConfig(root, false);
102
+ if (configUpdated) {
103
+ console.log(pc.green(' Updated hush.yaml source paths'));
104
+ }
105
+ console.log('');
106
+ if (migratedCount > 0 || configUpdated) {
107
+ console.log(pc.green(pc.bold(`Migration complete.`)));
108
+ console.log(pc.dim('\nNext steps:'));
109
+ console.log(pc.dim(' 1. git add .hush.encrypted .hush.*.encrypted hush.yaml'));
110
+ console.log(pc.dim(' 2. git rm .env.encrypted .env.*.encrypted (if tracked)'));
111
+ console.log(pc.dim(' 3. git commit -m "chore: migrate to Hush v5 format"'));
112
+ }
113
+ else {
114
+ console.log(pc.dim('No changes made.'));
115
+ }
116
+ }
@@ -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":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAy+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;AAuiDhD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
@@ -6,15 +6,15 @@ import pc from 'picocolors';
6
6
  const SKILL_FILES = {
7
7
  'SKILL.md': `---
8
8
  name: hush-secrets
9
- description: Manage secrets safely using Hush CLI. Use when working with .env files, environment variables, secrets, API keys, database URLs, credentials, or configuration. NEVER read .env files directly - always use hush commands instead to prevent exposing secrets to the LLM.
9
+ description: Manage secrets safely using Hush CLI. Use when working with .hush files, environment variables, secrets, API keys, database URLs, credentials, or configuration. NEVER read .hush files directly - always use hush commands instead to prevent exposing secrets to the LLM.
10
10
  allowed-tools: Bash(hush:*), Bash(npx hush:*), Bash(brew:*), Bash(npm:*), Bash(pnpm:*), Bash(age-keygen:*), Read, Grep, Glob, Write, Bash(cat:*), Bash(grep:*)
11
11
  ---
12
12
 
13
13
  # Hush - AI-Native Secrets Management
14
14
 
15
- **CRITICAL: NEVER read root .env files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
15
+ **CRITICAL: NEVER read .hush 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** at the project root. Subdirectory \`.env\` files are **templates** (safe to commit and read) that reference root secrets via \`\${VAR}\` syntax.
17
+ Hush keeps secrets **encrypted at rest** at the project root using \`.hush.encrypted\` files. 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,7 +35,7 @@ This tells you:
35
35
 
36
36
  | You See | What It Means | Action |
37
37
  |---------|---------------|--------|
38
- | \`SECURITY WARNING: Unencrypted .env files\` | Plaintext secrets at project root! | Run \`npx hush encrypt\` immediately |
38
+ | \`SECURITY WARNING: Unencrypted .env files\` | Plaintext .env files found (legacy or output) | Run \`npx hush migrate\` or delete them |
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\` |
@@ -53,12 +53,12 @@ npx hush encrypt # Encrypts any existing .env files, deletes plaintext
53
53
  npx hush inspect # Verify setup
54
54
  \`\`\`
55
55
 
56
- ### Scenario 2: Existing .env Files Found
56
+ ### Scenario 2: Existing .env Files Found (Migration from v4)
57
57
 
58
58
  \`\`\`bash
59
59
  npx hush status # Check what's there
60
- npx hush encrypt # Encrypt them (auto-deletes plaintext after verification)
61
- npx hush inspect # Confirm everything is encrypted
60
+ npx hush migrate # Migrate .env.encrypted to .hush.encrypted
61
+ npx hush inspect # Confirm everything is migrated
62
62
  \`\`\`
63
63
 
64
64
  ### Scenario 3: Hush Already Set Up (Team Member Joining)
@@ -423,9 +423,9 @@ The generated config looks like:
423
423
 
424
424
  \`\`\`yaml
425
425
  sources:
426
- shared: .env
427
- development: .env.development
428
- production: .env.production
426
+ shared: .hush
427
+ development: .hush.development
428
+ production: .hush.production
429
429
 
430
430
  targets:
431
431
  - name: root
@@ -461,29 +461,29 @@ Customize targets for your monorepo. Common patterns:
461
461
  format: yaml
462
462
  \`\`\`
463
463
 
464
- ### Step 5: Create initial \`.env\` files
464
+ ### Step 5: Create initial \`.hush\` files
465
465
 
466
- Create \`.env\` with shared secrets:
466
+ Create \`.hush\` with shared secrets:
467
467
 
468
468
  \`\`\`bash
469
- # .env
469
+ # .hush
470
470
  DATABASE_URL=postgres://localhost/mydb
471
471
  API_KEY=your_api_key_here
472
472
  NEXT_PUBLIC_API_URL=http://localhost:3000
473
473
  \`\`\`
474
474
 
475
- Create \`.env.development\` for dev-specific values:
475
+ Create \`.hush.development\` for dev-specific values:
476
476
 
477
477
  \`\`\`bash
478
- # .env.development
478
+ # .hush.development
479
479
  DEBUG=true
480
480
  LOG_LEVEL=debug
481
481
  \`\`\`
482
482
 
483
- Create \`.env.production\` for production values:
483
+ Create \`.hush.production\` for production values:
484
484
 
485
485
  \`\`\`bash
486
- # .env.production
486
+ # .hush.production
487
487
  DEBUG=false
488
488
  LOG_LEVEL=error
489
489
  \`\`\`
@@ -495,9 +495,9 @@ npx hush encrypt
495
495
  \`\`\`
496
496
 
497
497
  This creates:
498
- - \`.env.encrypted\`
499
- - \`.env.development.encrypted\`
500
- - \`.env.production.encrypted\`
498
+ - \`.hush.encrypted\`
499
+ - \`.hush.development.encrypted\`
500
+ - \`.hush.production.encrypted\`
501
501
 
502
502
  ### Step 7: Verify setup
503
503
 
@@ -511,22 +511,26 @@ npx hush inspect
511
511
  Add these lines to \`.gitignore\`:
512
512
 
513
513
  \`\`\`gitignore
514
- # Hush - plaintext env files (generated, not committed)
514
+ # Hush - plaintext source files (encrypted versions are committed)
515
+ .hush
516
+ .hush.local
517
+ .hush.development
518
+ .hush.production
519
+
520
+ # Output files (generated by hush decrypt, not committed)
515
521
  .env
516
- .env.local
517
- .env.development
518
- .env.production
522
+ .env.*
519
523
  .dev.vars
520
524
 
521
525
  # Keep encrypted files (these ARE committed)
522
- !.env.encrypted
523
- !.env.*.encrypted
526
+ !.hush.encrypted
527
+ !.hush.*.encrypted
524
528
  \`\`\`
525
529
 
526
530
  ### Step 9: Commit encrypted files
527
531
 
528
532
  \`\`\`bash
529
- git add .sops.yaml hush.yaml .env*.encrypted .gitignore
533
+ git add .sops.yaml hush.yaml .hush*.encrypted .gitignore
530
534
  git commit -m "chore: add Hush secrets management"
531
535
  \`\`\`
532
536
 
@@ -552,8 +556,8 @@ After setup, verify everything works:
552
556
  - [ ] \`npx hush status\` shows configuration
553
557
  - [ ] \`npx hush inspect\` shows masked variables
554
558
  - [ ] \`npx hush run -- env\` can decrypt and run (secrets stay in memory!)
555
- - [ ] \`.env.encrypted\` files are committed to git
556
- - [ ] Plaintext \`.env\` files are in \`.gitignore\`
559
+ - [ ] \`.hush.encrypted\` files are committed to git
560
+ - [ ] Plaintext \`.hush\` and \`.env\` files are in \`.gitignore\`
557
561
 
558
562
  ---
559
563
 
@@ -701,7 +705,7 @@ hush init
701
705
 
702
706
  ### hush encrypt
703
707
 
704
- Encrypt source \`.env\` files to \`.env.encrypted\` files.
708
+ Encrypt source \`.hush\` files to \`.hush.encrypted\` files.
705
709
 
706
710
  \`\`\`bash
707
711
  hush encrypt
@@ -723,14 +727,25 @@ hush status
723
727
 
724
728
  ### hush push
725
729
 
726
- Push production secrets to Cloudflare Workers.
730
+ Push production secrets to Cloudflare (Workers and Pages).
727
731
 
728
732
  \`\`\`bash
729
- hush push # Push secrets
733
+ hush push # Push all targets
734
+ hush push -t api # Push specific target
730
735
  hush push --dry-run # Preview without pushing
731
736
  hush push --dry-run --verbose # Detailed preview of what would be pushed
732
737
  \`\`\`
733
738
 
739
+ **For Cloudflare Pages:** Add \`push_to\` configuration to your target:
740
+ \`\`\`yaml
741
+ targets:
742
+ - name: app
743
+ format: dotenv
744
+ push_to:
745
+ type: cloudflare-pages
746
+ project: my-pages-project
747
+ \`\`\`
748
+
734
749
  ---
735
750
 
736
751
  ## Debugging Commands
@@ -895,10 +910,11 @@ hush has DB_URL -q && hush has API_KEY -q && echo "All set"
895
910
 
896
911
  ### hush push
897
912
 
898
- Push production secrets to Cloudflare Workers.
913
+ Push production secrets to Cloudflare (Workers and Pages).
899
914
 
900
915
  \`\`\`bash
901
- hush push # Push secrets
916
+ hush push # Push all targets
917
+ hush push -t api # Push specific target
902
918
  hush push --dry-run # Preview without pushing
903
919
  \`\`\`
904
920
 
@@ -977,9 +993,9 @@ hush skill --local # Install to ./.claude/skills/
977
993
 
978
994
  \`\`\`yaml
979
995
  sources:
980
- shared: .env
981
- development: .env.development
982
- production: .env.production
996
+ shared: .hush
997
+ development: .hush.development
998
+ production: .hush.production
983
999
 
984
1000
  targets:
985
1001
  - name: root
@@ -1034,7 +1050,7 @@ targets:
1034
1050
  | Expo | \`EXPO_PUBLIC_*\` | \`include: [EXPO_PUBLIC_*]\` |
1035
1051
  | Gatsby | \`GATSBY_*\` | \`include: [GATSBY_*]\` |
1036
1052
 
1037
- ### Variable Interpolation (v4+)
1053
+ ### Variable Interpolation (v5+)
1038
1054
 
1039
1055
  Reference other variables using \`\${VAR}\` syntax:
1040
1056
 
@@ -1100,8 +1116,13 @@ This will show:
1100
1116
 
1101
1117
  #### Path A: "SECURITY WARNING: Unencrypted .env files detected"
1102
1118
  \`\`\`bash
1119
+ # If migrating from v4 (has .env.encrypted files):
1120
+ npx hush migrate # Converts to .hush.encrypted format
1121
+
1122
+ # If new setup with plaintext .env files:
1123
+ mv .env .hush # Rename to .hush
1103
1124
  npx hush init # If no hush.yaml exists
1104
- npx hush encrypt # Encrypts files and DELETES plaintext automatically
1125
+ npx hush encrypt # Encrypts .hush files
1105
1126
  npx hush status # Verify the warning is gone
1106
1127
  \`\`\`
1107
1128
 
@@ -1232,14 +1253,35 @@ Look at the 🚫 EXCLUDED section to see which pattern is filtering out your var
1232
1253
 
1233
1254
  ### "Wrangler dev not seeing secrets"
1234
1255
 
1235
- If you are using \`hush run -- wrangler dev\` and secrets are missing, Wrangler is likely being blocked by a local file.
1256
+ If you are using \`hush run -- wrangler dev\` and secrets are missing:
1236
1257
 
1237
- **The Fix:**
1238
- 1. **Delete .dev.vars**: Run \`rm .dev.vars\` inside your worker directory.
1239
- 2. **Run normally**: \`hush run -- wrangler dev\`
1258
+ **Step 1: Check for blocking files**
1259
+ \`\`\`bash
1260
+ ls -la .dev.vars # If this exists, it blocks Hush secrets
1261
+ \`\`\`
1262
+
1263
+ **Step 2: Delete the blocking file**
1264
+ \`\`\`bash
1265
+ rm .dev.vars
1266
+ \`\`\`
1240
1267
 
1241
- **Explanation:**
1242
- 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.
1268
+ **Step 3: Run normally**
1269
+ \`\`\`bash
1270
+ npx hush run -t api -- wrangler dev
1271
+ \`\`\`
1272
+
1273
+ **Step 4: If still not working, update Wrangler**
1274
+ \`\`\`bash
1275
+ npm update wrangler
1276
+ \`\`\`
1277
+
1278
+ **Why this happens:**
1279
+ - Wrangler has a strict rule: if \`.dev.vars\` exists (even empty!), it ignores ALL environment variables
1280
+ - Hush automatically sets \`CLOUDFLARE_INCLUDE_PROCESS_ENV=true\` for you
1281
+ - But Wrangler only respects this when no \`.dev.vars\` file exists
1282
+ - Older Wrangler versions may not support \`CLOUDFLARE_INCLUDE_PROCESS_ENV\` at all
1283
+
1284
+ **Prevention tip:** Never use \`hush decrypt\` for Wrangler targets—always use \`hush run\`.
1243
1285
 
1244
1286
  ### "Variable appears in wrong places"
1245
1287
 
@@ -1292,6 +1334,26 @@ npx hush inspect # See what's new
1292
1334
  \`\`\`bash
1293
1335
  npx hush push --dry-run # Preview first
1294
1336
  npx hush push # Actually push
1337
+ npx hush push -t api # Push specific target
1338
+ \`\`\`
1339
+
1340
+ ### "Push to Cloudflare Pages"
1341
+
1342
+ First, add \`push_to\` to your target in \`hush.yaml\`:
1343
+ \`\`\`yaml
1344
+ targets:
1345
+ - name: app
1346
+ path: ./app
1347
+ format: dotenv
1348
+ push_to:
1349
+ type: cloudflare-pages
1350
+ project: my-pages-project
1351
+ \`\`\`
1352
+
1353
+ Then push:
1354
+ \`\`\`bash
1355
+ npx hush push -t app --dry-run # Preview first
1356
+ npx hush push -t app # Actually push
1295
1357
  \`\`\`
1296
1358
 
1297
1359
  ### "Build and deploy"
@@ -1442,11 +1504,11 @@ Key Status:
1442
1504
  \`\`\`
1443
1505
 
1444
1506
  **Reading this:**
1445
- - There's a security issue - plaintext files exist
1507
+ - There are .env files present (legacy or output from decrypt)
1446
1508
  - The project is configured with key management
1447
1509
  - Keys are properly set up and backed up
1448
1510
 
1449
- **To fix:** Run \`npx hush encrypt\`
1511
+ **To fix:** Run \`npx hush migrate\` (if v4) or delete/gitignore these .env files
1450
1512
 
1451
1513
  ### npx hush inspect output explained
1452
1514
 
@@ -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;AAwCjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHzE"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAyCjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHzE"}
@@ -9,6 +9,7 @@ import { opInstalled } from '../lib/onepassword.js';
9
9
  import { FORMAT_OUTPUT_FILES } from '../types.js';
10
10
  function findRootPlaintextEnvFiles(root) {
11
11
  const results = [];
12
+ // Only warn about .env files (legacy/output), not .hush files (Hush's source files)
12
13
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
13
14
  for (const pattern of plaintextPatterns) {
14
15
  const filePath = join(root, pattern);
@@ -58,8 +59,8 @@ export async function statusCommand(options) {
58
59
  console.log('');
59
60
  console.log(pc.yellow('These files may expose secrets to AI assistants and version control.'));
60
61
  console.log(pc.bold('\nTo fix:'));
61
- console.log(pc.dim(' 1. Run: npx hush encrypt'));
62
- console.log(pc.dim(' 2. The plaintext files will be automatically deleted after encryption'));
62
+ console.log(pc.dim(' 1. Run: npx hush migrate (if upgrading from v4)'));
63
+ console.log(pc.dim(' 2. Delete or gitignore these .env files'));
63
64
  console.log(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars\n'));
64
65
  }
65
66
  console.log(pc.bold('Config:'));
@@ -118,10 +119,10 @@ export async function statusCommand(options) {
118
119
  ? pc.green(` ${label}`)
119
120
  : pc.dim(` ${label} (not found)`));
120
121
  }
121
- const localPath = join(root, '.env.local');
122
- console.log(existsSync(localPath)
123
- ? pc.green(' .env.local (overrides)')
124
- : pc.dim(' .env.local (optional, not found)'));
122
+ const localEncryptedPath = join(root, config.sources.local + '.encrypted');
123
+ console.log(existsSync(localEncryptedPath)
124
+ ? pc.green(` ${config.sources.local}.encrypted (overrides)`)
125
+ : pc.dim(` ${config.sources.local}.encrypted (optional, not found)`));
125
126
  console.log(pc.bold('\nTargets:'));
126
127
  for (const target of config.targets) {
127
128
  const filter = describeFilter(target);
@@ -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/dist/types.js CHANGED
@@ -1,9 +1,9 @@
1
1
  export const CURRENT_SCHEMA_VERSION = 2;
2
2
  export const DEFAULT_SOURCES = {
3
- shared: '.env',
4
- development: '.env.development',
5
- production: '.env.production',
6
- local: '.env.local',
3
+ shared: '.hush',
4
+ development: '.hush.development',
5
+ production: '.hush.production',
6
+ local: '.hush.local',
7
7
  };
8
8
  export const FORMAT_OUTPUT_FILES = {
9
9
  dotenv: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "4.1.2",
3
+ "version": "5.0.0",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {