@chriscode/hush 4.2.0 → 5.0.1

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.
Files changed (78) hide show
  1. package/dist/cli.js +58 -29
  2. package/dist/commands/check.d.ts +3 -3
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +30 -33
  5. package/dist/commands/decrypt.d.ts +2 -2
  6. package/dist/commands/decrypt.d.ts.map +1 -1
  7. package/dist/commands/decrypt.js +52 -55
  8. package/dist/commands/edit.d.ts +2 -2
  9. package/dist/commands/edit.d.ts.map +1 -1
  10. package/dist/commands/edit.js +10 -12
  11. package/dist/commands/encrypt.d.ts +2 -2
  12. package/dist/commands/encrypt.d.ts.map +1 -1
  13. package/dist/commands/encrypt.js +27 -29
  14. package/dist/commands/expansions.d.ts +2 -2
  15. package/dist/commands/expansions.d.ts.map +1 -1
  16. package/dist/commands/expansions.js +46 -44
  17. package/dist/commands/has.d.ts +2 -2
  18. package/dist/commands/has.d.ts.map +1 -1
  19. package/dist/commands/has.js +12 -15
  20. package/dist/commands/init.d.ts +2 -2
  21. package/dist/commands/init.d.ts.map +1 -1
  22. package/dist/commands/init.js +107 -87
  23. package/dist/commands/inspect.d.ts +2 -2
  24. package/dist/commands/inspect.d.ts.map +1 -1
  25. package/dist/commands/inspect.js +14 -16
  26. package/dist/commands/keys.d.ts +2 -1
  27. package/dist/commands/keys.d.ts.map +1 -1
  28. package/dist/commands/keys.js +47 -49
  29. package/dist/commands/list.d.ts +2 -2
  30. package/dist/commands/list.d.ts.map +1 -1
  31. package/dist/commands/list.js +11 -14
  32. package/dist/commands/migrate.d.ts +7 -0
  33. package/dist/commands/migrate.d.ts.map +1 -0
  34. package/dist/commands/migrate.js +117 -0
  35. package/dist/commands/push.d.ts +2 -2
  36. package/dist/commands/push.d.ts.map +1 -1
  37. package/dist/commands/push.js +41 -45
  38. package/dist/commands/resolve.d.ts +2 -2
  39. package/dist/commands/resolve.d.ts.map +1 -1
  40. package/dist/commands/resolve.js +25 -28
  41. package/dist/commands/run.d.ts +2 -2
  42. package/dist/commands/run.d.ts.map +1 -1
  43. package/dist/commands/run.js +35 -39
  44. package/dist/commands/set.d.ts +2 -2
  45. package/dist/commands/set.d.ts.map +1 -1
  46. package/dist/commands/set.js +61 -70
  47. package/dist/commands/skill.d.ts +2 -2
  48. package/dist/commands/skill.d.ts.map +1 -1
  49. package/dist/commands/skill.js +186 -487
  50. package/dist/commands/status.d.ts +2 -2
  51. package/dist/commands/status.d.ts.map +1 -1
  52. package/dist/commands/status.js +52 -55
  53. package/dist/commands/template.d.ts +2 -2
  54. package/dist/commands/template.d.ts.map +1 -1
  55. package/dist/commands/template.js +36 -39
  56. package/dist/commands/trace.d.ts +2 -2
  57. package/dist/commands/trace.d.ts.map +1 -1
  58. package/dist/commands/trace.js +16 -19
  59. package/dist/config/loader.js +3 -3
  60. package/dist/context.d.ts +3 -0
  61. package/dist/context.d.ts.map +1 -0
  62. package/dist/context.js +59 -0
  63. package/dist/core/parse.js +3 -3
  64. package/dist/core/sops.js +9 -9
  65. package/dist/core/template.d.ts +2 -2
  66. package/dist/core/template.d.ts.map +1 -1
  67. package/dist/core/template.js +11 -12
  68. package/dist/lib/age.js +9 -9
  69. package/dist/lib/fs.d.ts +25 -0
  70. package/dist/lib/fs.d.ts.map +1 -0
  71. package/dist/lib/fs.js +36 -0
  72. package/dist/lib/onepassword.d.ts.map +1 -1
  73. package/dist/lib/onepassword.js +41 -4
  74. package/dist/types.d.ts +91 -0
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +4 -4
  77. package/dist/utils/version-check.js +5 -5
  78. package/package.json +3 -2
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import pc from 'picocolors';
4
+ import { defaultContext } from './context.js';
4
5
  import { encryptCommand } from './commands/encrypt.js';
5
6
  import { decryptCommand } from './commands/decrypt.js';
6
7
  import { editCommand } from './commands/edit.js';
