@chriscode/hush 2.8.2 → 2.9.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 +40 -4
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +18 -5
- package/dist/commands/resolve.d.ts +8 -0
- package/dist/commands/resolve.d.ts.map +1 -0
- package/dist/commands/resolve.js +111 -0
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +113 -10
- package/dist/commands/trace.d.ts +8 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +87 -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
|
@@ -15,6 +15,8 @@ import { hasCommand } from './commands/has.js';
|
|
|
15
15
|
import { checkCommand } from './commands/check.js';
|
|
16
16
|
import { skillCommand } from './commands/skill.js';
|
|
17
17
|
import { keysCommand } from './commands/keys.js';
|
|
18
|
+
import { resolveCommand } from './commands/resolve.js';
|
|
19
|
+
import { traceCommand } from './commands/trace.js';
|
|
18
20
|
import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
|
|
19
21
|
import { checkForUpdate } from './utils/version-check.js';
|
|
20
22
|
const require = createRequire(import.meta.url);
|
|
@@ -40,6 +42,10 @@ ${pc.bold('Commands:')}
|
|
|
40
42
|
status Show configuration and status
|
|
41
43
|
skill Install Claude Code / OpenCode skill
|
|
42
44
|
keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
|
|
45
|
+
|
|
46
|
+
${pc.bold('Debugging Commands:')}
|
|
47
|
+
resolve <target> Show what variables a target receives (AI-safe)
|
|
48
|
+
trace <key> Trace a variable through sources and targets (AI-safe)
|
|
43
49
|
|
|
44
50
|
${pc.bold('Deprecated Commands:')}
|
|
45
51
|
decrypt Write secrets to disk (unsafe - use 'run' instead)
|
|
@@ -47,9 +53,10 @@ ${pc.bold('Deprecated Commands:')}
|
|
|
47
53
|
${pc.bold('Options:')}
|
|
48
54
|
-e, --env <env> Environment: development or production (default: development)
|
|
49
55
|
-r, --root <dir> Root directory (default: current directory)
|
|
50
|
-
-t, --target <t> Target name from hush.yaml (run only)
|
|
56
|
+
-t, --target <t> Target name from hush.yaml (run/resolve only)
|
|
51
57
|
-q, --quiet Suppress output (has/check commands)
|
|
52
58
|
--dry-run Preview changes without applying (push only)
|
|
59
|
+
--verbose Show detailed output (push --dry-run only)
|
|
53
60
|
--warn Warn but exit 0 on drift (check only)
|
|
54
61
|
--json Output machine-readable JSON (check only)
|
|
55
62
|
--only-changed Only check git-modified files (check only)
|
|
@@ -107,6 +114,7 @@ function parseArgs(args) {
|
|
|
107
114
|
let envExplicit = false;
|
|
108
115
|
let root = process.cwd();
|
|
109
116
|
let dryRun = false;
|
|
117
|
+
let verbose = false;
|
|
110
118
|
let quiet = false;
|
|
111
119
|
let warn = false;
|
|
112
120
|
let json = false;
|
|
@@ -154,6 +162,10 @@ function parseArgs(args) {
|
|
|
154
162
|
dryRun = true;
|
|
155
163
|
continue;
|
|
156
164
|
}
|
|
165
|
+
if (arg === '--verbose') {
|
|
166
|
+
verbose = true;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
157
169
|
if (arg === '-q' || arg === '--quiet') {
|
|
158
170
|
quiet = true;
|
|
159
171
|
continue;
|
|
@@ -230,12 +242,20 @@ function parseArgs(args) {
|
|
|
230
242
|
key = arg;
|
|
231
243
|
continue;
|
|
232
244
|
}
|
|
245
|
+
if (command === 'trace' && !arg.startsWith('-') && !key) {
|
|
246
|
+
key = arg;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (command === 'resolve' && !arg.startsWith('-') && !target) {
|
|
250
|
+
target = arg;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
233
253
|
if (command === 'keys' && !arg.startsWith('-') && !subcommand) {
|
|
234
254
|
subcommand = arg;
|
|
235
255
|
continue;
|
|
236
256
|
}
|
|
237
257
|
}
|
|
238
|
-
return { command, subcommand, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs };
|
|
258
|
+
return { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs };
|
|
239
259
|
}
|
|
240
260
|
function checkMigrationNeeded(root, command) {
|
|
241
261
|
const skipCommands = ['', 'help', 'version', 'init', 'skill'];
|
|
@@ -271,7 +291,7 @@ async function main() {
|
|
|
271
291
|
printHelp();
|
|
272
292
|
process.exit(0);
|
|
273
293
|
}
|
|
274
|
-
const { command, subcommand, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs } = parseArgs(args);
|
|
294
|
+
const { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs } = parseArgs(args);
|
|
275
295
|
if (command !== 'run' && !json && !quiet) {
|
|
276
296
|
checkForUpdate(VERSION);
|
|
277
297
|
}
|
|
@@ -331,7 +351,7 @@ async function main() {
|
|
|
331
351
|
await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
|
|
332
352
|
break;
|
|
333
353
|
case 'push':
|
|
334
|
-
await pushCommand({ root, dryRun });
|
|
354
|
+
await pushCommand({ root, dryRun, verbose });
|
|
335
355
|
break;
|
|
336
356
|
case 'status':
|
|
337
357
|
await statusCommand({ root });
|
|
@@ -347,6 +367,22 @@ async function main() {
|
|
|
347
367
|
}
|
|
348
368
|
await keysCommand({ root, subcommand, vault, force });
|
|
349
369
|
break;
|
|
370
|
+
case 'resolve':
|
|
371
|
+
if (!target) {
|
|
372
|
+
console.error(pc.red('Usage: hush resolve <target>'));
|
|
373
|
+
console.error(pc.dim('Example: hush resolve api-workers'));
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
await resolveCommand({ root, env, target });
|
|
377
|
+
break;
|
|
378
|
+
case 'trace':
|
|
379
|
+
if (!key) {
|
|
380
|
+
console.error(pc.red('Usage: hush trace <KEY>'));
|
|
381
|
+
console.error(pc.dim('Example: hush trace DATABASE_URL'));
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
await traceCommand({ root, env, key });
|
|
385
|
+
break;
|
|
350
386
|
default:
|
|
351
387
|
if (command) {
|
|
352
388
|
console.error(pc.red(`Unknown command: ${command}`));
|
|
@@ -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;
|
|
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"}
|
package/dist/commands/push.js
CHANGED
|
@@ -8,9 +8,14 @@ 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) {
|
|
11
|
+
function pushSecret(key, value, targetDir, dryRun, verbose) {
|
|
12
12
|
if (dryRun) {
|
|
13
|
-
|
|
13
|
+
if (verbose) {
|
|
14
|
+
console.log(pc.green(` + ${key}`));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(pc.dim(` [dry-run] ${key}`));
|
|
18
|
+
}
|
|
14
19
|
return true;
|
|
15
20
|
}
|
|
16
21
|
try {
|
|
@@ -28,11 +33,14 @@ function pushSecret(key, value, targetDir, dryRun) {
|
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
export async function pushCommand(options) {
|
|
31
|
-
const { root, dryRun } = options;
|
|
36
|
+
const { root, dryRun, verbose } = options;
|
|
32
37
|
const config = loadConfig(root);
|
|
33
38
|
console.log(pc.blue('Pushing production secrets to Cloudflare Workers...'));
|
|
34
39
|
if (dryRun) {
|
|
35
40
|
console.log(pc.yellow('(dry-run mode)'));
|
|
41
|
+
if (verbose) {
|
|
42
|
+
console.log(pc.dim('(verbose output enabled)'));
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
const sharedEncrypted = join(root, config.sources.shared + '.encrypted');
|
|
38
46
|
const prodEncrypted = join(root, config.sources.production + '.encrypted');
|
|
@@ -59,11 +67,16 @@ export async function pushCommand(options) {
|
|
|
59
67
|
for (const target of wranglerTargets) {
|
|
60
68
|
const targetDir = join(root, target.path);
|
|
61
69
|
const filtered = filterVarsForTarget(interpolated, target);
|
|
62
|
-
|
|
70
|
+
if (dryRun && verbose) {
|
|
71
|
+
console.log(pc.blue(`\n[DRY RUN] Would push to ${target.name} (${target.path}/):`));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.log(pc.blue(`\n${target.name} (${target.path}/)`));
|
|
75
|
+
}
|
|
63
76
|
let success = 0;
|
|
64
77
|
let failed = 0;
|
|
65
78
|
for (const { key, value } of filtered) {
|
|
66
|
-
if (pushSecret(key, value, targetDir, dryRun)) {
|
|
79
|
+
if (pushSecret(key, value, targetDir, dryRun, verbose)) {
|
|
67
80
|
if (!dryRun)
|
|
68
81
|
console.log(pc.green(` ${key}`));
|
|
69
82
|
success++;
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { loadConfig } from '../config/loader.js';
|
|
5
|
+
import { interpolateVars } from '../core/interpolate.js';
|
|
6
|
+
import { mergeVars } from '../core/merge.js';
|
|
7
|
+
import { parseEnvContent } from '../core/parse.js';
|
|
8
|
+
import { decrypt as sopsDecrypt } from '../core/sops.js';
|
|
9
|
+
import { FORMAT_OUTPUT_FILES } from '../types.js';
|
|
10
|
+
function matchesPattern(key, pattern) {
|
|
11
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
12
|
+
return regex.test(key);
|
|
13
|
+
}
|
|
14
|
+
function matchesAnyPattern(key, patterns) {
|
|
15
|
+
for (const pattern of patterns) {
|
|
16
|
+
if (matchesPattern(key, pattern)) {
|
|
17
|
+
return pattern;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function getOutputFilename(target, env) {
|
|
23
|
+
if (target.filename) {
|
|
24
|
+
return target.filename;
|
|
25
|
+
}
|
|
26
|
+
return FORMAT_OUTPUT_FILES[target.format][env];
|
|
27
|
+
}
|
|
28
|
+
export async function resolveCommand(options) {
|
|
29
|
+
const { root, env, target: targetName } = options;
|
|
30
|
+
const config = loadConfig(root);
|
|
31
|
+
const target = config.targets.find(t => t.name === targetName);
|
|
32
|
+
if (!target) {
|
|
33
|
+
console.error(pc.red(`Target not found: ${targetName}`));
|
|
34
|
+
console.error(pc.dim('Available targets: ' + config.targets.map(t => t.name).join(', ')));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const sharedEncrypted = join(root, config.sources.shared + '.encrypted');
|
|
38
|
+
const envEncrypted = join(root, config.sources[env] + '.encrypted');
|
|
39
|
+
const localEncrypted = join(root, config.sources.local + '.encrypted');
|
|
40
|
+
const varsBySource = new Map();
|
|
41
|
+
const allVars = new Map();
|
|
42
|
+
const loadSource = (path, sourceName) => {
|
|
43
|
+
if (!existsSync(path))
|
|
44
|
+
return;
|
|
45
|
+
const content = sopsDecrypt(path);
|
|
46
|
+
const vars = parseEnvContent(content);
|
|
47
|
+
const sourceVars = [];
|
|
48
|
+
for (const v of vars) {
|
|
49
|
+
const varSource = { key: v.key, value: v.value, source: sourceName };
|
|
50
|
+
sourceVars.push(varSource);
|
|
51
|
+
allVars.set(v.key, varSource);
|
|
52
|
+
}
|
|
53
|
+
varsBySource.set(sourceName, sourceVars);
|
|
54
|
+
};
|
|
55
|
+
loadSource(sharedEncrypted, config.sources.shared);
|
|
56
|
+
loadSource(envEncrypted, config.sources[env]);
|
|
57
|
+
loadSource(localEncrypted, config.sources.local);
|
|
58
|
+
if (allVars.size === 0) {
|
|
59
|
+
console.error(pc.red('No encrypted files found'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const merged = mergeVars(...Array.from(varsBySource.values()).map(sources => sources.map(s => ({ key: s.key, value: s.value }))));
|
|
63
|
+
const interpolated = interpolateVars(merged);
|
|
64
|
+
const included = [];
|
|
65
|
+
const excluded = [];
|
|
66
|
+
for (const v of interpolated) {
|
|
67
|
+
const varSource = allVars.get(v.key);
|
|
68
|
+
const source = varSource?.source || 'unknown';
|
|
69
|
+
if (target.include && target.include.length > 0) {
|
|
70
|
+
const matchedInclude = matchesAnyPattern(v.key, target.include);
|
|
71
|
+
if (!matchedInclude) {
|
|
72
|
+
excluded.push({ key: v.key, pattern: `not in include: ${target.include.join(', ')}` });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (target.exclude && target.exclude.length > 0) {
|
|
77
|
+
const matchedExclude = matchesAnyPattern(v.key, target.exclude);
|
|
78
|
+
if (matchedExclude) {
|
|
79
|
+
excluded.push({ key: v.key, pattern: matchedExclude });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
included.push({ key: v.key, source });
|
|
84
|
+
}
|
|
85
|
+
const outputFile = getOutputFilename(target, env);
|
|
86
|
+
console.log(pc.bold(`\nTarget: ${pc.cyan(target.name)}`));
|
|
87
|
+
console.log(`Path: ${pc.dim(target.path + '/')}`);
|
|
88
|
+
console.log(`Format: ${pc.dim(target.format)} ${pc.dim(`(${outputFile})`)}`);
|
|
89
|
+
console.log(`Environment: ${pc.dim(env)}`);
|
|
90
|
+
console.log(pc.green(`\n✅ INCLUDED VARIABLES (${included.length}):`));
|
|
91
|
+
if (included.length === 0) {
|
|
92
|
+
console.log(pc.dim(' (none)'));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const maxKeyLen = Math.max(...included.map(v => v.key.length));
|
|
96
|
+
for (const v of included) {
|
|
97
|
+
console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(source: ${v.source})`)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log(pc.red(`\n🚫 EXCLUDED VARIABLES (${excluded.length}):`));
|
|
101
|
+
if (excluded.length === 0) {
|
|
102
|
+
console.log(pc.dim(' (none)'));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const maxKeyLen = Math.max(...excluded.map(v => v.key.length));
|
|
106
|
+
for (const v of excluded) {
|
|
107
|
+
console.log(` ${v.key.padEnd(maxKeyLen)} ${pc.dim(`(matches: ${v.pattern})`)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
console.log('');
|
|
111
|
+
}
|
|
@@ -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;AAkqChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
|
package/dist/commands/skill.js
CHANGED
|
@@ -105,6 +105,41 @@ npx hush run -e production -- npm build # Production
|
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
108
|
+
## Debugging Secret Issues
|
|
109
|
+
|
|
110
|
+
When a variable is missing or not appearing where expected, use these commands:
|
|
111
|
+
|
|
112
|
+
### Why is my variable missing from a target?
|
|
113
|
+
|
|
114
|
+
\`\`\`bash
|
|
115
|
+
npx hush resolve <target> # See what variables a target receives
|
|
116
|
+
npx hush resolve api-workers # Example: check api-workers target
|
|
117
|
+
npx hush resolve api-workers -e prod # Check with production env
|
|
118
|
+
\`\`\`
|
|
119
|
+
|
|
120
|
+
This shows:
|
|
121
|
+
- ✅ **Included variables** - what the target will receive
|
|
122
|
+
- 🚫 **Excluded variables** - what was filtered out and WHY (which pattern matched)
|
|
123
|
+
|
|
124
|
+
### Where does a specific variable go?
|
|
125
|
+
|
|
126
|
+
\`\`\`bash
|
|
127
|
+
npx hush trace <KEY> # Trace a variable through all targets
|
|
128
|
+
npx hush trace DATABASE_URL # Example: trace DATABASE_URL
|
|
129
|
+
\`\`\`
|
|
130
|
+
|
|
131
|
+
This shows:
|
|
132
|
+
- Which source files contain the variable
|
|
133
|
+
- Which targets include/exclude it and why
|
|
134
|
+
|
|
135
|
+
### Preview what would be pushed
|
|
136
|
+
|
|
137
|
+
\`\`\`bash
|
|
138
|
+
npx hush push --dry-run --verbose # See exactly what would be pushed
|
|
139
|
+
\`\`\`
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
108
143
|
## Running Programs with Secrets
|
|
109
144
|
|
|
110
145
|
**This is the primary way to use secrets - they never touch disk!**
|
|
@@ -413,6 +448,16 @@ Run \`npx hush init\` to generate configuration.
|
|
|
413
448
|
|
|
414
449
|
### "No sources defined in hush.yaml"
|
|
415
450
|
Edit \`hush.yaml\` and add your source files under \`sources:\`.
|
|
451
|
+
|
|
452
|
+
### "npm warn Unknown project config node-linker"
|
|
453
|
+
This warning appears when running \`npx hush\` in a pnpm workspace because npm doesn't recognize pnpm-specific config in \`.npmrc\`.
|
|
454
|
+
|
|
455
|
+
**Fix:** Add \`loglevel=error\` to the project's \`.npmrc\`:
|
|
456
|
+
\`\`\`bash
|
|
457
|
+
echo "loglevel=error" >> .npmrc
|
|
458
|
+
\`\`\`
|
|
459
|
+
|
|
460
|
+
This suppresses npm warnings while still showing errors. This is a per-project fix for any project using pnpm.
|
|
416
461
|
`,
|
|
417
462
|
'REFERENCE.md': `# Hush Command Reference
|
|
418
463
|
|
|
@@ -548,8 +593,45 @@ Push production secrets to Cloudflare Workers.
|
|
|
548
593
|
\`\`\`bash
|
|
549
594
|
hush push # Push secrets
|
|
550
595
|
hush push --dry-run # Preview without pushing
|
|
596
|
+
hush push --dry-run --verbose # Detailed preview of what would be pushed
|
|
597
|
+
\`\`\`
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## Debugging Commands
|
|
602
|
+
|
|
603
|
+
### hush resolve <target>
|
|
604
|
+
|
|
605
|
+
Show what variables a specific target will receive, with filtering details.
|
|
606
|
+
|
|
607
|
+
\`\`\`bash
|
|
608
|
+
hush resolve api-workers # Check api-workers target
|
|
609
|
+
hush resolve api-workers -e prod # Check with production environment
|
|
551
610
|
\`\`\`
|
|
552
611
|
|
|
612
|
+
**Output shows:**
|
|
613
|
+
- ✅ Included variables (with source file)
|
|
614
|
+
- 🚫 Excluded variables (with matching pattern)
|
|
615
|
+
|
|
616
|
+
**Use when:** A target is missing expected variables
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
### hush trace <KEY>
|
|
621
|
+
|
|
622
|
+
Trace a specific variable through all sources and targets.
|
|
623
|
+
|
|
624
|
+
\`\`\`bash
|
|
625
|
+
hush trace DATABASE_URL # Trace DATABASE_URL
|
|
626
|
+
hush trace STRIPE_SECRET_KEY # Trace another variable
|
|
627
|
+
\`\`\`
|
|
628
|
+
|
|
629
|
+
**Output shows:**
|
|
630
|
+
- Which source files contain the variable
|
|
631
|
+
- Which targets include/exclude it (and why)
|
|
632
|
+
|
|
633
|
+
**Use when:** You need to understand why a variable appears in some places but not others
|
|
634
|
+
|
|
553
635
|
---
|
|
554
636
|
|
|
555
637
|
## Deprecated Commands (Avoid)
|
|
@@ -923,25 +1005,46 @@ Tell user: "Your editor will open. Add or modify secrets, then save and close."
|
|
|
923
1005
|
|
|
924
1006
|
### "My app can't find DATABASE_URL"
|
|
925
1007
|
|
|
926
|
-
1.
|
|
1008
|
+
1. **Trace the variable** to see where it exists and where it goes:
|
|
927
1009
|
\`\`\`bash
|
|
928
|
-
npx hush
|
|
1010
|
+
npx hush trace DATABASE_URL
|
|
929
1011
|
\`\`\`
|
|
1012
|
+
This shows which source files have it and which targets include/exclude it.
|
|
930
1013
|
|
|
931
|
-
2. Check
|
|
1014
|
+
2. **Check if it exists** in your current environment:
|
|
932
1015
|
\`\`\`bash
|
|
933
|
-
npx hush
|
|
1016
|
+
npx hush has DATABASE_URL
|
|
934
1017
|
\`\`\`
|
|
935
1018
|
|
|
936
|
-
3.
|
|
1019
|
+
3. **Resolve the target** to see what variables it receives:
|
|
937
1020
|
\`\`\`bash
|
|
938
|
-
|
|
1021
|
+
npx hush resolve api-workers
|
|
939
1022
|
\`\`\`
|
|
940
1023
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1024
|
+
### "Target is missing expected variables"
|
|
1025
|
+
|
|
1026
|
+
\`\`\`bash
|
|
1027
|
+
npx hush resolve <target-name> # See included/excluded variables
|
|
1028
|
+
npx hush resolve <target-name> -e prod # Check production
|
|
1029
|
+
\`\`\`
|
|
1030
|
+
|
|
1031
|
+
Look at the 🚫 EXCLUDED section to see which pattern is filtering out your variable.
|
|
1032
|
+
|
|
1033
|
+
### "Variable appears in wrong places"
|
|
1034
|
+
|
|
1035
|
+
\`\`\`bash
|
|
1036
|
+
npx hush trace <VARIABLE_NAME>
|
|
1037
|
+
\`\`\`
|
|
1038
|
+
|
|
1039
|
+
This shows the full disposition across all targets - which include it and which exclude it.
|
|
1040
|
+
|
|
1041
|
+
### "Push is missing some secrets"
|
|
1042
|
+
|
|
1043
|
+
\`\`\`bash
|
|
1044
|
+
npx hush push --dry-run --verbose
|
|
1045
|
+
\`\`\`
|
|
1046
|
+
|
|
1047
|
+
This shows exactly what would be pushed to each target.
|
|
945
1048
|
|
|
946
1049
|
---
|
|
947
1050
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/commands/trace.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAU,MAAM,aAAa,CAAC;AAEvD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAuCD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8DvE"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { loadConfig } from '../config/loader.js';
|
|
5
|
+
import { parseEnvContent } from '../core/parse.js';
|
|
6
|
+
import { decrypt as sopsDecrypt } from '../core/sops.js';
|
|
7
|
+
function matchesPattern(key, pattern) {
|
|
8
|
+
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
9
|
+
return regex.test(key);
|
|
10
|
+
}
|
|
11
|
+
function matchesAnyPattern(key, patterns) {
|
|
12
|
+
for (const pattern of patterns) {
|
|
13
|
+
if (matchesPattern(key, pattern)) {
|
|
14
|
+
return pattern;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function getTargetDisposition(key, target) {
|
|
20
|
+
if (target.include && target.include.length > 0) {
|
|
21
|
+
const matchedInclude = matchesAnyPattern(key, target.include);
|
|
22
|
+
if (!matchedInclude) {
|
|
23
|
+
return { status: 'not_included', reason: `not in include: ${target.include.join(', ')}` };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (target.exclude && target.exclude.length > 0) {
|
|
27
|
+
const matchedExclude = matchesAnyPattern(key, target.exclude);
|
|
28
|
+
if (matchedExclude) {
|
|
29
|
+
return { status: 'excluded', reason: `matches exclude: ${matchedExclude}` };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { status: 'included' };
|
|
33
|
+
}
|
|
34
|
+
export async function traceCommand(options) {
|
|
35
|
+
const { root, env, key } = options;
|
|
36
|
+
const config = loadConfig(root);
|
|
37
|
+
console.log(pc.bold(`\nTracing variable: ${pc.cyan(key)}\n`));
|
|
38
|
+
console.log(pc.blue('Source Status:'));
|
|
39
|
+
const sources = [
|
|
40
|
+
{ name: config.sources.shared, path: join(root, config.sources.shared + '.encrypted'), found: false },
|
|
41
|
+
{ name: config.sources.development, path: join(root, config.sources.development + '.encrypted'), found: false },
|
|
42
|
+
{ name: config.sources.production, path: join(root, config.sources.production + '.encrypted'), found: false },
|
|
43
|
+
{ name: config.sources.local, path: join(root, config.sources.local + '.encrypted'), found: false },
|
|
44
|
+
];
|
|
45
|
+
const maxSourceLen = Math.max(...sources.map(s => s.name.length));
|
|
46
|
+
for (const source of sources) {
|
|
47
|
+
if (!existsSync(source.path)) {
|
|
48
|
+
console.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.dim('(file not found)')}`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const content = sopsDecrypt(source.path);
|
|
53
|
+
const vars = parseEnvContent(content);
|
|
54
|
+
const found = vars.some(v => v.key === key);
|
|
55
|
+
source.found = found;
|
|
56
|
+
if (found) {
|
|
57
|
+
console.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.green('✅ Present')}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.dim('❌ Not found')}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.red('⚠️ Decrypt failed')}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const foundInAnySource = sources.some(s => s.found);
|
|
68
|
+
console.log(pc.blue(`\nTarget Disposition (Environment: ${env}):`));
|
|
69
|
+
const maxTargetLen = Math.max(...config.targets.map(t => t.name.length));
|
|
70
|
+
for (const target of config.targets) {
|
|
71
|
+
const disposition = getTargetDisposition(key, target);
|
|
72
|
+
const targetLabel = `[${target.name}]`.padEnd(maxTargetLen + 2);
|
|
73
|
+
if (!foundInAnySource) {
|
|
74
|
+
console.log(` ${targetLabel} : ${pc.yellow('⚠️ Variable not in any source')}`);
|
|
75
|
+
}
|
|
76
|
+
else if (disposition.status === 'included') {
|
|
77
|
+
console.log(` ${targetLabel} : ${pc.green('✅ Included')}`);
|
|
78
|
+
}
|
|
79
|
+
else if (disposition.status === 'excluded') {
|
|
80
|
+
console.log(` ${targetLabel} : ${pc.red(`🚫 Excluded`)} ${pc.dim(`(${disposition.reason})`)}`);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(` ${targetLabel} : ${pc.red(`🚫 Not included`)} ${pc.dim(`(${disposition.reason})`)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log('');
|
|
87
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export interface RunOptions {
|
|
|
50
50
|
export interface PushOptions {
|
|
51
51
|
root: string;
|
|
52
52
|
dryRun: boolean;
|
|
53
|
+
verbose: boolean;
|
|
53
54
|
}
|
|
54
55
|
export interface StatusOptions {
|
|
55
56
|
root: string;
|
|
@@ -94,6 +95,16 @@ export interface SkillOptions {
|
|
|
94
95
|
global?: boolean;
|
|
95
96
|
local?: boolean;
|
|
96
97
|
}
|
|
98
|
+
export interface ResolveOptions {
|
|
99
|
+
root: string;
|
|
100
|
+
env: Environment;
|
|
101
|
+
target: string;
|
|
102
|
+
}
|
|
103
|
+
export interface TraceOptions {
|
|
104
|
+
root: string;
|
|
105
|
+
env: Environment;
|
|
106
|
+
key: string;
|
|
107
|
+
}
|
|
97
108
|
export declare const DEFAULT_SOURCES: SourceFiles;
|
|
98
109
|
export declare const FORMAT_OUTPUT_FILES: Record<OutputFormat, Record<Environment, string>>;
|
|
99
110
|
//# sourceMappingURL=types.d.ts.map
|
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;AAEvD,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,WAAW,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,YAAY,GAAG,OAAO,CAAC;CAC1D;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,YAAY,GAAG,OAAO,CAAC;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|