@chriscode/hush 5.0.0 → 5.0.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.
Files changed (77) hide show
  1. package/dist/cli.js +39 -26
  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 +27 -31
  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 +92 -100
  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 +2 -1
  33. package/dist/commands/migrate.d.ts.map +1 -1
  34. package/dist/commands/migrate.js +38 -37
  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 +149 -459
  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 +48 -52
  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 +60 -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 +92 -0
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/utils/version-check.js +5 -5
  77. 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';
@@ -35,7 +36,7 @@ ${pc.bold('Commands:')}
35
36
  init Initialize hush.yaml config
36
37
  encrypt Encrypt source .hush files
37
38
  run -- <cmd> Run command with secrets in memory (AI-safe)
38
- set <KEY> Set a single secret interactively (AI-safe)
39
+ set [VALUE] <KEY> Set a single secret (AI-safe, prompts if no value)
39
40
  edit [file] Edit all secrets in $EDITOR
40
41
  list List all variables (shows values)
41
42
  inspect List all variables (masked values, AI-safe)
@@ -99,6 +100,8 @@ ${pc.bold('File Naming (v5+):')}
99
100
  .hush.development Development secrets (source file)
100
101
  .hush.encrypted Encrypted shared secrets (committed)
101
102
  .hush.development.encrypted Encrypted dev secrets (committed)
103
+
104
+ Subdirectories support templates (e.g. apps/web/.hush.development)
102
105
 
103
106
  The .env files are reserved for other tools (Wrangler, Metro, etc.).
104
107
 
@@ -110,8 +113,9 @@ ${pc.bold('Examples:')}
110
113
  hush run -e prod -- npm build Run with production secrets
111
114
  hush run -t api -- wrangler dev Run filtered for 'api' target (root secrets only)
112
115
  cd apps/mobile && hush run -- expo start Run from subdirectory (applies template + target filters)
113
- hush set DATABASE_URL Set a secret interactively (AI-safe)
114
- 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)
115
119
  hush set API_KEY -e prod Set a production secret
116
120
  hush keys setup Pull key from 1Password or verify local
117
121
  hush keys generate Generate new key + backup to 1Password
@@ -165,6 +169,7 @@ function parseArgs(args) {
165
169
  let vault;
166
170
  let file;
167
171
  let key;
172
+ let value;
168
173
  let target;
169
174
  let cmdArgs = [];
170
175
  for (let i = 0; i < args.length; i++) {
@@ -271,8 +276,16 @@ function parseArgs(args) {
271
276
  }
272
277
  continue;
273
278
  }
274
- if (command === 'set' && !arg.startsWith('-') && !key) {
275
- 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
+ }
276
289
  continue;
277
290
  }
278
291
  if (command === 'has' && !arg.startsWith('-') && !key) {
@@ -292,7 +305,7 @@ function parseArgs(args) {
292
305
  continue;
293
306
  }
294
307
  }
295
- 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 };
296
309
  }
297
310
  function checkMigrationNeeded(root, command) {
298
311
  const skipCommands = ['', 'help', 'version', 'init', 'skill', 'migrate'];
@@ -328,7 +341,7 @@ async function main() {
328
341
  printHelp();
329
342
  process.exit(0);
330
343
  }
331
- 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);
332
345
  if (command !== 'run' && !json && !quiet) {
333
346
  checkForUpdate(VERSION);
334
347
  }
