@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
@@ -1,3 +1,3 @@
1
- import type { StatusOptions } from '../types.js';
2
- export declare function statusCommand(options: StatusOptions): Promise<void>;
1
+ import type { HushContext, StatusOptions } from '../types.js';
2
+ export declare function statusCommand(ctx: HushContext, options: StatusOptions): Promise<void>;
3
3
  //# sourceMappingURL=status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwCjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHzE"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAyC9D,wBAAsB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAoH3F"}
@@ -1,31 +1,27 @@
1
- import { existsSync } from 'node:fs';
2
1
  import { join } from 'node:path';
3
2
  import pc from 'picocolors';
4
- import { findConfigPath, loadConfig } from '../config/loader.js';
5
3
  import { describeFilter } from '../core/filter.js';
6
- import { isAgeKeyConfigured, isSopsInstalled } from '../core/sops.js';
7
- import { keyExists } from '../lib/age.js';
8
- import { opInstalled } from '../lib/onepassword.js';
9
4
  import { FORMAT_OUTPUT_FILES } from '../types.js';
10
- function findRootPlaintextEnvFiles(root) {
5
+ function findRootPlaintextEnvFiles(ctx, root) {
11
6
  const results = [];
7
+ // Only warn about .env files (legacy/output), not .hush files (Hush's source files)
12
8
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
13
9
  for (const pattern of plaintextPatterns) {
14
10
  const filePath = join(root, pattern);
15
- if (existsSync(filePath)) {
11
+ if (ctx.fs.existsSync(filePath)) {
16
12
  results.push(pattern);
17
13
  }
18
14
  }
19
15
  return results;
20
16
  }
21
- function getProjectFromConfig(root) {
22
- const config = loadConfig(root);
17
+ function getProjectFromConfig(ctx, root) {
18
+ const config = ctx.config.loadConfig(root);
23
19
  if (config.project)
24
20
  return config.project;
25
21
  const pkgPath = join(root, 'package.json');
26
- if (existsSync(pkgPath)) {
22
+ if (ctx.fs.existsSync(pkgPath)) {
27
23
  try {
28
- const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf-8'));
24
+ const pkg = JSON.parse(ctx.fs.readFileSync(pkgPath, 'utf-8'));
29
25
  if (typeof pkg.repository === 'string') {
30
26
  const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
31
27
  if (match)
@@ -43,68 +39,69 @@ function getProjectFromConfig(root) {
43
39
  }
44
40
  return null;
45
41
  }
46
- export async function statusCommand(options) {
42
+ export async function statusCommand(ctx, options) {
47
43
  const { root } = options;
48
- const config = loadConfig(root);
49
- const configPath = findConfigPath(root);
50
- console.log(pc.blue('Hush Status\n'));
51
- const plaintextFiles = findRootPlaintextEnvFiles(root);
44
+ const config = ctx.config.loadConfig(root);
45
+ const projectRootResult = ctx.config.findProjectRoot(root);
46
+ const configPath = projectRootResult ? projectRootResult.configPath : null;
47
+ ctx.logger.log(pc.blue('Hush Status\n'));
48
+ const plaintextFiles = findRootPlaintextEnvFiles(ctx, root);
52
49
  if (plaintextFiles.length > 0) {
53
- console.log(pc.bgRed(pc.white(pc.bold(' SECURITY WARNING '))));
54
- console.log(pc.red(pc.bold('\nUnencrypted .env files detected at project root!\n')));
50
+ ctx.logger.log(pc.bgRed(pc.white(pc.bold(' SECURITY WARNING '))));
51
+ ctx.logger.log(pc.red(pc.bold('\nUnencrypted .env files detected at project root!\n')));
55
52
  for (const file of plaintextFiles) {
56
- console.log(pc.red(` ${file}`));
53
+ ctx.logger.log(pc.red(` ${file}`));
57
54
  }
58
- console.log('');
59
- console.log(pc.yellow('These files may expose secrets to AI assistants and version control.'));
60
- console.log(pc.bold('\nTo fix:'));
61
- console.log(pc.dim(' 1. Run: npx hush encrypt'));
62
- console.log(pc.dim(' 2. The plaintext files will be automatically deleted after encryption'));
63
- console.log(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars\n'));
55
+ ctx.logger.log('');
56
+ ctx.logger.log(pc.yellow('These files may expose secrets to AI assistants and version control.'));
57
+ ctx.logger.log(pc.bold('\nTo fix:'));
58
+ ctx.logger.log(pc.dim(' 1. Run: npx hush migrate (if upgrading from v4)'));
59
+ ctx.logger.log(pc.dim(' 2. Delete or gitignore these .env files'));
60
+ ctx.logger.log(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars\n'));
64
61
  }
65
- console.log(pc.bold('Config:'));
62
+ ctx.logger.log(pc.bold('Config:'));
66
63
  if (configPath) {
67
- console.log(pc.green(` ${configPath.replace(root + '/', '')}`));
64
+ ctx.logger.log(pc.green(` ${configPath.replace(root + '/', '')}`));
68
65
  }
69
66
  else {
70
- console.log(pc.dim(' No hush.yaml found (using defaults)'));
67
+ ctx.logger.log(pc.dim(' No hush.yaml found (using defaults)'));
71
68
  }
72
- const project = getProjectFromConfig(root);
69
+ const project = getProjectFromConfig(ctx, root);
73
70
  if (configPath) {
74
71
  if (project) {
75
- console.log(pc.green(` Project: ${project}`));
72
+ ctx.logger.log(pc.green(` Project: ${project}`));
76
73
  }
77
74
  else {
78
- console.log(pc.yellow(' Project: not set'));
79
- console.log(pc.dim(' Add "project: my-org/my-repo" to hush.yaml for key management'));
75
+ ctx.logger.log(pc.yellow(' Project: not set'));
76
+ ctx.logger.log(pc.dim(' Add "project: my-org/my-repo" to hush.yaml for key management'));
80
77
  }
81
78
  }
82
- console.log(pc.bold('\nPrerequisites:'));
83
- console.log(isSopsInstalled()
79
+ ctx.logger.log(pc.bold('\nPrerequisites:'));
80
+ ctx.logger.log(ctx.sops.isSopsInstalled()
84
81
  ? pc.green(' SOPS installed')
85
82
  : pc.red(' SOPS not installed (brew install sops)'));
86
- console.log(isAgeKeyConfigured()
83
+ ctx.logger.log(ctx.age.ageAvailable()
87
84
  ? pc.green(' age key configured')
88
85
  : pc.yellow(' age key not found at ~/.config/sops/age/key.txt'));
89
86
  if (project) {
90
- const hasLocalKey = keyExists(project);
91
- console.log(pc.bold('\nKey Status:'));
92
- console.log(hasLocalKey
87
+ const hasLocalKey = ctx.age.keyExists(project);
88
+ ctx.logger.log(pc.bold('\nKey Status:'));
89
+ ctx.logger.log(hasLocalKey
93
90
  ? pc.green(` Local key: ~/.config/sops/age/keys/${project.replace(/\//g, '-')}.txt`)
94
91
  : pc.yellow(' Local key: not found'));
95
- if (opInstalled()) {
96
- console.log(pc.dim(' 1Password CLI: installed'));
97
- console.log(pc.dim(' Run "npx hush keys list" to check backup status'));
92
+ if (ctx.onepassword.opAvailable()) {
93
+ ctx.logger.log(pc.dim(' 1Password CLI: installed'));
94
+ ctx.logger.log(pc.dim(' Run "npx hush keys list" to check backup status'));
98
95
  }
99
96
  else {
100
- console.log(pc.dim(' 1Password CLI: not installed'));
97
+ ctx.logger.log(pc.dim(' 1Password CLI: not installed'));
101
98
  }
102
99
  if (!hasLocalKey) {
103
- console.log(pc.bold('\n To set up keys:'));
104
- console.log(pc.dim(' npx hush keys setup # Pull from 1Password or generate'));
100
+ ctx.logger.log(pc.bold('\n To set up keys:'));
101
+ ctx.logger.log(pc.dim(' npx hush keys setup # Pull from 1Password or generate'));
105
102
  }
106
103
  }
107
- console.log(pc.bold('\nSource Files:'));
104
+ ctx.logger.log(pc.bold('\nSource Files:'));
108
105
  const sources = [
109
106
  { key: 'shared', path: config.sources.shared },
110
107
  { key: 'development', path: config.sources.development },
@@ -112,25 +109,25 @@ export async function statusCommand(options) {
112
109
  ];
113
110
  for (const { key, path } of sources) {
114
111
  const encryptedPath = join(root, path + '.encrypted');
115
- const exists = existsSync(encryptedPath);
112
+ const exists = ctx.fs.existsSync(encryptedPath);
116
113
  const label = `${path}.encrypted`;
117
- console.log(exists
114
+ ctx.logger.log(exists
118
115
  ? pc.green(` ${label}`)
119
116
  : pc.dim(` ${label} (not found)`));
120
117
  }
121
- const localPath = join(root, '.env.local');
122
- console.log(existsSync(localPath)
123
- ? pc.green(' .env.local (overrides)')
124
- : pc.dim(' .env.local (optional, not found)'));
125
- console.log(pc.bold('\nTargets:'));
118
+ const localEncryptedPath = join(root, config.sources.local + '.encrypted');
119
+ ctx.logger.log(ctx.fs.existsSync(localEncryptedPath)
120
+ ? pc.green(` ${config.sources.local}.encrypted (overrides)`)
121
+ : pc.dim(` ${config.sources.local}.encrypted (optional, not found)`));
122
+ ctx.logger.log(pc.bold('\nTargets:'));
126
123
  for (const target of config.targets) {
127
124
  const filter = describeFilter(target);
128
125
  const devOutput = FORMAT_OUTPUT_FILES[target.format].development;
129
- console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path + '/')} ` +
126
+ ctx.logger.log(` ${pc.cyan(target.name)} ${pc.dim(target.path + '/')} ` +
130
127
  `${pc.magenta(target.format)} -> ${devOutput}`);
131
128
  if (filter !== 'all vars') {
132
- console.log(pc.dim(` ${filter}`));
129
+ ctx.logger.log(pc.dim(` ${filter}`));
133
130
  }
134
131
  }
135
- console.log('');
132
+ ctx.logger.log('');
136
133
  }
@@ -1,7 +1,7 @@
1
- import type { Environment } from '../types.js';
1
+ import type { Environment, HushContext } from '../types.js';
2
2
  export interface TemplateOptions {
3
3
  root: string;
4
4
  env: Environment;
5
5
  }
6
- export declare function templateCommand(options: TemplateOptions): Promise<void>;
6
+ export declare function templateCommand(ctx: HushContext, options: TemplateOptions): Promise<void>;
7
7
  //# sourceMappingURL=template.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/commands/template.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,WAAW,EAAsB,MAAM,aAAa,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;CAClB;AA4CD,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuF7E"}
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/commands/template.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAsB,WAAW,EAAE,MAAM,aAAa,CAAC;AAEhF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;CAClB;AA4CD,wBAAsB,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuF/F"}
@@ -1,31 +1,28 @@
1
- import { existsSync } from 'node:fs';
2
1
  import { join, relative } from 'node:path';
3
2
  import pc from 'picocolors';
4
- import { loadConfig, findProjectRoot } from '../config/loader.js';
5
3
  import { interpolateVars } from '../core/interpolate.js';
6
4
  import { mergeVars } from '../core/merge.js';
7
5
  import { parseEnvContent } from '../core/parse.js';
8
- import { decrypt as sopsDecrypt } from '../core/sops.js';
9
6
  import { maskValue } from '../core/mask.js';
10
7
  import { loadLocalTemplates, resolveTemplateVars } from '../core/template.js';
11
8
  function getEncryptedPath(sourcePath) {
12
9
  return sourcePath + '.encrypted';
13
10
  }
14
- function getDecryptedSecrets(projectRoot, env, config) {
11
+ function getDecryptedSecrets(ctx, projectRoot, env, config) {
15
12
  const sharedEncrypted = join(projectRoot, getEncryptedPath(config.sources.shared));
16
13
  const envEncrypted = join(projectRoot, getEncryptedPath(config.sources[env]));
17
14
  const localEncrypted = join(projectRoot, getEncryptedPath(config.sources.local));
18
15
  const varSources = [];
19
- if (existsSync(sharedEncrypted)) {
20
- const content = sopsDecrypt(sharedEncrypted);
16
+ if (ctx.fs.existsSync(sharedEncrypted)) {
17
+ const content = ctx.sops.decrypt(sharedEncrypted);
21
18
  varSources.push(parseEnvContent(content));
22
19
  }
23
- if (existsSync(envEncrypted)) {
24
- const content = sopsDecrypt(envEncrypted);
20
+ if (ctx.fs.existsSync(envEncrypted)) {
21
+ const content = ctx.sops.decrypt(envEncrypted);
25
22
  varSources.push(parseEnvContent(content));
26
23
  }
27
- if (existsSync(localEncrypted)) {
28
- const content = sopsDecrypt(localEncrypted);
24
+ if (ctx.fs.existsSync(localEncrypted)) {
25
+ const content = ctx.sops.decrypt(localEncrypted);
29
26
  varSources.push(parseEnvContent(content));
30
27
  }
31
28
  if (varSources.length === 0) {
@@ -41,42 +38,42 @@ function getRootSecretsAsRecord(vars) {
41
38
  }
42
39
  return record;
43
40
  }
44
- export async function templateCommand(options) {
41
+ export async function templateCommand(ctx, options) {
45
42
  const { root, env } = options;
46
43
  const contextDir = root;
47
- const projectInfo = findProjectRoot(contextDir);
44
+ const projectInfo = ctx.config.findProjectRoot(contextDir);
48
45
  if (!projectInfo) {
49
- console.error(pc.red('No hush.yaml found in current directory or any parent directory.'));
50
- console.error(pc.dim('Run: npx hush init'));
51
- process.exit(1);
46
+ ctx.logger.error('No hush.yaml found in current directory or any parent directory.');
47
+ ctx.logger.error(pc.dim('Run: npx hush init'));
48
+ ctx.process.exit(1);
52
49
  }
53
50
  const { projectRoot } = projectInfo;
54
- const config = loadConfig(projectRoot);
55
- const localTemplate = loadLocalTemplates(contextDir, env);
51
+ const config = ctx.config.loadConfig(projectRoot);
52
+ const localTemplate = loadLocalTemplates(contextDir, env, ctx.fs);
56
53
  if (!localTemplate.hasTemplate) {
57
- console.log(pc.yellow('No local template found in current directory.'));
58
- console.log(pc.dim(`Looked for: .env, .env.${env}, .env.local`));
59
- console.log('');
60
- console.log(pc.dim('Without a local template, hush run will inject all root secrets.'));
61
- console.log(pc.dim('Create a .env file to define which variables this directory needs.'));
54
+ ctx.logger.log(pc.yellow('No local template found in current directory.'));
55
+ ctx.logger.log(pc.dim(`Looked for: .hush, .hush.${env}, .hush.local`));
56
+ ctx.logger.log('');
57
+ ctx.logger.log(pc.dim('Without a local template, hush run will inject all root secrets.'));
58
+ ctx.logger.log(pc.dim('Create a .hush file to define which variables this directory needs.'));
62
59
  return;
63
60
  }
64
- const rootSecrets = getDecryptedSecrets(projectRoot, env, config);
61
+ const rootSecrets = getDecryptedSecrets(ctx, projectRoot, env, config);
65
62
  const rootSecretsRecord = getRootSecretsAsRecord(rootSecrets);
66
- const resolvedVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
63
+ const resolvedVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: ctx.process.env });
67
64
  const relPath = relative(projectRoot, contextDir) || '.';
68
- console.log('');
69
- console.log(pc.bold(`Template: ${relPath}/`));
70
- console.log(pc.dim(`Project root: ${projectRoot}`));
71
- console.log(pc.dim(`Environment: ${env}`));
72
- console.log(pc.dim(`Files: ${localTemplate.files.join(', ')}`));
73
- console.log('');
65
+ ctx.logger.log('');
66
+ ctx.logger.log(pc.bold(`Template: ${relPath}/`));
67
+ ctx.logger.log(pc.dim(`Project root: ${projectRoot}`));
68
+ ctx.logger.log(pc.dim(`Environment: ${env}`));
69
+ ctx.logger.log(pc.dim(`Files: ${localTemplate.files.join(', ')}`));
70
+ ctx.logger.log('');
74
71
  const templateVarKeys = new Set(localTemplate.vars.map(v => v.key));
75
72
  const rootSecretKeys = new Set(Object.keys(rootSecretsRecord));
76
73
  let fromRoot = 0;
77
74
  let fromLocal = 0;
78
- console.log(pc.bold('Variables:'));
79
- console.log('');
75
+ ctx.logger.log(pc.bold('Variables:'));
76
+ ctx.logger.log('');
80
77
  const maxKeyLen = Math.max(...resolvedVars.map(v => v.key.length), 20);
81
78
  for (const resolved of resolvedVars) {
82
79
  const original = localTemplate.vars.find(v => v.key === resolved.key);
@@ -89,23 +86,23 @@ export async function templateCommand(options) {
89
86
  const refName = refMatch ? refMatch[1] : '';
90
87
  const isEnvRef = refName.startsWith('env:');
91
88
  if (isEnvRef) {
92
- console.log(` ${pc.cyan(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${masked}`);
89
+ ctx.logger.log(` ${pc.cyan(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${masked}`);
93
90
  fromLocal++;
94
91
  }
95
92
  else if (rootSecretKeys.has(refName)) {
96
- console.log(` ${pc.green(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${masked}`);
93
+ ctx.logger.log(` ${pc.green(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${masked}`);
97
94
  fromRoot++;
98
95
  }
99
96
  else {
100
- console.log(` ${pc.yellow(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${pc.yellow('(unresolved)')}`);
97
+ ctx.logger.log(` ${pc.yellow(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${pc.yellow('(unresolved)')}`);
101
98
  }
102
99
  }
103
100
  else {
104
- console.log(` ${pc.white(keyPadded)} = ${masked} ${pc.dim('(literal)')}`);
101
+ ctx.logger.log(` ${pc.white(keyPadded)} = ${masked} ${pc.dim('(literal)')}`);
105
102
  fromLocal++;
106
103
  }
107
104
  }
108
- console.log('');
109
- console.log(pc.dim(`Total: ${resolvedVars.length} variables (${fromRoot} from root, ${fromLocal} local/literal)`));
110
- console.log('');
105
+ ctx.logger.log('');
106
+ ctx.logger.log(pc.dim(`Total: ${resolvedVars.length} variables (${fromRoot} from root, ${fromLocal} local/literal)`));
107
+ ctx.logger.log('');
111
108
  }
@@ -1,8 +1,8 @@
1
- import type { Environment } from '../types.js';
1
+ import type { Environment, HushContext } from '../types.js';
2
2
  export interface TraceOptions {
3
3
  root: string;
4
4
  env: Environment;
5
5
  key: string;
6
6
  }
7
- export declare function traceCommand(options: TraceOptions): Promise<void>;
7
+ export declare function traceCommand(ctx: HushContext, options: TraceOptions): Promise<void>;
8
8
  //# sourceMappingURL=trace.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/commands/trace.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;AAEpE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAuCD,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8DzF"}
@@ -1,9 +1,6 @@
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 { parseEnvContent } from '../core/parse.js';
6
- import { decrypt as sopsDecrypt } from '../core/sops.js';
7
4
  function matchesPattern(key, pattern) {
8
5
  const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
9
6
  return regex.test(key);
@@ -31,11 +28,11 @@ function getTargetDisposition(key, target) {
31
28
  }
32
29
  return { status: 'included' };
33
30
  }
34
- export async function traceCommand(options) {
31
+ export async function traceCommand(ctx, options) {
35
32
  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:'));
33
+ const config = ctx.config.loadConfig(root);
34
+ ctx.logger.log(pc.bold(`\nTracing variable: ${pc.cyan(key)}\n`));
35
+ ctx.logger.log(pc.blue('Source Status:'));
39
36
  const sources = [
40
37
  { name: config.sources.shared, path: join(root, config.sources.shared + '.encrypted'), found: false },
41
38
  { name: config.sources.development, path: join(root, config.sources.development + '.encrypted'), found: false },
@@ -44,44 +41,44 @@ export async function traceCommand(options) {
44
41
  ];
45
42
  const maxSourceLen = Math.max(...sources.map(s => s.name.length));
46
43
  for (const source of sources) {
47
- if (!existsSync(source.path)) {
48
- console.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.dim('(file not found)')}`);
44
+ if (!ctx.fs.existsSync(source.path)) {
45
+ ctx.logger.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.dim('(file not found)')}`);
49
46
  continue;
50
47
  }
51
48
  try {
52
- const content = sopsDecrypt(source.path);
49
+ const content = ctx.sops.decrypt(source.path);
53
50
  const vars = parseEnvContent(content);
54
51
  const found = vars.some(v => v.key === key);
55
52
  source.found = found;
56
53
  if (found) {
57
- console.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.green('✅ Present')}`);
54
+ ctx.logger.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.green('✅ Present')}`);
58
55
  }
59
56
  else {
60
- console.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.dim('❌ Not found')}`);
57
+ ctx.logger.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.dim('❌ Not found')}`);
61
58
  }
62
59
  }
63
60
  catch {
64
- console.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.red('⚠️ Decrypt failed')}`);
61
+ ctx.logger.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.red('⚠️ Decrypt failed')}`);
65
62
  }
66
63
  }
67
64
  const foundInAnySource = sources.some(s => s.found);
68
- console.log(pc.blue(`\nTarget Disposition (Environment: ${env}):`));
65
+ ctx.logger.log(pc.blue(`\nTarget Disposition (Environment: ${env}):`));
69
66
  const maxTargetLen = Math.max(...config.targets.map(t => t.name.length));
70
67
  for (const target of config.targets) {
71
68
  const disposition = getTargetDisposition(key, target);
72
69
  const targetLabel = `[${target.name}]`.padEnd(maxTargetLen + 2);
73
70
  if (!foundInAnySource) {
74
- console.log(` ${targetLabel} : ${pc.yellow('⚠️ Variable not in any source')}`);
71
+ ctx.logger.log(` ${targetLabel} : ${pc.yellow('⚠️ Variable not in any source')}`);
75
72
  }
76
73
  else if (disposition.status === 'included') {
77
- console.log(` ${targetLabel} : ${pc.green('✅ Included')}`);
74
+ ctx.logger.log(` ${targetLabel} : ${pc.green('✅ Included')}`);
78
75
  }
79
76
  else if (disposition.status === 'excluded') {
80
- console.log(` ${targetLabel} : ${pc.red(`🚫 Excluded`)} ${pc.dim(`(${disposition.reason})`)}`);
77
+ ctx.logger.log(` ${targetLabel} : ${pc.red(`🚫 Excluded`)} ${pc.dim(`(${disposition.reason})`)}`);
81
78
  }
82
79
  else {
83
- console.log(` ${targetLabel} : ${pc.red(`🚫 Not included`)} ${pc.dim(`(${disposition.reason})`)}`);
80
+ ctx.logger.log(` ${targetLabel} : ${pc.red(`🚫 Not included`)} ${pc.dim(`(${disposition.reason})`)}`);
84
81
  }
85
82
  }
86
- console.log('');
83
+ ctx.logger.log('');
87
84
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { fs } from '../lib/fs.js';
2
2
  import { join, dirname, resolve } from 'node:path';
3
3
  import { parse as parseYaml } from 'yaml';
4
4
  import { DEFAULT_SOURCES, CURRENT_SCHEMA_VERSION } from '../types.js';
@@ -6,7 +6,7 @@ const CONFIG_FILENAMES = ['hush.yaml', 'hush.yml'];
6
6
  export function findConfigPath(root) {
7
7
  for (const filename of CONFIG_FILENAMES) {
8
8
  const configPath = join(root, filename);
9
- if (existsSync(configPath)) {
9
+ if (fs.existsSync(configPath)) {
10
10
  return configPath;
11
11
  }
12
12
  }
@@ -34,7 +34,7 @@ export function loadConfig(root) {
34
34
  targets: [{ name: 'root', path: '.', format: 'dotenv' }],
35
35
  };
36
36
  }
37
- const content = readFileSync(configPath, 'utf-8');
37
+ const content = fs.readFileSync(configPath, 'utf-8');
38
38
  const parsed = parseYaml(content);
39
39
  return {
40
40
  // Support both 'version' and 'schema_version' (prefer schema_version)
@@ -0,0 +1,3 @@
1
+ import type { HushContext } from './types.js';
2
+ export declare const defaultContext: HushContext;
3
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,eAAO,MAAM,cAAc,EAAE,WAkD5B,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { fs } from './lib/fs.js';
2
+ import { spawnSync, execSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ import { loadConfig, findProjectRoot } from './config/loader.js';
5
+ import { decrypt, isSopsInstalled } from './core/sops.js';
6
+ import { ageAvailable, ageGenerate, agePublicFromPrivate, keyExists, keySave, keyLoad, keyPath } from './lib/age.js';
7
+ import { opAvailable, opGetKey, opStoreKey } from './lib/onepassword.js';
8
+ import pc from 'picocolors';
9
+ export const defaultContext = {
10
+ fs,
11
+ path: {
12
+ join,
13
+ },
14
+ exec: {
15
+ spawnSync: (command, args, options) => {
16
+ // @ts-ignore - types are compatible at runtime
17
+ return spawnSync(command, args, options);
18
+ },
19
+ execSync: (command, options) => {
20
+ // @ts-ignore - types are compatible at runtime
21
+ return execSync(command, options);
22
+ },
23
+ },
24
+ logger: {
25
+ log: (message) => console.log(message),
26
+ error: (message) => console.error(pc.red(message)),
27
+ warn: (message) => console.warn(pc.yellow(message)),
28
+ info: (message) => console.info(pc.blue(message)),
29
+ },
30
+ process: {
31
+ cwd: () => process.cwd(),
32
+ exit: (code) => process.exit(code),
33
+ env: process.env,
34
+ stdin: process.stdin,
35
+ stdout: process.stdout,
36
+ },
37
+ config: {
38
+ loadConfig,
39
+ findProjectRoot,
40
+ },
41
+ age: {
42
+ ageAvailable,
43
+ ageGenerate,
44
+ keyExists,
45
+ keySave,
46
+ keyPath,
47
+ keyLoad,
48
+ agePublicFromPrivate,
49
+ },
50
+ onepassword: {
51
+ opAvailable,
52
+ opGetKey,
53
+ opStoreKey,
54
+ },
55
+ sops: {
56
+ decrypt,
57
+ isSopsInstalled,
58
+ },
59
+ };
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { fs } from '../lib/fs.js';
2
2
  export function parseEnvContent(content) {
3
3
  const vars = [];
4
4
  const lines = content.split('\n');
@@ -21,10 +21,10 @@ export function parseEnvContent(content) {
21
21
  return vars;
22
22
  }
23
23
  export function parseEnvFile(filePath) {
24
- if (!existsSync(filePath)) {
24
+ if (!fs.existsSync(filePath)) {
25
25
  return [];
26
26
  }
27
- const content = readFileSync(filePath, 'utf-8');
27
+ const content = fs.readFileSync(filePath, 'utf-8');
28
28
  return parseEnvContent(content);
29
29
  }
30
30
  export function varsToRecord(vars) {
package/dist/core/sops.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execSync, spawnSync } from 'node:child_process';
2
- import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
2
+ import { fs } from '../lib/fs.js';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir, homedir } from 'node:os';
5
5
  function getAgeKeyFile() {
@@ -7,7 +7,7 @@ function getAgeKeyFile() {
7
7
  return process.env.SOPS_AGE_KEY_FILE;
8
8
  }
9
9
  const defaultPath = join(homedir(), '.config', 'sops', 'age', 'key.txt');
10
- if (existsSync(defaultPath)) {
10
+ if (fs.existsSync(defaultPath)) {
11
11
  return defaultPath;
12
12
  }
13
13
  return undefined;
@@ -35,7 +35,7 @@ export function isAgeKeyConfigured() {
35
35
  return getAgeKeyFile() !== undefined;
36
36
  }
37
37
  export function decrypt(filePath) {
38
- if (!existsSync(filePath)) {
38
+ if (!fs.existsSync(filePath)) {
39
39
  throw new Error(`Encrypted file not found: ${filePath}`);
40
40
  }
41
41
  if (!isSopsInstalled()) {
@@ -59,7 +59,7 @@ export function decrypt(filePath) {
59
59
  }
60
60
  }
61
61
  export function encrypt(inputPath, outputPath) {
62
- if (!existsSync(inputPath)) {
62
+ if (!fs.existsSync(inputPath)) {
63
63
  throw new Error(`Input file not found: ${inputPath}`);
64
64
  }
65
65
  if (!isSopsInstalled()) {
@@ -78,7 +78,7 @@ export function encrypt(inputPath, outputPath) {
78
78
  }
79
79
  }
80
80
  export function edit(filePath) {
81
- if (!existsSync(filePath)) {
81
+ if (!fs.existsSync(filePath)) {
82
82
  throw new Error(`Encrypted file not found: ${filePath}`);
83
83
  }
84
84
  if (!isSopsInstalled()) {
@@ -98,7 +98,7 @@ export function setKey(filePath, key, value) {
98
98
  throw new Error('SOPS is not installed. Install with: brew install sops');
99
99
  }
100
100
  let content = '';
101
- if (existsSync(filePath)) {
101
+ if (fs.existsSync(filePath)) {
102
102
  content = decrypt(filePath);
103
103
  }
104
104
  const lines = content.split('\n').filter(line => line.trim() !== '');
@@ -117,7 +117,7 @@ export function setKey(filePath, key, value) {
117
117
  const newContent = updatedLines.join('\n') + '\n';
118
118
  const tempFile = join(tmpdir(), `hush-temp-${Date.now()}.env`);
119
119
  try {
120
- writeFileSync(tempFile, newContent, 'utf-8');
120
+ fs.writeFileSync(tempFile, newContent, 'utf-8');
121
121
  execSync(`sops --input-type dotenv --output-type dotenv --encrypt "${tempFile}" > "${filePath}"`, {
122
122
  encoding: 'utf-8',
123
123
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -125,8 +125,8 @@ export function setKey(filePath, key, value) {
125
125
  });
126
126
  }
127
127
  finally {
128
- if (existsSync(tempFile)) {
129
- unlinkSync(tempFile);
128
+ if (fs.existsSync(tempFile)) {
129
+ fs.unlinkSync(tempFile);
130
130
  }
131
131
  }
132
132
  }
@@ -1,11 +1,11 @@
1
1
  import { type InterpolateOptions } from './interpolate.js';
2
- import type { EnvVar, Environment } from '../types.js';
2
+ import type { EnvVar, Environment, HushContext } from '../types.js';
3
3
  export interface LocalTemplateResult {
4
4
  hasTemplate: boolean;
5
5
  templateDir: string;
6
6
  vars: EnvVar[];
7
7
  files: string[];
8
8
  }
9
- export declare function loadLocalTemplates(contextDir: string, env: Environment): LocalTemplateResult;
9
+ export declare function loadLocalTemplates(contextDir: string, env: Environment, fs: HushContext['fs']): LocalTemplateResult;
10
10
  export declare function resolveTemplateVars(templateVars: EnvVar[], rootSecrets: Record<string, string>, options?: InterpolateOptions): EnvVar[];
11
11
  //# sourceMappingURL=template.d.ts.map