@@ -19,6 +20,7 @@ import { resolveCommand } from './commands/resolve.js';
19
20
  import { traceCommand } from './commands/trace.js';
20
21
  import { templateCommand } from './commands/template.js';
21
22
  import { expansionsCommand } from './commands/expansions.js';
23
+ import { migrateCommand } from './commands/migrate.js';
22
24
  import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
23
25
  import { checkForUpdate } from './utils/version-check.js';
24
26
  const require = createRequire(import.meta.url);
@@ -32,9 +34,9 @@ ${pc.bold('Usage:')}
32
34
 
33
35
  ${pc.bold('Commands:')}
34
36
  init Initialize hush.yaml config
35
- encrypt Encrypt source .env files
37
+ encrypt Encrypt source .hush files
36
38
  run -- <cmd> Run command with secrets in memory (AI-safe)
37
- set <KEY> Set a single secret interactively (AI-safe)
39
+ set [VALUE] <KEY> Set a single secret (AI-safe, prompts if no value)
38
40
  edit [file] Edit all secrets in $EDITOR
39
41
  list List all variables (shows values)
40
42
  inspect List all variables (masked values, AI-safe)
@@ -44,6 +46,7 @@ ${pc.bold('Commands:')}
44
46
  status Show configuration and status
45
47
  skill Install Claude Code / OpenCode skill
46
48
  keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
49
+ migrate Migrate from v4 (.env.encrypted) to v5 (.hush.encrypted)
47
50
 
48
51
  ${pc.bold('Debugging Commands:')}
49
52
  resolve <target> Show what variables a target receives (AI-safe)
@@ -72,7 +75,7 @@ ${pc.bold('Options:')}
72
75
  -h, --help Show this help message
73
76
  -v, --version Show version number
74
77
 
75
- ${pc.bold('Variable Expansion (v4+):')}
78
+ ${pc.bold('Variable Expansion (v5+):')}
76
79
  Subdirectory .env files can reference root secrets:
77
80
 
78
81
  \${VAR} Pull VAR from root secrets
@@ -90,15 +93,29 @@ ${pc.bold('Variable Expansion (v4+):')}
90
93
 
91
94
  Subdirectory templates are safe to commit - they contain no secrets.
92
95
 
96
+ ${pc.bold('File Naming (v5+):')}
97
+ Hush uses .hush files instead of .env to avoid conflicts with other tools:
98
+
99
+ .hush Shared secrets (source file)
100
+ .hush.development Development secrets (source file)
101
+ .hush.encrypted Encrypted shared secrets (committed)
102
+ .hush.development.encrypted Encrypted dev secrets (committed)
103
+
104
+ Subdirectories support templates (e.g. apps/web/.hush.development)
105
+
106
+ The .env files are reserved for other tools (Wrangler, Metro, etc.).
107
+
93
108
  ${pc.bold('Examples:')}
94
109
  hush init Initialize config + generate keys
95
- hush encrypt Encrypt .env files
110
+ hush migrate Migrate v4 .env.encrypted to v5 .hush.encrypted
111
+ hush encrypt Encrypt .hush files
96
112
  hush run -- npm start Run with secrets in memory (AI-safe!)
97
113
  hush run -e prod -- npm build Run with production secrets
98
114
  hush run -t api -- wrangler dev Run filtered for 'api' target (root secrets only)
99
115
  cd apps/mobile && hush run -- expo start Run from subdirectory (applies template + target filters)
100
- hush set DATABASE_URL Set a secret interactively (AI-safe)
101
- hush set API_KEY --gui Set secret via macOS dialog (for AI agents)
116
+ hush set DATABASE_URL Set a secret interactively (prompts for value)
117
+ hush set "myvalue" API_KEY Set a secret inline (no prompt)
118
+ hush set API_KEY --gui Set secret via GUI dialog (for AI agents)
102
119
  hush set API_KEY -e prod Set a production secret
103
120
  hush keys setup Pull key from 1Password or verify local
104
121
  hush keys generate Generate new key + backup to 1Password