@@ -336,16 +349,16 @@ async function main() {
336
349
  try {
337
350
  switch (command) {
338
351
  case 'init':
339
- await initCommand({ root });
352
+ await initCommand(defaultContext, { root });
340
353
  break;
341
354
  case 'encrypt':
342
- await encryptCommand({ root });
355
+ await encryptCommand(defaultContext, { root });
343
356
  break;
344
357
  case 'decrypt':
345
- await decryptCommand({ root, env, force });
358
+ await decryptCommand(defaultContext, { root, env, force });
346
359
  break;
347
360
  case 'run':
348
- await runCommand({ root, env, target, command: cmdArgs });
361
+ await runCommand(defaultContext, { root, env, target, command: cmdArgs });
349
362
  break;
350
363
  case 'set': {
351
364
  let setFile = 'shared';
@@ -355,36 +368,36 @@ async function main() {
355
368
  else if (envExplicit) {
356
369
  setFile = env;
357
370
  }
358
- await setCommand({ root, file: setFile, key, gui });
371
+ await setCommand(defaultContext, { root, file: setFile, key, value, gui });
359
372
  break;
360
373
  }
361
374
  case 'edit':
362
- await editCommand({ root, file });
375
+ await editCommand(defaultContext, { root, file });
363
376
  break;
364
377
  case 'list':
365
- await listCommand({ root, env });
378
+ await listCommand(defaultContext, { root, env });
366
379
  break;
367
380
  case 'inspect':
368
- await inspectCommand({ root, env });
381
+ await inspectCommand(defaultContext, { root, env });
369
382
  break;
370
383
  case 'has':
371
384
  if (!key) {
372
385
  console.error(pc.red('Usage: hush has <KEY>'));
373
386
  process.exit(1);
374
387
  }
375
- await hasCommand({ root, env, key, quiet });
388
+ await hasCommand(defaultContext, { root, env, key, quiet });
376
389
  break;
377
390
  case 'check':
378
- await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
391
+ await checkCommand(defaultContext, { root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
379
392
  break;
380
393
  case 'push':
381
- await pushCommand({ root, dryRun, verbose, target });
394
+ await pushCommand(defaultContext, { root, dryRun, verbose, target });
382
395
  break;
383
396
  case 'status':
384
- await statusCommand({ root });
397
+ await statusCommand(defaultContext, { root });
385
398
  break;
386
399
  case 'skill':
387
- await skillCommand({ root, global, local });
400
+ await skillCommand(defaultContext, { root, global, local });
388
401
  break;
389
402
  case 'keys':
390
403
  if (!subcommand) {
@@ -392,7 +405,7 @@ async function main() {
392
405
  console.error(pc.dim('Commands: setup, generate, pull, push, list'));
393
406
  process.exit(1);
394
407
  }
395
- await keysCommand({ root, subcommand, vault, force });
408
+ await keysCommand(defaultContext, { root, subcommand, vault, force });
396
409
  break;
397
410
  case 'resolve':
398
411
  if (!target) {
@@ -400,7 +413,7 @@ async function main() {
400
413
  console.error(pc.dim('Example: hush resolve api-workers'));
401
414
  process.exit(1);
402
415
  }
403
- await resolveCommand({ root, env, target });
416
+ await resolveCommand(defaultContext, { root, env, target });
404
417
  break;
405
418
  case 'trace':
406
419
  if (!key) {
@@ -408,16 +421,16 @@ async function main() {
408
421
  console.error(pc.dim('Example: hush trace DATABASE_URL'));
409
422
  process.exit(1);
410
423
  }
411
- await traceCommand({ root, env, key });
424
+ await traceCommand(defaultContext, { root, env, key });
412
425
  break;
413
426
  case 'template':
414
- await templateCommand({ root, env });
427
+ await templateCommand(defaultContext, { root, env });
415
428
  break;
416
429
  case 'expansions':
417
- await expansionsCommand({ root, env });
430
+ await expansionsCommand(defaultContext, { root, env });
418
431
  break;
419
432
  case 'migrate':
420
- await migrateCommand({ root, dryRun });
433
+ await migrateCommand(defaultContext, { root, dryRun });
421
434
  break;
422
435
  default:
423
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;AAmF/G,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAuMD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
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,7 +38,7 @@ 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 = [];
47
43
  // Only warn about .env files (legacy/output files), NOT .hush files (Hush's source files)
48
44
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
@@ -50,7 +46,7 @@ function findPlaintextEnvFiles(root) {
50
46
  function scanDir(dir, relativePath = '') {
51
47
  let entries;
52
48
  try {
53
- entries = readdirSync(dir);
49
+ entries = ctx.fs.readdirSync(dir);
54
50
  }
55
51
  catch {
56
52
  return;
@@ -61,7 +57,7 @@ function findPlaintextEnvFiles(root) {
61
57
  const fullPath = join(dir, entry);
62
58
  const relPath = relativePath ? `${relativePath}/${entry}` : entry;
63
59
  try {
64
- if (statSync(fullPath).isDirectory()) {
60
+ if (ctx.fs.statSync(fullPath).isDirectory()) {
65
61
  scanDir(fullPath, relPath);
66
62
  }
67
63
  else if (plaintextPatterns.includes(entry)) {
@@ -76,9 +72,9 @@ function findPlaintextEnvFiles(root) {
76
72
  scanDir(root);
77
73
  return results;
78
74
  }
79
- export async function check(options) {
75
+ export async function check(ctx, options) {
80
76
  const { root, requireSource, onlyChanged, allowPlaintext } = options;
81
- if (!isSopsInstalled()) {
77
+ if (!ctx.sops.isSopsInstalled()) {
82
78
  return {
83
79
  status: 'error',
84
80
  files: [{
@@ -92,11 +88,11 @@ export async function check(options) {
92
88
  }],
93
89
  };
94
90
  }
95
- const config = loadConfig(root);
91
+ const config = ctx.config.loadConfig(root);
96
92
  const pairs = getSourceEncryptedPairs(config);
97
- const result = checkPairs(root, pairs, requireSource, onlyChanged);
93
+ const result = checkPairs(ctx, root, pairs, requireSource, onlyChanged);
98
94
  if (!allowPlaintext) {
99
- const plaintextFiles = findPlaintextEnvFiles(root);
95
+ const plaintextFiles = findPlaintextEnvFiles(ctx, root);
100
96
  if (plaintextFiles.length > 0) {
101
97
  result.plaintextFiles = plaintextFiles;
102
98
  result.status = 'plaintext';
@@ -104,8 +100,8 @@ export async function check(options) {
104
100
  }
105
101
  return result;
106
102
  }
107
- function checkPairs(root, pairs, requireSource, onlyChanged) {
108
- const changedFiles = onlyChanged ? getGitChangedFiles(root) : null;
103
+ function checkPairs(ctx, root, pairs, requireSource, onlyChanged) {
104
+ const changedFiles = onlyChanged ? getGitChangedFiles(ctx, root) : null;
109
105
  const results = [];
110
106
  for (const { source, encrypted } of pairs) {
111
107
  const sourcePath = join(root, source);
@@ -117,7 +113,7 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
117
113
  continue;
118
114
  }
119
115
  }
120
- if (!existsSync(sourcePath)) {
116
+ if (!ctx.fs.existsSync(sourcePath)) {
121
117
  if (requireSource) {
122
118
  results.push({
123
119
  source,
@@ -131,8 +127,8 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
131
127
  }
132
128
  continue;
133
129
  }
134
- if (!existsSync(encryptedPath)) {
135
- const sourceContent = readFileSync(sourcePath, 'utf-8');
130
+ if (!ctx.fs.existsSync(encryptedPath)) {
131
+ const sourceContent = ctx.fs.readFileSync(sourcePath, 'utf-8');
136
132
  const sourceVars = parseEnvContent(sourceContent);
137
133
  const allKeys = sourceVars.map(v => v.key);
138
134
  results.push({
@@ -147,8 +143,8 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
147
143
  continue;
148
144
  }
149
145
  try {
150
- const decryptedContent = sopsDecrypt(encryptedPath);
151
- const sourceContent = readFileSync(sourcePath, 'utf-8');
146
+ const decryptedContent = ctx.sops.decrypt(encryptedPath);
147
+ const sourceContent = ctx.fs.readFileSync(sourcePath, 'utf-8');
152
148
  const sourceVars = parseEnvContent(sourceContent);
153
149
  const encryptedVars = parseEnvContent(decryptedContent);
154
150
  const diff = computeDiff(sourceVars, encryptedVars);
@@ -277,31 +273,31 @@ function formatTextOutput(result) {
277
273
  function formatJsonOutput(result) {
278
274
  return JSON.stringify(result, null, 2);
279
275
  }
280
- export async function checkCommand(options) {
281
- const result = await check(options);
276
+ export async function checkCommand(ctx, options) {
277
+ const result = await check(ctx, options);
282
278
  if (!options.quiet) {
283
279
  if (options.json) {
284
- console.log(formatJsonOutput(result));
280
+ ctx.logger.log(formatJsonOutput(result));
285
281
  }
286
282
  else {
287
- console.log(formatTextOutput(result));
283
+ ctx.logger.log(formatTextOutput(result));
288
284
  }
289
285
  }
290
286
  if (result.status === 'plaintext' && !options.warn) {
291
- process.exit(4);
287
+ ctx.process.exit(4);
292
288
  }
293
289
  if (result.status === 'error') {
294
290
  const hasSopsError = result.files.some(f => f.error === 'SOPS_NOT_INSTALLED');
295
291
  const hasDecryptError = result.files.some(f => f.error === 'DECRYPT_FAILED');
296
292
  if (hasSopsError || hasDecryptError) {
297
- process.exit(3);
293
+ ctx.process.exit(3);
298
294
  }
299
295
  if (result.files.some(f => f.error === 'SOURCE_MISSING')) {
300
- process.exit(2);
296
+ ctx.process.exit(2);
301
297
  }
302
298
  }
303
299
  if (result.status === 'drift' && !options.warn) {
304
- process.exit(1);
300
+ ctx.process.exit(1);
305
301
  }
306
- process.exit(0);
302
+ ctx.process.exit(0);
307
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
  }
@@ -1,3 +1,3 @@
1
- import type { EncryptOptions } from '../types.js';
2
- export declare function encryptCommand(options: EncryptOptions): Promise<void>;
1
+ import type { EncryptOptions, HushContext } from '../types.js';
2
+ export declare function encryptCommand(ctx: HushContext, options: EncryptOptions): Promise<void>;
3
3
  //# sourceMappingURL=encrypt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../src/commands/encrypt.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AASlD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAsF3E"}
1
+ {"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../src/commands/encrypt.ts"],"names":[],"mappings":"AAIC,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAShE,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAsF7F"}