@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
@@ -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;AAyCjD,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,32 +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 = [];
12
7
  // Only warn about .env files (legacy/output), not .hush files (Hush's source files)
13
8
  const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
14
9
  for (const pattern of plaintextPatterns) {
15
10
  const filePath = join(root, pattern);
16
- if (existsSync(filePath)) {
11
+ if (ctx.fs.existsSync(filePath)) {
17
12
  results.push(pattern);
18
13
  }
19
14
  }
20
15
  return results;
21
16
  }
22
- function getProjectFromConfig(root) {
23
- const config = loadConfig(root);
17
+ function getProjectFromConfig(ctx, root) {
18
+ const config = ctx.config.loadConfig(root);
24
19
  if (config.project)
25
20
  return config.project;
26
21
  const pkgPath = join(root, 'package.json');
27
- if (existsSync(pkgPath)) {
22
+ if (ctx.fs.existsSync(pkgPath)) {
28
23
  try {
29
- const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf-8'));
24
+ const pkg = JSON.parse(ctx.fs.readFileSync(pkgPath, 'utf-8'));
30
25
  if (typeof pkg.repository === 'string') {
31
26
  const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
32
27
  if (match)
@@ -44,68 +39,69 @@ function getProjectFromConfig(root) {
44
39
  }
45
40
  return null;
46
41
  }
47
- export async function statusCommand(options) {
42
+ export async function statusCommand(ctx, options) {
48
43
  const { root } = options;
49
- const config = loadConfig(root);
50
- const configPath = findConfigPath(root);
51
- console.log(pc.blue('Hush Status\n'));
52
- 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);
53
49
  if (plaintextFiles.length > 0) {
54
- console.log(pc.bgRed(pc.white(pc.bold(' SECURITY WARNING '))));
55
- 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')));
56
52
  for (const file of plaintextFiles) {
57
- console.log(pc.red(` ${file}`));
53
+ ctx.logger.log(pc.red(` ${file}`));
58
54
  }
59
- console.log('');
60
- console.log(pc.yellow('These files may expose secrets to AI assistants and version control.'));
61
- console.log(pc.bold('\nTo fix:'));
62
- console.log(pc.dim(' 1. Run: npx hush migrate (if upgrading from v4)'));
63
- console.log(pc.dim(' 2. Delete or gitignore these .env files'));
64
- 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'));
65
61
  }
66
- console.log(pc.bold('Config:'));
62
+ ctx.logger.log(pc.bold('Config:'));
67
63
  if (configPath) {
68
- console.log(pc.green(` ${configPath.replace(root + '/', '')}`));
64
+ ctx.logger.log(pc.green(` ${configPath.replace(root + '/', '')}`));
69
65
  }
70
66
  else {
71
- console.log(pc.dim(' No hush.yaml found (using defaults)'));
67
+ ctx.logger.log(pc.dim(' No hush.yaml found (using defaults)'));
72
68
  }
73
- const project = getProjectFromConfig(root);
69
+ const project = getProjectFromConfig(ctx, root);
74
70
  if (configPath) {
75
71
  if (project) {
76
- console.log(pc.green(` Project: ${project}`));
72
+ ctx.logger.log(pc.green(` Project: ${project}`));
77
73
  }
78
74
  else {
79
- console.log(pc.yellow(' Project: not set'));
80
- 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'));
81
77
  }
82
78
  }
83
- console.log(pc.bold('\nPrerequisites:'));
84
- console.log(isSopsInstalled()
79
+ ctx.logger.log(pc.bold('\nPrerequisites:'));
80
+ ctx.logger.log(ctx.sops.isSopsInstalled()
85
81
  ? pc.green(' SOPS installed')
86
82
  : pc.red(' SOPS not installed (brew install sops)'));
87
- console.log(isAgeKeyConfigured()
83
+ ctx.logger.log(ctx.age.ageAvailable()
88
84
  ? pc.green(' age key configured')
89
85
  : pc.yellow(' age key not found at ~/.config/sops/age/key.txt'));
90
86
  if (project) {
91
- const hasLocalKey = keyExists(project);
92
- console.log(pc.bold('\nKey Status:'));
93
- console.log(hasLocalKey
87
+ const hasLocalKey = ctx.age.keyExists(project);
88
+ ctx.logger.log(pc.bold('\nKey Status:'));
89
+ ctx.logger.log(hasLocalKey
94
90
  ? pc.green(` Local key: ~/.config/sops/age/keys/${project.replace(/\//g, '-')}.txt`)
95
91
  : pc.yellow(' Local key: not found'));
96
- if (opInstalled()) {
97
- console.log(pc.dim(' 1Password CLI: installed'));
98
- console.log(pc.dim(' Run "npx hush keys list" to check backup status'));
92
+ if (ctx.onepassword.opInstalled()) {
93
+ ctx.logger.log(pc.dim(' 1Password CLI: installed'));
94
+ ctx.logger.log(pc.dim(' Run "npx hush keys list" to check backup status'));
99
95
  }
100
96
  else {
101
- console.log(pc.dim(' 1Password CLI: not installed'));
97
+ ctx.logger.log(pc.dim(' 1Password CLI: not installed'));
102
98
  }
103
99
  if (!hasLocalKey) {
104
- console.log(pc.bold('\n To set up keys:'));
105
- 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'));
106
102
  }
107
103
  }
108
- console.log(pc.bold('\nSource Files:'));
104
+ ctx.logger.log(pc.bold('\nSource Files:'));
109
105
  const sources = [
110
106
  { key: 'shared', path: config.sources.shared },
111
107
  { key: 'development', path: config.sources.development },
@@ -113,25 +109,25 @@ export async function statusCommand(options) {
113
109
  ];
114
110
  for (const { key, path } of sources) {
115
111
  const encryptedPath = join(root, path + '.encrypted');
116
- const exists = existsSync(encryptedPath);
112
+ const exists = ctx.fs.existsSync(encryptedPath);
117
113
  const label = `${path}.encrypted`;
118
- console.log(exists
114
+ ctx.logger.log(exists
119
115
  ? pc.green(` ${label}`)
120
116
  : pc.dim(` ${label} (not found)`));
121
117
  }
122
118
  const localEncryptedPath = join(root, config.sources.local + '.encrypted');
123
- console.log(existsSync(localEncryptedPath)
119
+ ctx.logger.log(ctx.fs.existsSync(localEncryptedPath)
124
120
  ? pc.green(` ${config.sources.local}.encrypted (overrides)`)
125
121
  : pc.dim(` ${config.sources.local}.encrypted (optional, not found)`));
126
- console.log(pc.bold('\nTargets:'));
122
+ ctx.logger.log(pc.bold('\nTargets:'));
127
123
  for (const target of config.targets) {
128
124
  const filter = describeFilter(target);
129
125
  const devOutput = FORMAT_OUTPUT_FILES[target.format].development;
130
- console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path + '/')} ` +
126
+ ctx.logger.log(` ${pc.cyan(target.name)} ${pc.dim(target.path + '/')} ` +
131
127
  `${pc.magenta(target.format)} -> ${devOutput}`);
132
128
  if (filter !== 'all vars') {
133
- console.log(pc.dim(` ${filter}`));
129
+ ctx.logger.log(pc.dim(` ${filter}`));
134
130
  }
135
131
  }
136
- console.log('');
132
+ ctx.logger.log('');
137
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,WAmD5B,CAAC"}
@@ -0,0 +1,60 @@
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 { opInstalled, 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
+ opInstalled,
52
+ opAvailable,
53
+ opGetKey,
54
+ opStoreKey,
55
+ },
56
+ sops: {
57
+ decrypt,
58
+ isSopsInstalled,
59
+ },
60
+ };
@@ -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