@@ -152,6 +169,7 @@ function parseArgs(args) {
152
169
  let vault;
153
170
  let file;
154
171
  let key;
172
+ let value;
155
173
  let target;
156
174
  let cmdArgs = [];
157
175
  for (let i = 0; i < args.length; i++) {
@@ -258,8 +276,16 @@ function parseArgs(args) {
258
276
  }
259
277
  continue;
260
278
  }
261
- if (command === 'set' && !arg.startsWith('-') && !key) {
262
- key = arg;
279
+ if (command === 'set' && !arg.startsWith('-')) {
280
+ if (!key) {
281
+ key = arg;
282
+ }
283
+ else if (!value) {
284
+ // Second positional arg: shift key to value, this arg is the key
285
+ // Syntax: hush set <VALUE> <KEY>
286
+ value = key;
287
+ key = arg;
288
+ }
263
289
  continue;
264
290
  }
265
291
  if (command === 'has' && !arg.startsWith('-') && !key) {
@@ -279,10 +305,10 @@ function parseArgs(args) {
279
305
  continue;
280
306
  }
281
307
  }
282
- return { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs };
308
+ return { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, value, target, cmdArgs };
283
309
  }
284
310
  function checkMigrationNeeded(root, command) {
285
- const skipCommands = ['', 'help', 'version', 'init', 'skill'];
311
+ const skipCommands = ['', 'help', 'version', 'init', 'skill', 'migrate'];
286
312
  if (skipCommands.includes(command))
287
313
  return;
288
314
  const configPath = findConfigPath(root);
@@ -315,7 +341,7 @@ async function main() {
315
341
  printHelp();
316
342
  process.exit(0);
317
343
  }
318
- 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);
344
+ const { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, value, target, cmdArgs } = parseArgs(args);
319
345
  if (command !== 'run' && !json && !quiet) {
320
346
  checkForUpdate(VERSION);
321
347
  }
@@ -323,16 +349,16 @@ async function main() {
323
349
  try {
324
350
  switch (command) {
325
351
  case 'init':
326
- await initCommand({ root });
352
+ await initCommand(defaultContext, { root });
327
353
  break;
328
354
  case 'encrypt':
329
- await encryptCommand({ root });
355
+ await encryptCommand(defaultContext, { root });
330
356
  break;
331
357
  case 'decrypt':
332
- await decryptCommand({ root, env, force });
358
+ await decryptCommand(defaultContext, { root, env, force });
333
359
  break;
334
360
  case 'run':
335
- await runCommand({ root, env, target, command: cmdArgs });
361
+ await runCommand(defaultContext, { root, env, target, command: cmdArgs });
336
362
  break;
337
363
  case 'set': {
338
364
  let setFile = 'shared';
@@ -342,36 +368,36 @@ async function main() {
342
368
  else if (envExplicit) {
343
369
  setFile = env;
344
370
  }
345
- await setCommand({ root, file: setFile, key, gui });
371
+ await setCommand(defaultContext, { root, file: setFile, key, value, gui });
346
372
  break;
347
373
  }
348
374
  case 'edit':
349
- await editCommand({ root, file });
375
+ await editCommand(defaultContext, { root, file });
350
376
  break;
351
377
  case 'list':
352
- await listCommand({ root, env });
378
+ await listCommand(defaultContext, { root, env });
353
379
  break;
354
380
  case 'inspect':
355
- await inspectCommand({ root, env });
381
+ await inspectCommand(defaultContext, { root, env });
356
382
  break;
357
383
  case 'has':
358
384
  if (!key) {
359
385
  console.error(pc.red('Usage: hush has <KEY>'));
360
386
  process.exit(1);
361
387
  }
362
- await hasCommand({ root, env, key, quiet });
388
+ await hasCommand(defaultContext, { root, env, key, quiet });
363
389
  break;
364
390
  case 'check':
365
- await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
391
+ await checkCommand(defaultContext, { root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
366
392
  break;
367
393
  case 'push':
368
- await pushCommand({ root, dryRun, verbose, target });
394
+ await pushCommand(defaultContext, { root, dryRun, verbose, target });
369
395
  break;
370
396
  case 'status':
371
- await statusCommand({ root });
397
+ await statusCommand(defaultContext, { root });
372
398
  break;
373
399
  case 'skill':
374
- await skillCommand({ root, global, local });
400
+ await skillCommand(defaultContext, { root, global, local });
375
401
  break;
376
402
  case 'keys':
377
403
  if (!subcommand) {
@@ -379,7 +405,7 @@ async function main() {
379
405
  console.error(pc.dim('Commands: setup, generate, pull, push, list'));
380
406
  process.exit(1);
381
407
  }
382
- await keysCommand({ root, subcommand, vault, force });
408
+ await keysCommand(defaultContext, { root, subcommand, vault, force });
383
409
  break;
384
410
  case 'resolve':
385
411
  if (!target) {
@@ -387,7 +413,7 @@ async function main() {
387
413
  console.error(pc.dim('Example: hush resolve api-workers'));
388
414
  process.exit(1);
389
415
  }
390
- await resolveCommand({ root, env, target });
416
+ await resolveCommand(defaultContext, { root, env, target });
391
417
  break;
392
418
  case 'trace':
393
419
  if (!key) {
@@ -395,13 +421,16 @@ async function main() {
395
421
  console.error(pc.dim('Example: hush trace DATABASE_URL'));
396
422
  process.exit(1);
397
423
  }
398
- await traceCommand({ root, env, key });
424
+ await traceCommand(defaultContext, { root, env, key });
399
425
  break;
400
426
  case 'template':
401
- await templateCommand({ root, env });
427
+ await templateCommand(defaultContext, { root, env });
402
428
  break;
403
429
  case 'expansions':
404
- await expansionsCommand({ root, env });
430
+ await expansionsCommand(defaultContext, { root, env });
431
+ break;
432
+ case 'migrate':
433
+ await migrateCommand(defaultContext, { root, dryRun });
405
434
  break;
406
435
  default:
407
436
  if (command) {
@@ -1,4 +1,4 @@
1
- import type { CheckOptions, CheckResult } from '../types.js';
2
- export declare function check(options: CheckOptions): Promise<CheckResult>;
3
- export declare function checkCommand(options: CheckOptions): Promise<void>;
1
+ import type { CheckOptions, CheckResult, HushContext } from '../types.js';
2
+ export declare function check(ctx: HushContext, options: CheckOptions): Promise<CheckResult>;
3
+ export declare function checkCommand(ctx: HushContext, options: CheckOptions): Promise<void>;
4
4
  //# sourceMappingURL=check.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,MAAM,aAAa,CAAC;AAkF/G,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAuMD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,WAAW,EAAE,MAAM,aAAa,CAAC;AAmF5H,wBAAsB,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BzF;AAwMD,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCzF"}
@@ -1,10 +1,6 @@
1
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
1
  import { join } from 'node:path';
3
- import { execSync } from 'node:child_process';
4
2
  import pc from 'picocolors';
5
- import { loadConfig } from '../config/loader.js';
6
3
  import { parseEnvContent } from '../core/parse.js';
7
- import { decrypt as sopsDecrypt, isSopsInstalled } from '../core/sops.js';
8
4
  import { computeDiff, isInSync } from '../lib/diff.js';
9
5
  function getSourceEncryptedPairs(config) {
10
6
  const pairs = [];
@@ -31,10 +27,10 @@ function getSourceEncryptedPairs(config) {
31
27
  }
32
28
  return pairs;
33
29
  }
34
- function getGitChangedFiles(root) {
30
+ function getGitChangedFiles(ctx, root) {
35
31
  try {
36
- const staged = execSync('git diff --cached --name-only', { cwd: root, encoding: 'utf-8' });
37
- const unstaged = execSync('git diff --name-only', { cwd: root, encoding: 'utf-8' });
32
+ const staged = ctx.exec.execSync('git diff --cached --name-only', { cwd: root, encoding: 'utf-8' });
33
+ const unstaged = ctx.exec.execSync('git diff --name-only', { cwd: root, encoding: 'utf-8' });
38
34
  const files = [...staged.split('\n'), ...unstaged.split('\n')].filter(Boolean);
39
35
  return new Set(files);
40
36
  }
@@ -42,14 +38,15 @@ function getGitChangedFiles(root) {
42
38
  return new Set();
43
39
  }
44
40
  }
45
- function findPlaintextEnvFiles(root) {
41
+ function findPlaintextEnvFiles(ctx, root) {
46
42
  const results = [];
43
+ // Only warn about .env files (legacy/output files), NOT .hush files (Hush's source files)
47
44
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
48
45
  const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
49
46
  function scanDir(dir, relativePath = '') {
50
47
  let entries;
51
48
  try {
52
- entries = readdirSync(dir);
49
+ entries = ctx.fs.readdirSync(dir);
53
50
  }
54
51
  catch {
55
52
  return;
@@ -60,7 +57,7 @@ function findPlaintextEnvFiles(root) {
60
57
  const fullPath = join(dir, entry);
61
58
  const relPath = relativePath ? `${relativePath}/${entry}` : entry;
62
59
  try {
63
- if (statSync(fullPath).isDirectory()) {
60
+ if (ctx.fs.statSync(fullPath).isDirectory()) {
64
61
  scanDir(fullPath, relPath);
65
62
  }
66
63
  else if (plaintextPatterns.includes(entry)) {
@@ -75,9 +72,9 @@ function findPlaintextEnvFiles(root) {
75
72
  scanDir(root);
76
73
  return results;
77
74
  }
78
- export async function check(options) {
75
+ export async function check(ctx, options) {
79
76
  const { root, requireSource, onlyChanged, allowPlaintext } = options;
80
- if (!isSopsInstalled()) {
77
+ if (!ctx.sops.isSopsInstalled()) {
81
78
  return {
82
79
  status: 'error',
83
80
  files: [{
@@ -91,11 +88,11 @@ export async function check(options) {
91
88
  }],
92
89
  };
93
90
  }
94
- const config = loadConfig(root);
91
+ const config = ctx.config.loadConfig(root);
95
92
  const pairs = getSourceEncryptedPairs(config);
96
- const result = checkPairs(root, pairs, requireSource, onlyChanged);
93
+ const result = checkPairs(ctx, root, pairs, requireSource, onlyChanged);
97
94
  if (!allowPlaintext) {
98
- const plaintextFiles = findPlaintextEnvFiles(root);
95
+ const plaintextFiles = findPlaintextEnvFiles(ctx, root);
99
96
  if (plaintextFiles.length > 0) {
100
97
  result.plaintextFiles = plaintextFiles;
101
98
  result.status = 'plaintext';
@@ -103,8 +100,8 @@ export async function check(options) {
103
100
  }
104
101
  return result;
105
102
  }
106
- function checkPairs(root, pairs, requireSource, onlyChanged) {
107
- const changedFiles = onlyChanged ? getGitChangedFiles(root) : null;
103
+ function checkPairs(ctx, root, pairs, requireSource, onlyChanged) {
104
+ const changedFiles = onlyChanged ? getGitChangedFiles(ctx, root) : null;
108
105
  const results = [];
109
106
  for (const { source, encrypted } of pairs) {
110
107
  const sourcePath = join(root, source);
@@ -116,7 +113,7 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
116
113
  continue;
117
114
  }
118
115
  }
119
- if (!existsSync(sourcePath)) {
116
+ if (!ctx.fs.existsSync(sourcePath)) {
120
117
  if (requireSource) {
121
118
  results.push({
122
119
  source,
@@ -130,8 +127,8 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
130
127
  }
131
128
  continue;
132
129
  }
133
- if (!existsSync(encryptedPath)) {
134
- const sourceContent = readFileSync(sourcePath, 'utf-8');
130
+ if (!ctx.fs.existsSync(encryptedPath)) {
131
+ const sourceContent = ctx.fs.readFileSync(sourcePath, 'utf-8');
135
132
  const sourceVars = parseEnvContent(sourceContent);
136
133
  const allKeys = sourceVars.map(v => v.key);
137
134
  results.push({
@@ -146,8 +143,8 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
146
143
  continue;
147
144
  }
148
145
  try {
149
- const decryptedContent = sopsDecrypt(encryptedPath);
150
- const sourceContent = readFileSync(sourcePath, 'utf-8');
146
+ const decryptedContent = ctx.sops.decrypt(encryptedPath);
147
+ const sourceContent = ctx.fs.readFileSync(sourcePath, 'utf-8');
151
148
  const sourceVars = parseEnvContent(sourceContent);
152
149
  const encryptedVars = parseEnvContent(decryptedContent);
153
150
  const diff = computeDiff(sourceVars, encryptedVars);
@@ -206,8 +203,8 @@ function formatTextOutput(result) {
206
203
  lines.push(pc.yellow('These files contain plaintext secrets that could be exposed to AI assistants.'));
207
204
  lines.push('');
208
205
  lines.push(pc.bold('To fix:'));
209
- lines.push(pc.dim(' 1. Run: hush encrypt'));
210
- lines.push(pc.dim(' 2. Delete the plaintext files (the .encrypted versions are your source of truth)'));
206
+ lines.push(pc.dim(' 1. Run: hush migrate (if upgrading from v4)'));
207
+ lines.push(pc.dim(' 2. Delete or gitignore these .env files'));
211
208
  lines.push(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars'));
212
209
  lines.push('');
213
210
  lines.push(pc.dim('To allow plaintext files (not recommended): --allow-plaintext'));
@@ -276,31 +273,31 @@ function formatTextOutput(result) {
276
273
  function formatJsonOutput(result) {
277
274
  return JSON.stringify(result, null, 2);
278
275
  }
279
- export async function checkCommand(options) {
280
- const result = await check(options);
276
+ export async function checkCommand(ctx, options) {
277
+ const result = await check(ctx, options);
281
278
  if (!options.quiet) {
282
279
  if (options.json) {
283
- console.log(formatJsonOutput(result));
280
+ ctx.logger.log(formatJsonOutput(result));
284
281
  }
285
282
  else {
286
- console.log(formatTextOutput(result));
283
+ ctx.logger.log(formatTextOutput(result));
287
284
  }
288
285
  }
289
286
  if (result.status === 'plaintext' && !options.warn) {
290
- process.exit(4);
287
+ ctx.process.exit(4);
291
288
  }
292
289
  if (result.status === 'error') {
293
290
  const hasSopsError = result.files.some(f => f.error === 'SOPS_NOT_INSTALLED');
294
291
  const hasDecryptError = result.files.some(f => f.error === 'DECRYPT_FAILED');
295
292
  if (hasSopsError || hasDecryptError) {
296
- process.exit(3);
293
+ ctx.process.exit(3);
297
294
  }
298
295
  if (result.files.some(f => f.error === 'SOURCE_MISSING')) {
299
- process.exit(2);
296
+ ctx.process.exit(2);
300
297
  }
301
298
  }
302
299
  if (result.status === 'drift' && !options.warn) {
303
- process.exit(1);
300
+ ctx.process.exit(1);
304
301
  }
305
- process.exit(0);
302
+ ctx.process.exit(0);
306
303
  }
@@ -1,3 +1,3 @@
1
- import type { DecryptOptions } from '../types.js';
2
- export declare function decryptCommand(options: DecryptOptions): Promise<void>;
1
+ import type { DecryptOptions, HushContext } from '../types.js';
2
+ export declare function decryptCommand(ctx: HushContext, options: DecryptOptions): Promise<void>;
3
3
  //# sourceMappingURL=decrypt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../src/commands/decrypt.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAU,MAAM,aAAa,CAAC;AAmD1D,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA8F3E"}
1
+ {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../src/commands/decrypt.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;AAmDvE,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA8F7F"}
@@ -1,60 +1,57 @@
1
1
  import { createInterface } from 'node:readline';
2
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
2
  import { join } from 'node:path';
4
3
  import pc from 'picocolors';
5
- import { loadConfig } from '../config/loader.js';
6
4
  import { filterVarsForTarget } from '../core/filter.js';
7
5
  import { interpolateVars, getUnresolvedVars } from '../core/interpolate.js';
8
6
  import { mergeVars } from '../core/merge.js';
9
7
  import { parseEnvContent, parseEnvFile } from '../core/parse.js';
10
- import { decrypt as sopsDecrypt } from '../core/sops.js';
11
8
  import { formatVars } from '../formats/index.js';
12
9
  import { FORMAT_OUTPUT_FILES } from '../types.js';
13
10
  function getEncryptedPath(sourcePath) {
14
11
  return sourcePath + '.encrypted';
15
12
  }
16
- async function confirmDangerousOperation() {
17
- if (!process.stdin.isTTY) {
18
- console.error(pc.red('\nError: decrypt --force requires interactive confirmation.'));
19
- console.error(pc.dim('This command cannot be run in non-interactive environments.'));
20
- console.error(pc.dim('\nUse "hush run -- <command>" instead to inject secrets into memory.'));
13
+ async function confirmDangerousOperation(ctx) {
14
+ if (!ctx.process.stdin.isTTY) {
15
+ ctx.logger.error('\nError: decrypt --force requires interactive confirmation.');
16
+ ctx.logger.error('This command cannot be run in non-interactive environments.');
17
+ ctx.logger.error('\nUse "hush run -- <command>" instead to inject secrets into memory.');
21
18
  return false;
22
19
  }
23
- console.log('');
24
- console.log(pc.red('━'.repeat(70)));
25
- console.log(pc.red(pc.bold(' ⚠️ WARNING: WRITING PLAINTEXT SECRETS TO DISK')));
26
- console.log(pc.red('━'.repeat(70)));
27
- console.log('');
28
- console.log(pc.yellow(' This will create unencrypted .env files that:'));
29
- console.log(pc.dim(' • Can be read by AI assistants, scripts, and other tools'));
30
- console.log(pc.dim(' • May accidentally be committed to git'));
31
- console.log(pc.dim(' • Defeat the "encrypted at rest" security model'));
32
- console.log('');
33
- console.log(pc.green(' Recommended alternative:'));
34
- console.log(pc.cyan(' hush run -- <your-command>'));
35
- console.log(pc.dim(' Decrypts to memory only, secrets never touch disk.'));
36
- console.log('');
37
- console.log(pc.red('━'.repeat(70)));
38
- console.log('');
20
+ ctx.logger.log('');
21
+ ctx.logger.log(pc.red('━'.repeat(70)));
22
+ ctx.logger.log(pc.red(pc.bold(' ⚠️ WARNING: WRITING PLAINTEXT SECRETS TO DISK')));
23
+ ctx.logger.log(pc.red('━'.repeat(70)));
24
+ ctx.logger.log('');
25
+ ctx.logger.log(pc.yellow(' This will create unencrypted .env files that:'));
26
+ ctx.logger.log(pc.dim(' • Can be read by AI assistants, scripts, and other tools'));
27
+ ctx.logger.log(pc.dim(' • May accidentally be committed to git'));
28
+ ctx.logger.log(pc.dim(' • Defeat the "encrypted at rest" security model'));
29
+ ctx.logger.log('');
30
+ ctx.logger.log(pc.green(' Recommended alternative:'));
31
+ ctx.logger.log(pc.cyan(' hush run -- <command>'));
32
+ ctx.logger.log(pc.dim(' Decrypts to memory only, secrets never touch disk.'));
33
+ ctx.logger.log('');
34
+ ctx.logger.log(pc.red('━'.repeat(70)));
35
+ ctx.logger.log('');
39
36
  const rl = createInterface({
40
- input: process.stdin,
41
- output: process.stdout,
37
+ input: ctx.process.stdin,
38
+ output: ctx.process.stdout,
42
39
  });
43
40
  return new Promise((resolve) => {
44
41
  rl.question(`${pc.bold('Type "yes" to proceed:')} `, (answer) => {
45
42
  rl.close();
46
43
  if (answer.toLowerCase() === 'yes') {
47
- console.log('');
44
+ ctx.logger.log('');
48
45
  resolve(true);
49
46
  }
50
47
  else {
51
- console.log(pc.dim('\nAborted. No files were written.'));
48
+ ctx.logger.log(pc.dim('\nAborted. No files were written.'));
52
49
  resolve(false);
53
50
  }
54
51
  });
55
52
  });
56
53
  }
57
- export async function decryptCommand(options) {
54
+ export async function decryptCommand(ctx, options) {
58
55
  const { root, env, force } = options;
59
56
  if (!force) {
60
57
  console.error(pc.red('Error: decrypt requires --force flag'));
@@ -66,64 +63,64 @@ export async function decryptCommand(options) {
66
63
  console.error(pc.cyan(' hush decrypt --force'));
67
64
  process.exit(1);
68
65
  }
69
- const confirmed = await confirmDangerousOperation();
66
+ const confirmed = await confirmDangerousOperation(ctx);
70
67
  if (!confirmed) {
71
- process.exit(0);
68
+ ctx.process.exit(0);
72
69
  }
73
- const config = loadConfig(root);
74
- console.log(pc.yellow(`⚠️ Writing unencrypted secrets for ${env}...`));
70
+ const config = ctx.config.loadConfig(root);
71
+ ctx.logger.log(pc.yellow(`⚠️ Writing unencrypted secrets for ${env}...`));
75
72
  const sharedEncrypted = join(root, getEncryptedPath(config.sources.shared));
76
73
  const envEncrypted = join(root, getEncryptedPath(config.sources[env]));
77
- const localPath = join(root, '.env.local');
74
+ const localPath = join(root, config.sources.local);
78
75
  const varSources = [];
79
- if (existsSync(sharedEncrypted)) {
80
- const content = sopsDecrypt(sharedEncrypted);
76
+ if (ctx.fs.existsSync(sharedEncrypted)) {
77
+ const content = ctx.sops.decrypt(sharedEncrypted);
81
78
  const vars = parseEnvContent(content);
82
79
  varSources.push(vars);
83
- console.log(pc.dim(` ${config.sources.shared}.encrypted: ${vars.length} vars`));
80
+ ctx.logger.log(pc.dim(` ${config.sources.shared}.encrypted: ${vars.length} vars`));
84
81
  }
85
- if (existsSync(envEncrypted)) {
86
- const content = sopsDecrypt(envEncrypted);
82
+ if (ctx.fs.existsSync(envEncrypted)) {
83
+ const content = ctx.sops.decrypt(envEncrypted);
87
84
  const vars = parseEnvContent(content);
88
85
  varSources.push(vars);
89
- console.log(pc.dim(` ${config.sources[env]}.encrypted: ${vars.length} vars`));
86
+ ctx.logger.log(pc.dim(` ${config.sources[env]}.encrypted: ${vars.length} vars`));
90
87
  }
91
- if (existsSync(localPath)) {
88
+ if (ctx.fs.existsSync(localPath)) {
92
89
  const vars = parseEnvFile(localPath);
93
90
  varSources.push(vars);
94
- console.log(pc.dim(` .env.local: ${vars.length} vars (overrides)`));
91
+ ctx.logger.log(pc.dim(` ${config.sources.local}: ${vars.length} vars (overrides)`));
95
92
  }
96
93
  if (varSources.length === 0) {
97
- console.error(pc.red('No encrypted files found'));
98
- console.error(pc.dim(`Expected: ${sharedEncrypted}`));
99
- process.exit(1);
94
+ ctx.logger.error(pc.red('No encrypted files found'));
95
+ ctx.logger.error(pc.dim(`Expected: ${sharedEncrypted}`));
96
+ ctx.process.exit(1);
100
97
  }
101
98
  const merged = mergeVars(...varSources);
102
99
  const interpolated = interpolateVars(merged);
103
100
  const unresolved = getUnresolvedVars(interpolated);
104
101
  if (unresolved.length > 0) {
105
- console.warn(pc.yellow(` Warning: ${unresolved.length} vars have unresolved references`));
102
+ ctx.logger.warn(pc.yellow(` Warning: ${unresolved.length} vars have unresolved references`));
106
103
  }
107
- console.log(pc.yellow(`\n⚠️ Writing to ${config.targets.length} targets:`));
104
+ ctx.logger.log(pc.yellow(`\n⚠️ Writing to ${config.targets.length} targets:`));
108
105
  for (const target of config.targets) {
109
106
  const targetDir = join(root, target.path);
110
107
  const filtered = filterVarsForTarget(interpolated, target);
111
108
  if (filtered.length === 0) {
112
- console.log(pc.dim(` ${target.path}/ - no matching vars, skipped`));
109
+ ctx.logger.log(pc.dim(` ${target.path}/ - no matching vars, skipped`));
113
110
  continue;
114
111
  }
115
112
  const outputFilename = FORMAT_OUTPUT_FILES[target.format][env];
116
113
  const outputPath = join(targetDir, outputFilename);
117
- if (!existsSync(targetDir)) {
118
- mkdirSync(targetDir, { recursive: true });
114
+ if (!ctx.fs.existsSync(targetDir)) {
115
+ ctx.fs.mkdirSync(targetDir, { recursive: true });
119
116
  }
120
117
  const content = formatVars(filtered, target.format);
121
- writeFileSync(outputPath, content, 'utf-8');
118
+ ctx.fs.writeFileSync(outputPath, content, 'utf-8');
122
119
  const relativePath = target.path === '.' ? outputFilename : `${target.path}/${outputFilename}`;
123
- console.log(pc.yellow(` ⚠️ ${relativePath}`) +
120
+ ctx.logger.log(pc.yellow(` ⚠️ ${relativePath}`) +
124
121
  pc.dim(` (${target.format}, ${filtered.length} vars)`));
125
122
  }
126
- console.log('');
127
- console.log(pc.yellow('⚠️ Decryption complete - plaintext secrets on disk'));
128
- console.log(pc.dim(' Delete these files when done, or use "hush run" next time.'));
123
+ ctx.logger.log('');
124
+ ctx.logger.log(pc.yellow('⚠️ Decryption complete - plaintext secrets on disk'));
125
+ ctx.logger.log(pc.dim(' Delete these files when done, or use "hush run" next time.'));
129
126
  }
@@ -1,3 +1,3 @@
1
- import type { EditOptions } from '../types.js';
2
- export declare function editCommand(options: EditOptions): Promise<void>;
1
+ import type { EditOptions, HushContext } from '../types.js';
2
+ export declare function editCommand(ctx: HushContext, options: EditOptions): Promise<void>;
3
3
  //# sourceMappingURL=edit.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../src/commands/edit.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBrE"}
1
+ {"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../src/commands/edit.ts"],"names":[],"mappings":"AAGC,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI7D,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBvF"}
@@ -1,22 +1,20 @@
1
- import { existsSync } from 'node:fs';
2
1
  import { join } from 'node:path';
3
2
  import pc from 'picocolors';
4
- import { loadConfig } from '../config/loader.js';
5
3
  import { edit as sopsEdit } from '../core/sops.js';
6
- export async function editCommand(options) {
4
+ export async function editCommand(ctx, options) {
7
5
  const { root, file } = options;
8
- const config = loadConfig(root);
6
+ const config = ctx.config.loadConfig(root);
9
7
  const fileKey = file ?? 'shared';
10
8
  const sourcePath = config.sources[fileKey];
11
9
  const encryptedPath = join(root, sourcePath + '.encrypted');
12
- if (!existsSync(encryptedPath)) {
13
- console.error(pc.red(`Encrypted file not found: ${sourcePath}.encrypted`));
14
- console.error(pc.dim('Run "hush encrypt" first to create encrypted files'));
15
- process.exit(1);
10
+ if (!ctx.fs.existsSync(encryptedPath)) {
11
+ ctx.logger.error(pc.red(`Encrypted file not found: ${sourcePath}.encrypted`));
12
+ ctx.logger.error(pc.dim('Run "hush encrypt" first to create encrypted files'));
13
+ ctx.process.exit(1);
16
14
  }
17
- console.log(pc.blue(`Editing ${sourcePath}.encrypted...`));
18
- console.log(pc.dim('Changes will be encrypted on save'));
15
+ ctx.logger.log(pc.blue(`Editing ${sourcePath}.encrypted...`));
16
+ ctx.logger.log(pc.dim('Changes will be encrypted on save'));
19
17
  sopsEdit(encryptedPath);
20
- console.log(pc.green('\nEdit complete'));
21
- console.log(pc.dim('Run "hush run -- <command>" to use updated secrets'));
18
+ ctx.logger.log(pc.green('\nEdit complete'));
19
+ ctx.logger.log(pc.dim('Run "hush run -- <command>" to use updated secrets'));
22
20
  }