@chriscode/hush 4.1.0 → 4.1.2

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
@@ -79,6 +79,11 @@ ${pc.bold('Variable Expansion (v4+):')}
79
79
  \${VAR:-default} Pull VAR, use default if missing
80
80
  \${env:VAR} Read from system environment (CI, etc.)
81
81
 
82
+ Behavior:
83
+ - Template vars are merged with target filters (additive)
84
+ - Template vars take precedence over target filters
85
+ - Subdirectory templates are safe to commit
86
+
82
87
  Example subdirectory template (apps/mobile/.env):
83
88
  EXPO_PUBLIC_API_URL=\${API_URL}
84
89
  PORT=\${PORT:-3000}
@@ -90,8 +95,8 @@ ${pc.bold('Examples:')}
90
95
  hush encrypt Encrypt .env files
91
96
  hush run -- npm start Run with secrets in memory (AI-safe!)
92
97
  hush run -e prod -- npm build Run with production secrets
93
- hush run -t api -- wrangler dev Run filtered for 'api' target
94
- cd apps/mobile && hush run -- expo start Run from subdirectory with templates
98
+ hush run -t api -- wrangler dev Run filtered for 'api' target (root secrets only)
99
+ cd apps/mobile && hush run -- expo start Run from subdirectory (applies template + target filters)
95
100
  hush set DATABASE_URL Set a secret interactively (AI-safe)
96
101
  hush set API_KEY --gui Set secret via macOS dialog (for AI agents)
97
102
  hush set API_KEY -e prod Set a production secret
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,WAAW,EAAU,MAAM,aAAa,CAAC;AAG/D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AA6BD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmG3E"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/commands/resolve.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAU,WAAW,EAAU,MAAM,aAAa,CAAC;AAG/D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AA6BD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAmI3E"}
@@ -6,6 +6,7 @@ import { interpolateVars } from '../core/interpolate.js';
6
6
  import { mergeVars } from '../core/merge.js';
7
7
  import { parseEnvContent } from '../core/parse.js';
8
8
  import { decrypt as sopsDecrypt } from '../core/sops.js';
9
+ import { loadLocalTemplates } from '../core/template.js';
9
10
  import { FORMAT_OUTPUT_FILES } from '../types.js';
10
11
  function matchesPattern(key, pattern) {
11
12
  const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
@@ -87,7 +88,7 @@ export async function resolveCommand(options) {
87
88
  console.log(`Path: ${pc.dim(target.path + '/')}`);
88
89
  console.log(`Format: ${pc.dim(target.format)} ${pc.dim(`(${outputFile})`)}`);
89
90
  console.log(`Environment: ${pc.dim(env)}`);
90
- console.log(pc.green(`\n✅ INCLUDED VARIABLES (${included.length}):`));
91
+ console.log(pc.green(`\n✅ ROOT SECRETS (Matched Filters) (${included.length}):`));
91
92
  if (included.length === 0) {
92
93
  console.log(pc.dim(' (none)'));
93
94
  }
@@ -107,5 +108,33 @@ export async function resolveCommand(options) {
107
108
  console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(matches: ${v.pattern})`)}`);
108
109
  }
109
110
  }
