@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 +24 -7
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +3 -2
- package/dist/commands/encrypt.js +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +36 -8
- package/dist/commands/migrate.d.ts +6 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +116 -0
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +121 -19
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +109 -47
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +7 -6
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +16 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -4
- package/package.json +1 -1
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 .
|
|
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
|
|
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 (
|
|
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
|
|
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;
|
|
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"}
|
package/dist/commands/check.js
CHANGED
|
@@ -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
|
|
210
|
-
lines.push(pc.dim(' 2. Delete
|
|
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'));
|
package/dist/commands/encrypt.js
CHANGED
|
@@ -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 .
|
|
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;
|
|
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"}
|
package/dist/commands/init.js
CHANGED
|
@@ -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('
|
|
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('\
|
|
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 (
|
|
183
|
-
console.log(pc.green(' 1. npx hush
|
|
184
|
-
console.log(pc.dim(' 2. npx hush inspect') + pc.dim('
|
|
185
|
-
console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim('
|
|
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('
|
|
189
|
-
console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim('
|
|
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 @@
|
|
|
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":"
|
|
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"}
|
package/dist/commands/push.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
150
|
+
for (const target of pushableTargets) {
|
|
68
151
|
const targetDir = join(root, target.path);
|
|
69
|
-
const
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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;
|
|
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"}
|
package/dist/commands/skill.js
CHANGED
|
@@ -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 .
|
|
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
|
|
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
|
|
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
|
|
61
|
-
npx hush inspect # Confirm everything is
|
|
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: .
|
|
427
|
-
development: .
|
|
428
|
-
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 \`.
|
|
464
|
+
### Step 5: Create initial \`.hush\` files
|
|
465
465
|
|
|
466
|
-
Create \`.
|
|
466
|
+
Create \`.hush\` with shared secrets:
|
|
467
467
|
|
|
468
468
|
\`\`\`bash
|
|
469
|
-
# .
|
|
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 \`.
|
|
475
|
+
Create \`.hush.development\` for dev-specific values:
|
|
476
476
|
|
|
477
477
|
\`\`\`bash
|
|
478
|
-
# .
|
|
478
|
+
# .hush.development
|
|
479
479
|
DEBUG=true
|
|
480
480
|
LOG_LEVEL=debug
|
|
481
481
|
\`\`\`
|
|
482
482
|
|
|
483
|
-
Create \`.
|
|
483
|
+
Create \`.hush.production\` for production values:
|
|
484
484
|
|
|
485
485
|
\`\`\`bash
|
|
486
|
-
# .
|
|
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
|
-
- \`.
|
|
499
|
-
- \`.
|
|
500
|
-
- \`.
|
|
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
|
|
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
|
|
517
|
-
.env.development
|
|
518
|
-
.env.production
|
|
522
|
+
.env.*
|
|
519
523
|
.dev.vars
|
|
520
524
|
|
|
521
525
|
# Keep encrypted files (these ARE committed)
|
|
522
|
-
!.
|
|
523
|
-
!.
|
|
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 .
|
|
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
|
-
- [ ] \`.
|
|
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 \`.
|
|
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
|
|
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
|
|
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: .
|
|
981
|
-
development: .
|
|
982
|
-
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 (
|
|
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
|
|
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
|
|
1256
|
+
If you are using \`hush run -- wrangler dev\` and secrets are missing:
|
|
1236
1257
|
|
|
1237
|
-
**
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
**
|
|
1242
|
-
|
|
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
|
|
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
|
|
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;
|
|
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"}
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
|
62
|
-
console.log(pc.dim(' 2.
|
|
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
|
|
122
|
-
console.log(existsSync(
|
|
123
|
-
? pc.green(
|
|
124
|
-
: pc.dim(
|
|
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,
|
|
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"}
|
package/dist/config/loader.js
CHANGED
|
@@ -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;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAC7E,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,YAAY,CAAC;
|
|
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: '.
|
|
4
|
-
development: '.
|
|
5
|
-
production: '.
|
|
6
|
-
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: {
|