@chriscode/hush 4.1.1 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +11 -5
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +121 -19
- package/dist/commands/resolve.d.ts.map +1 -1
- package/dist/commands/resolve.js +30 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +32 -13
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +66 -11
- 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/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,7 @@ ${pc.bold('Commands:')}
|
|
|
40
40
|
inspect List all variables (masked values, AI-safe)
|
|
41
41
|
has <key> Check if a secret exists (exit 0 if set, 1 if not)
|
|
42
42
|
check Verify secrets are encrypted (for pre-commit hooks)
|
|
43
|
-
push Push secrets to Cloudflare Workers
|
|
43
|
+
push Push secrets to Cloudflare (Workers and Pages)
|
|
44
44
|
status Show configuration and status
|
|
45
45
|
skill Install Claude Code / OpenCode skill
|
|
46
46
|
keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
|
|
@@ -57,7 +57,7 @@ ${pc.bold('Advanced Commands:')}
|
|
|
57
57
|
${pc.bold('Options:')}
|
|
58
58
|
-e, --env <env> Environment: development or production (default: development)
|
|
59
59
|
-r, --root <dir> Root directory (default: current directory)
|
|
60
|
-
-t, --target <t> Target name from hush.yaml (run/resolve
|
|
60
|
+
-t, --target <t> Target name from hush.yaml (run/resolve/push)
|
|
61
61
|
-q, --quiet Suppress output (has/check commands)
|
|
62
62
|
--dry-run Preview changes without applying (push only)
|
|
63
63
|
--verbose Show detailed output (push --dry-run only)
|
|
@@ -79,6 +79,11 @@ ${pc.bold('Variable Expansion (v4+):')}
|
|
|
79
79
|
\${VAR:-default} Pull VAR, use default if missing
|
|
80
80
|
\${env:VAR} Read from system environment (CI, etc.)
|
|
81
81
|
|
|
82
|
+
Behavior:
|
|
83
|
+
- Template vars are merged with target filters (additive)
|
|
84
|
+
- Template vars take precedence over target filters
|
|
85
|
+
- Subdirectory templates are safe to commit
|
|
86
|
+
|
|
82
87
|
Example subdirectory template (apps/mobile/.env):
|
|
83
88
|
EXPO_PUBLIC_API_URL=\${API_URL}
|
|
84
89
|
PORT=\${PORT:-3000}
|
|
@@ -90,8 +95,8 @@ ${pc.bold('Examples:')}
|
|
|
90
95
|
hush encrypt Encrypt .env files
|
|
91
96
|
hush run -- npm start Run with secrets in memory (AI-safe!)
|
|
92
97
|
hush run -e prod -- npm build Run with production secrets
|
|
93
|
-
hush run -t api -- wrangler dev Run filtered for 'api' target
|
|
94
|
-
cd apps/mobile && hush run -- expo start Run from subdirectory
|
|
98
|
+
hush run -t api -- wrangler dev Run filtered for 'api' target (root secrets only)
|
|
99
|
+
cd apps/mobile && hush run -- expo start Run from subdirectory (applies template + target filters)
|
|
95
100
|
hush set DATABASE_URL Set a secret interactively (AI-safe)
|
|
96
101
|
hush set API_KEY --gui Set secret via macOS dialog (for AI agents)
|
|
97
102
|
hush set API_KEY -e prod Set a production secret
|
|
@@ -105,6 +110,7 @@ ${pc.bold('Examples:')}
|
|
|
105
110
|
hush has API_KEY -q && echo "API_KEY is configured"
|
|
106
111
|
hush check Verify secrets are encrypted
|
|
107
112
|
hush push --dry-run Preview push to Cloudflare
|
|
113
|
+
hush push -t app Push only the 'app' target
|
|
108
114
|
hush status Show current status
|
|
109
115
|
hush skill Install Claude skill (interactive)
|
|
110
116
|
`);
|
|
@@ -359,7 +365,7 @@ async function main() {
|
|
|
359
365
|
await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
|
|
360
366
|
break;
|
|
361
367
|
case 'push':
|
|
362
|
-
await pushCommand({ root, dryRun, verbose });
|
|
368
|
+
await pushCommand({ root, dryRun, verbose, target });
|
|
363
369
|
break;
|
|
364
370
|
case 'status':
|
|
365
371
|
await statusCommand({ root });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push.d.ts","sourceRoot":"","sources":["../../src/commands/push.ts"],"names":[],"mappings":"
|
|
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":"resolve.d.ts","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAU,WAAW,EAAU,MAAM,aAAa,CAAC;AAG/D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AA6BD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmI3E"}
|
package/dist/commands/resolve.js
CHANGED
|
@@ -6,6 +6,7 @@ import { interpolateVars } from '../core/interpolate.js';
|
|
|
6
6
|
import { mergeVars } from '../core/merge.js';
|
|
7
7
|
import { parseEnvContent } from '../core/parse.js';
|
|
8
8
|
import { decrypt as sopsDecrypt } from '../core/sops.js';
|
|
9
|
+
import { loadLocalTemplates } from '../core/template.js';
|
|
9
10
|
import { FORMAT_OUTPUT_FILES } from '../types.js';
|
|
10
11
|
function matchesPattern(key, pattern) {
|
|
11
12
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
@@ -87,7 +88,7 @@ export async function resolveCommand(options) {
|
|
|
87
88
|
console.log(`Path: ${pc.dim(target.path + '/')}`);
|
|
88
89
|
console.log(`Format: ${pc.dim(target.format)} ${pc.dim(`(${outputFile})`)}`);
|
|
89
90
|
console.log(`Environment: ${pc.dim(env)}`);
|
|
90
|
-
console.log(pc.green(`\n✅
|
|
91
|
+
console.log(pc.green(`\n✅ ROOT SECRETS (Matched Filters) (${included.length}):`));
|
|
91
92
|
if (included.length === 0) {
|
|
92
93
|
console.log(pc.dim(' (none)'));
|
|
93
94
|
}
|
|
@@ -107,5 +108,33 @@ export async function resolveCommand(options) {
|
|
|
107
108
|
console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(matches: ${v.pattern})`)}`);
|
|
108
109
|
}
|
|
109
110
|
}
|
|
111
|
+
const targetAbsPath = join(root, target.path);
|
|
112
|
+
const localTemplate = loadLocalTemplates(targetAbsPath, env);
|
|
113
|
+
if (localTemplate.hasTemplate) {
|
|
114
|
+
console.log(pc.blue(`\n📄 TEMPLATE EXPANSIONS (${pc.dim(join(target.path, '.env'))}):`));
|
|
115
|
+
const maxKeyLen = Math.max(...localTemplate.vars.map(v => v.key.length));
|
|
116
|
+
for (const v of localTemplate.vars) {
|
|
117
|
+
console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`← ${v.value}`)}`);
|
|
118
|
+
}
|
|
119
|
+
// Calculate final merged list for clarity
|
|
120
|
+
const finalKeys = new Set([
|
|
121
|
+
...included.map(v => v.key),
|
|
122
|
+
...localTemplate.vars.map(v => v.key)
|
|
123
|
+
]);
|
|
124
|
+
console.log(pc.magenta(`\n📦 FINAL INJECTION (${finalKeys.size} total):`));
|
|
125
|
+
const sortedKeys = Array.from(finalKeys).sort();
|
|
126
|
+
for (const key of sortedKeys) {
|
|
127
|
+
const isTemplate = localTemplate.vars.some(v => v.key === key);
|
|
128
|
+
const isRoot = included.some(v => v.key === key);
|
|
129
|
+
let sourceInfo = '';
|
|
130
|
+
if (isTemplate && isRoot)
|
|
131
|
+
sourceInfo = pc.dim('(template overrides root)');
|
|
132
|
+
else if (isTemplate)
|
|
133
|
+
sourceInfo = pc.dim('(template)');
|
|
134
|
+
else if (isRoot)
|
|
135
|
+
sourceInfo = pc.dim('(root)');
|
|
136
|
+
console.log(` ${key} ${sourceInfo}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
110
139
|
console.log('');
|
|
111
140
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAkD/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAkD/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA8GnE"}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import { loadConfig, findProjectRoot } from '../config/loader.js';
|
|
6
6
|
import { filterVarsForTarget } from '../core/filter.js';
|
|
@@ -67,20 +67,26 @@ export async function runCommand(options) {
|
|
|
67
67
|
const rootSecrets = getDecryptedSecrets(projectRoot, env, config);
|
|
68
68
|
const rootSecretsRecord = getRootSecretsAsRecord(rootSecrets);
|
|
69
69
|
const localTemplate = loadLocalTemplates(contextDir, env);
|
|
70
|
-
|
|
70
|
+
// 1. Resolve Template Vars
|
|
71
|
+
let templateVars = [];
|
|
71
72
|
if (localTemplate.hasTemplate) {
|
|
72
|
-
|
|
73
|
+
templateVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
// 2. Resolve Target Vars
|
|
76
|
+
let targetVars = [];
|
|
77
|
+
// Find target config: either explicit by name, or implicit by directory matching
|
|
78
|
+
const targetConfig = target
|
|
79
|
+
? config.targets.find(t => t.name === target)
|
|
80
|
+
: config.targets.find(t => resolve(projectRoot, t.path) === resolve(contextDir));
|
|
81
|
+
if (target && !targetConfig) {
|
|
82
|
+
console.error(pc.red(`Target "${target}" not found in hush.yaml`));
|
|
83
|
+
console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
if (targetConfig) {
|
|
87
|
+
targetVars = filterVarsForTarget(rootSecrets, targetConfig);
|
|
82
88
|
if (targetConfig.format === 'wrangler') {
|
|
83
|
-
|
|
89
|
+
targetVars.push({ key: 'CLOUDFLARE_INCLUDE_PROCESS_ENV', value: 'true' });
|
|
84
90
|
const devVarsPath = join(targetConfig.path, '.dev.vars');
|
|
85
91
|
const absDevVarsPath = join(projectRoot, devVarsPath);
|
|
86
92
|
if (existsSync(absDevVarsPath)) {
|
|
@@ -91,8 +97,21 @@ export async function runCommand(options) {
|
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
99
|
}
|
|
100
|
+
else if (!localTemplate.hasTemplate && !target) {
|
|
101
|
+
// If no template and no target matched (and not running explicit target), fallback to all secrets
|
|
102
|
+
// This maintains backward compatibility for running in root or non-target dirs without templates
|
|
103
|
+
targetVars = rootSecrets;
|
|
104
|
+
}
|
|
105
|
+
// 3. Merge (Template overrides Target)
|
|
106
|
+
let vars;
|
|
107
|
+
if (localTemplate.hasTemplate) {
|
|
108
|
+
// Merge target vars with template vars.
|
|
109
|
+
// Template vars take precedence over target vars.
|
|
110
|
+
// This allows "additive" behavior: get target vars + template vars.
|
|
111
|
+
vars = mergeVars(targetVars, templateVars);
|
|
112
|
+
}
|
|
94
113
|
else {
|
|
95
|
-
vars =
|
|
114
|
+
vars = targetVars;
|
|
96
115
|
}
|
|
97
116
|
const unresolved = getUnresolvedVars(vars);
|
|
98
117
|
if (unresolved.length > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8hDhD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
|
package/dist/commands/skill.js
CHANGED
|
@@ -155,7 +155,9 @@ Hush automatically:
|
|
|
155
155
|
2. Decrypts root secrets
|
|
156
156
|
3. Loads the local \`.env\` template
|
|
157
157
|
4. Resolves \`\${VAR}\` references against root secrets
|
|
158
|
-
5.
|
|
158
|
+
5. **Filters root secrets based on target config (include/exclude)**
|
|
159
|
+
6. **Merges them (Template overrides Target)**
|
|
160
|
+
7. Injects the result into your command
|
|
159
161
|
|
|
160
162
|
### Variable Expansion Syntax
|
|
161
163
|
|
|
@@ -721,14 +723,25 @@ hush status
|
|
|
721
723
|
|
|
722
724
|
### hush push
|
|
723
725
|
|
|
724
|
-
Push production secrets to Cloudflare Workers.
|
|
726
|
+
Push production secrets to Cloudflare (Workers and Pages).
|
|
725
727
|
|
|
726
728
|
\`\`\`bash
|
|
727
|
-
hush push # Push
|
|
729
|
+
hush push # Push all targets
|
|
730
|
+
hush push -t api # Push specific target
|
|
728
731
|
hush push --dry-run # Preview without pushing
|
|
729
732
|
hush push --dry-run --verbose # Detailed preview of what would be pushed
|
|
730
733
|
\`\`\`
|
|
731
734
|
|
|
735
|
+
**For Cloudflare Pages:** Add \`push_to\` configuration to your target:
|
|
736
|
+
\`\`\`yaml
|
|
737
|
+
targets:
|
|
738
|
+
- name: app
|
|
739
|
+
format: dotenv
|
|
740
|
+
push_to:
|
|
741
|
+
type: cloudflare-pages
|
|
742
|
+
project: my-pages-project
|
|
743
|
+
\`\`\`
|
|
744
|
+
|
|
732
745
|
---
|
|
733
746
|
|
|
734
747
|
## Debugging Commands
|
|
@@ -893,10 +906,11 @@ hush has DB_URL -q && hush has API_KEY -q && echo "All set"
|
|
|
893
906
|
|
|
894
907
|
### hush push
|
|
895
908
|
|
|
896
|
-
Push production secrets to Cloudflare Workers.
|
|
909
|
+
Push production secrets to Cloudflare (Workers and Pages).
|
|
897
910
|
|
|
898
911
|
\`\`\`bash
|
|
899
|
-
hush push # Push
|
|
912
|
+
hush push # Push all targets
|
|
913
|
+
hush push -t api # Push specific target
|
|
900
914
|
hush push --dry-run # Preview without pushing
|
|
901
915
|
\`\`\`
|
|
902
916
|
|
|
@@ -1230,14 +1244,35 @@ Look at the 🚫 EXCLUDED section to see which pattern is filtering out your var
|
|
|
1230
1244
|
|
|
1231
1245
|
### "Wrangler dev not seeing secrets"
|
|
1232
1246
|
|
|
1233
|
-
If you are using \`hush run -- wrangler dev\` and secrets are missing
|
|
1247
|
+
If you are using \`hush run -- wrangler dev\` and secrets are missing:
|
|
1234
1248
|
|
|
1235
|
-
**
|
|
1236
|
-
|
|
1237
|
-
|
|
1249
|
+
**Step 1: Check for blocking files**
|
|
1250
|
+
\`\`\`bash
|
|
1251
|
+
ls -la .dev.vars # If this exists, it blocks Hush secrets
|
|
1252
|
+
\`\`\`
|
|
1238
1253
|
|
|
1239
|
-
**
|
|
1240
|
-
|
|
1254
|
+
**Step 2: Delete the blocking file**
|
|
1255
|
+
\`\`\`bash
|
|
1256
|
+
rm .dev.vars
|
|
1257
|
+
\`\`\`
|
|
1258
|
+
|
|
1259
|
+
**Step 3: Run normally**
|
|
1260
|
+
\`\`\`bash
|
|
1261
|
+
npx hush run -t api -- wrangler dev
|
|
1262
|
+
\`\`\`
|
|
1263
|
+
|
|
1264
|
+
**Step 4: If still not working, update Wrangler**
|
|
1265
|
+
\`\`\`bash
|
|
1266
|
+
npm update wrangler
|
|
1267
|
+
\`\`\`
|
|
1268
|
+
|
|
1269
|
+
**Why this happens:**
|
|
1270
|
+
- Wrangler has a strict rule: if \`.dev.vars\` exists (even empty!), it ignores ALL environment variables
|
|
1271
|
+
- Hush automatically sets \`CLOUDFLARE_INCLUDE_PROCESS_ENV=true\` for you
|
|
1272
|
+
- But Wrangler only respects this when no \`.dev.vars\` file exists
|
|
1273
|
+
- Older Wrangler versions may not support \`CLOUDFLARE_INCLUDE_PROCESS_ENV\` at all
|
|
1274
|
+
|
|
1275
|
+
**Prevention tip:** Never use \`hush decrypt\` for Wrangler targets—always use \`hush run\`.
|
|
1241
1276
|
|
|
1242
1277
|
### "Variable appears in wrong places"
|
|
1243
1278
|
|
|
@@ -1290,6 +1325,26 @@ npx hush inspect # See what's new
|
|
|
1290
1325
|
\`\`\`bash
|
|
1291
1326
|
npx hush push --dry-run # Preview first
|
|
1292
1327
|
npx hush push # Actually push
|
|
1328
|
+
npx hush push -t api # Push specific target
|
|
1329
|
+
\`\`\`
|
|
1330
|
+
|
|
1331
|
+
### "Push to Cloudflare Pages"
|
|
1332
|
+
|
|
1333
|
+
First, add \`push_to\` to your target in \`hush.yaml\`:
|
|
1334
|
+
\`\`\`yaml
|
|
1335
|
+
targets:
|
|
1336
|
+
- name: app
|
|
1337
|
+
path: ./app
|
|
1338
|
+
format: dotenv
|
|
1339
|
+
push_to:
|
|
1340
|
+
type: cloudflare-pages
|
|
1341
|
+
project: my-pages-project
|
|
1342
|
+
\`\`\`
|
|
1343
|
+
|
|
1344
|
+
Then push:
|
|
1345
|
+
\`\`\`bash
|
|
1346
|
+
npx hush push -t app --dry-run # Preview first
|
|
1347
|
+
npx hush push -t app # Actually push
|
|
1293
1348
|
\`\`\`
|
|
1294
1349
|
|
|
1295
1350
|
### "Build and deploy"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK9C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ1D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAepG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAoBnD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAO5G;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,
|
|
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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"}
|