111
+ const targetAbsPath = join(root, target.path);
112
+ const localTemplate = loadLocalTemplates(targetAbsPath, env);
113
+ if (localTemplate.hasTemplate) {
114
+ console.log(pc.blue(`\n📄 TEMPLATE EXPANSIONS (${pc.dim(join(target.path, '.env'))}):`));
115
+ const maxKeyLen = Math.max(...localTemplate.vars.map(v => v.key.length));
116
+ for (const v of localTemplate.vars) {
117
+ console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`← ${v.value}`)}`);
118
+ }
119
+ // Calculate final merged list for clarity
120
+ const finalKeys = new Set([
121
+ ...included.map(v => v.key),
122
+ ...localTemplate.vars.map(v => v.key)
123
+ ]);
124
+ console.log(pc.magenta(`\n📦 FINAL INJECTION (${finalKeys.size} total):`));
125
+ const sortedKeys = Array.from(finalKeys).sort();
126
+ for (const key of sortedKeys) {
127
+ const isTemplate = localTemplate.vars.some(v => v.key === key);
128
+ const isRoot = included.some(v => v.key === key);
129
+ let sourceInfo = '';
130
+ if (isTemplate && isRoot)
131
+ sourceInfo = pc.dim('(template overrides root)');
132
+ else if (isTemplate)
133
+ sourceInfo = pc.dim('(template)');
134
+ else if (isRoot)
135
+ sourceInfo = pc.dim('(root)');
136
+ console.log(` ${key} ${sourceInfo}`);
137
+ }
138
+ }
110
139
  console.log('');
111
140
  }
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAkD/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFnE"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAkD/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA8GnE"}
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
- import { join } from 'node:path';
3
+ import { join, resolve } from 'node:path';
4
4
  import pc from 'picocolors';
5
5
  import { loadConfig, findProjectRoot } from '../config/loader.js';
6
6
  import { filterVarsForTarget } from '../core/filter.js';
@@ -67,20 +67,26 @@ export async function runCommand(options) {
67
67
  const rootSecrets = getDecryptedSecrets(projectRoot, env, config);
68
68
  const rootSecretsRecord = getRootSecretsAsRecord(rootSecrets);
69
69
  const localTemplate = loadLocalTemplates(contextDir, env);
70
- let vars;
70
+ // 1. Resolve Template Vars
71
+ let templateVars = [];
71
72
  if (localTemplate.hasTemplate) {
72
- vars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
73
+ templateVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
73
74
  }
74
- else if (target) {
75
- const targetConfig = config.targets.find(t => t.name === target);
76
- if (!targetConfig) {
77
- console.error(pc.red(`Target "${target}" not found in hush.yaml`));
78
- console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
79
- process.exit(1);
80
- }
81
- vars = filterVarsForTarget(rootSecrets, targetConfig);
75
+ // 2. Resolve Target Vars
76
+ let targetVars = [];
77
+ // Find target config: either explicit by name, or implicit by directory matching
78
+ const targetConfig = target
79
+ ? config.targets.find(t => t.name === target)
80
+ : config.targets.find(t => resolve(projectRoot, t.path) === resolve(contextDir));
81
+ if (target && !targetConfig) {
82
+ console.error(pc.red(`Target "${target}" not found in hush.yaml`));
83
+ console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
84
+ process.exit(1);
85
+ }
86
+ if (targetConfig) {
87
+ targetVars = filterVarsForTarget(rootSecrets, targetConfig);
82
88
  if (targetConfig.format === 'wrangler') {
83
- vars.push({ key: 'CLOUDFLARE_INCLUDE_PROCESS_ENV', value: 'true' });
89
+ targetVars.push({ key: 'CLOUDFLARE_INCLUDE_PROCESS_ENV', value: 'true' });
84
90
  const devVarsPath = join(targetConfig.path, '.dev.vars');
85
91
  const absDevVarsPath = join(projectRoot, devVarsPath);
86
92
  if (existsSync(absDevVarsPath)) {
@@ -91,8 +97,21 @@ export async function runCommand(options) {
91
97
  }
92
98
  }
93
99
  }
100
+ else if (!localTemplate.hasTemplate && !target) {
101
+ // If no template and no target matched (and not running explicit target), fallback to all secrets
102
+ // This maintains backward compatibility for running in root or non-target dirs without templates
103
+ targetVars = rootSecrets;
104
+ }
105
+ // 3. Merge (Template overrides Target)
106
+ let vars;
107
+ if (localTemplate.hasTemplate) {
108
+ // Merge target vars with template vars.
109
+ // Template vars take precedence over target vars.
110
+ // This allows "additive" behavior: get target vars + template vars.
111
+ vars = mergeVars(targetVars, templateVars);
112
+ }
94
113
  else {
95
- vars = rootSecrets;
114
+ vars = targetVars;
96
115
  }
97
116
  const unresolved = getUnresolvedVars(vars);
98
117
  if (unresolved.length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAu+ChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
1
+ {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAy+ChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
@@ -155,7 +155,9 @@ Hush automatically:
155
155
  2. Decrypts root secrets
156
156
  3. Loads the local \`.env\` template
157
157
  4. Resolves \`\${VAR}\` references against root secrets
158
- 5. Injects the result into your command
158
+ 5. **Filters root secrets based on target config (include/exclude)**
159
+ 6. **Merges them (Template overrides Target)**
160
+ 7. Injects the result into your command
159
161
 
160
162
  ### Variable Expansion Syntax
161
163
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {