@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,17 +1,12 @@
1
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
1
  import pc from 'picocolors';
4
2
  import { stringify as stringifyYaml } from 'yaml';
5
- import { findConfigPath } from '../config/loader.js';
6
- import { ageAvailable, ageGenerate, keyExists, keySave, keyPath } from '../lib/age.js';
7
- import { opAvailable, opGetKey, opStoreKey } from '../lib/onepassword.js';
8
3
  import { DEFAULT_SOURCES } from '../types.js';
9
- function getProjectFromPackageJson(root) {
10
- const pkgPath = join(root, 'package.json');
11
- if (!existsSync(pkgPath))
4
+ function getProjectFromPackageJson(ctx, root) {
5
+ const pkgPath = ctx.path.join(root, 'package.json');
6
+ if (!ctx.fs.existsSync(pkgPath))
12
7
  return null;
13
8
  try {
14
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
9
+ const pkg = JSON.parse(ctx.fs.readFileSync(pkgPath, 'utf-8'));
15
10
  if (typeof pkg.repository === 'string') {
16
11
  const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
17
12
  if (match)
@@ -28,86 +23,85 @@ function getProjectFromPackageJson(root) {
28
23
  }
29
24
  return null;
30
25
  }
31
- async function tryExistingLocalKey(project) {
32
- if (!keyExists(project))
26
+ async function tryExistingLocalKey(ctx, project) {
27
+ if (!ctx.age.keyExists(project))
33
28
  return null;
34
- const existing = await import('../lib/age.js').then(m => m.keyLoad(project));
29
+ const existing = await ctx.age.keyLoad(project);
35
30
  if (!existing)
36
31
  return null;
37
- console.log(pc.green(`Using existing key for ${pc.cyan(project)}`));
32
+ ctx.logger.log(pc.green(`Using existing key for ${pc.cyan(project)}`));
38
33
  return { publicKey: existing.public, source: 'existing' };
39
34
  }
40
- async function tryPullFrom1Password(project) {
41
- if (!opAvailable())
35
+ async function tryPullFrom1Password(ctx, project) {
36
+ if (!ctx.onepassword.opAvailable())
42
37
  return null;
43
- console.log(pc.dim('Checking 1Password for existing key...'));
44
- const priv = opGetKey(project);
38
+ ctx.logger.log(pc.dim('Checking 1Password for existing key...'));
39
+ const priv = ctx.onepassword.opGetKey(project);
45
40
  if (!priv)
46
41
  return null;
47
- const { agePublicFromPrivate } = await import('../lib/age.js');
48
- const pub = agePublicFromPrivate(priv);
49
- keySave(project, { private: priv, public: pub });
50
- console.log(pc.green(`Pulled key from 1Password for ${pc.cyan(project)}`));
42
+ const pub = ctx.age.agePublicFromPrivate(priv);
43
+ ctx.age.keySave(project, { private: priv, public: pub });
44
+ ctx.logger.log(pc.green(`Pulled key from 1Password for ${pc.cyan(project)}`));
51
45
  return { publicKey: pub, source: '1password' };
52
46
  }
53
- function generateAndBackupKey(project) {
54
- if (!ageAvailable()) {
55
- console.log(pc.yellow('age not installed. Run: brew install age'));
47
+ function generateAndBackupKey(ctx, project) {
48
+ if (!ctx.age.ageAvailable()) {
49
+ ctx.logger.log(pc.yellow('age not installed. Run: brew install age'));
56
50
  return null;
57
51
  }
58
- console.log(pc.blue(`Generating new key for ${pc.cyan(project)}...`));
59
- const key = ageGenerate();
60
- keySave(project, key);
61
- console.log(pc.green(`Saved to ${keyPath(project)}`));
62
- console.log(pc.dim(`Public: ${key.public}`));
63
- if (opAvailable()) {
52
+ ctx.logger.log(pc.blue(`Generating new key for ${pc.cyan(project)}...`));
53
+ const key = ctx.age.ageGenerate();
54
+ ctx.age.keySave(project, key);
55
+ ctx.logger.log(pc.green(`Saved to ${ctx.age.keyPath(project)}`));
56
+ ctx.logger.log(pc.dim(`Public: ${key.public}`));
57
+ if (ctx.onepassword.opAvailable()) {
64
58
  try {
65
- opStoreKey(project, key.private, key.public);
66
- console.log(pc.green('Backed up to 1Password.'));
59
+ ctx.onepassword.opStoreKey(project, key.private, key.public);
60
+ ctx.logger.log(pc.green('Backed up to 1Password.'));
67
61
  }
68
62
  catch (e) {
69
- console.warn(pc.yellow(`Could not backup to 1Password: ${e.message}`));
63
+ ctx.logger.warn(pc.yellow(`Could not backup to 1Password: ${e.message}`));
70
64
  }
71
65
  }
72
66
  return { publicKey: key.public, source: 'generated' };
73
67
  }
74
- async function setupKey(root, project) {
68
+ async function setupKey(ctx, root, project) {
75
69
  if (!project) {
76
- console.log(pc.yellow('No project identifier found. Skipping key setup.'));
77
- console.log(pc.dim('Add "project: my-project" to hush.yaml or set repository in package.json'));
70
+ ctx.logger.log(pc.yellow('No project identifier found. Skipping key setup.'));
71
+ ctx.logger.log(pc.dim('Add "project: my-org/my-repo" to hush.yaml or set repository in package.json'));
78
72
  return null;
79
73
  }
80
- return ((await tryExistingLocalKey(project)) ||
81
- (await tryPullFrom1Password(project)) ||
82
- generateAndBackupKey(project));
74
+ return ((await tryExistingLocalKey(ctx, project)) ||
75
+ (await tryPullFrom1Password(ctx, project)) ||
76
+ generateAndBackupKey(ctx, project));
83
77
  }
84
- function createSopsConfig(root, publicKey) {
85
- const sopsPath = join(root, '.sops.yaml');
86
- if (existsSync(sopsPath)) {
87
- console.log(pc.yellow('.sops.yaml already exists. Add this public key if needed:'));
88
- console.log(` ${publicKey}`);
78
+ function createSopsConfig(ctx, root, publicKey) {
79
+ const sopsPath = ctx.path.join(root, '.sops.yaml');
80
+ if (ctx.fs.existsSync(sopsPath)) {
81
+ ctx.logger.log(pc.yellow('.sops.yaml already exists. Add this public key if needed:'));
82
+ ctx.logger.log(` ${publicKey}`);
89
83
  return;
90
84
  }
91
85
  const sopsConfig = stringifyYaml({
92
86
  creation_rules: [{ encrypted_regex: '.*', age: publicKey }]
93
87
  });
94
- writeFileSync(sopsPath, sopsConfig, 'utf-8');
95
- console.log(pc.green('Created .sops.yaml'));
88
+ ctx.fs.writeFileSync(sopsPath, sopsConfig, 'utf-8');
89
+ ctx.logger.log(pc.green('Created .sops.yaml'));
96
90
  }
97
- function detectTargets(root) {
91
+ function detectTargets(ctx, root) {
98
92
  const targets = [{ name: 'root', path: '.', format: 'dotenv' }];
99
- const entries = readdirSync(root, { withFileTypes: true });
93
+ const entries = ctx.fs.readdirSync(root, { withFileTypes: true });
100
94
  for (const entry of entries) {
101
95
  if (!entry.isDirectory())
102
96
  continue;
103
97
  if (entry.name.startsWith('.') || entry.name === 'node_modules')
104
98
  continue;
105
- const dirPath = join(root, entry.name);
106
- const packageJsonPath = join(dirPath, 'package.json');
107
- const wranglerPath = join(dirPath, 'wrangler.toml');
108
- if (!existsSync(packageJsonPath))
99
+ const dirPath = ctx.path.join(root, entry.name);
100
+ const packageJsonPath = ctx.path.join(dirPath, 'package.json');
101
+ const wranglerPath = ctx.path.join(dirPath, 'wrangler.toml');
102
+ if (!ctx.fs.existsSync(packageJsonPath))
109
103
  continue;
110
- if (existsSync(wranglerPath)) {
104
+ if (ctx.fs.existsSync(wranglerPath)) {
111
105
  targets.push({
112
106
  name: entry.name,
113
107
  path: `./${entry.name}`,
@@ -125,44 +119,65 @@ function detectTargets(root) {
125
119
  }
126
120
  return targets;
127
121
  }
128
- function findExistingPlaintextEnvFiles(root) {
122
+ function findExistingPlaintextEnvFiles(ctx, root) {
129
123
  const patterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
130
124
  const found = [];
131
125
  for (const pattern of patterns) {
132
- const filePath = join(root, pattern);
133
- if (existsSync(filePath)) {
126
+ const filePath = ctx.path.join(root, pattern);
127
+ if (ctx.fs.existsSync(filePath)) {
134
128
  found.push(pattern);
135
129
  }
136
130
  }
137
131
  return found;
138
132
  }
139
- export async function initCommand(options) {
133
+ function findExistingEncryptedFiles(ctx, root) {
134
+ const patterns = ['.env.encrypted', '.env.development.encrypted', '.env.production.encrypted', '.env.local.encrypted'];
135
+ const found = [];
136
+ for (const pattern of patterns) {
137
+ const filePath = ctx.path.join(root, pattern);
138
+ if (ctx.fs.existsSync(filePath)) {
139
+ found.push(pattern);
140
+ }
141
+ }
142
+ return found;
143
+ }
144
+ export async function initCommand(ctx, options) {
140
145
  const { root } = options;
141
- const existingConfig = findConfigPath(root);
146
+ const existingConfig = ctx.config.findProjectRoot(root);
142
147
  if (existingConfig) {
143
- console.error(pc.red(`Config already exists: ${existingConfig}`));
144
- process.exit(1);
148
+ ctx.logger.error(pc.red(`Config already exists: ${existingConfig.configPath}`));
149
+ ctx.process.exit(1);
150
+ }
151
+ ctx.logger.log(pc.blue('Initializing hush...\n'));
152
+ const existingEncryptedFiles = findExistingEncryptedFiles(ctx, root);
153
+ if (existingEncryptedFiles.length > 0) {
154
+ ctx.logger.log(pc.bgYellow(pc.black(' V4 ENCRYPTED FILES DETECTED ')));
155
+ ctx.logger.log(pc.yellow('\nFound existing v4 encrypted files:'));
156
+ for (const file of existingEncryptedFiles) {
157
+ ctx.logger.log(pc.yellow(` ${file}`));
158
+ }
159
+ ctx.logger.log(pc.dim('\nRun "npx hush migrate" to convert to v5 format (.hush.encrypted).\n'));
145
160
  }
146
- console.log(pc.blue('Initializing hush...\n'));
147
- const existingEnvFiles = findExistingPlaintextEnvFiles(root);
161
+ const existingEnvFiles = findExistingPlaintextEnvFiles(ctx, root);
148
162
  if (existingEnvFiles.length > 0) {
149
- console.log(pc.bgYellow(pc.black(' EXISTING SECRETS DETECTED ')));
150
- console.log(pc.yellow('\nFound existing .env files:'));
163
+ ctx.logger.log(pc.bgYellow(pc.black(' PLAINTEXT .ENV FILES DETECTED ')));
164
+ ctx.logger.log(pc.yellow('\nFound existing .env files:'));
151
165
  for (const file of existingEnvFiles) {
152
- console.log(pc.yellow(` ${file}`));
166
+ ctx.logger.log(pc.yellow(` ${file}`));
153
167
  }
154
- console.log(pc.dim('\nThese will be encrypted after setup. Run "npx hush encrypt" when ready.\n'));
168
+ ctx.logger.log(pc.dim('\nRename these to .hush files, then run "npx hush encrypt".\n'));
169
+ ctx.logger.log(pc.dim('Example: mv .env .hush && mv .env.development .hush.development\n'));
155
170
  }
156
- const project = getProjectFromPackageJson(root);
171
+ const project = getProjectFromPackageJson(ctx, root);
157
172
  if (!project) {
158
- console.log(pc.yellow('No project identifier found in package.json.'));
159
- console.log(pc.dim('Tip: Add "project: my-org/my-repo" to hush.yaml after creation for key management.\n'));
173
+ ctx.logger.log(pc.yellow('No project identifier found in package.json.'));
174
+ ctx.logger.log(pc.dim('Tip: Add "project: my-org/my-repo" to hush.yaml after creation for key management.\n'));
160
175
  }
161
- const keyResult = await setupKey(root, project);
176
+ const keyResult = await setupKey(ctx, root, project);
162
177
  if (keyResult) {
163
- createSopsConfig(root, keyResult.publicKey);
178
+ createSopsConfig(ctx, root, keyResult.publicKey);
164
179
  }
165
- const targets = detectTargets(root);
180
+ const targets = detectTargets(ctx, root);
166
181
  const config = {
167
182
  version: 2,
168
183
  sources: DEFAULT_SOURCES,
@@ -171,24 +186,29 @@ export async function initCommand(options) {
171
186
  };
172
187
  const yaml = stringifyYaml(config, { indent: 2 });
173
188
  const schemaComment = '# yaml-language-server: $schema=https://unpkg.com/@chriscode/hush/schema.json\n';
174
- const configPath = join(root, 'hush.yaml');
175
- writeFileSync(configPath, schemaComment + yaml, 'utf-8');
176
- console.log(pc.green(`\nCreated ${configPath}`));
177
- console.log(pc.dim('\nDetected targets:'));
189
+ const configPath = ctx.path.join(root, 'hush.yaml');
190
+ ctx.fs.writeFileSync(configPath, schemaComment + yaml, 'utf-8');
191
+ ctx.logger.log(pc.green(`\nCreated ${configPath}`));
192
+ ctx.logger.log(pc.dim('\nDetected targets:'));
178
193
  for (const target of targets) {
179
- console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
194
+ ctx.logger.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
180
195
  }
181
- console.log(pc.bold('\nNext steps:'));
182
- if (existingEnvFiles.length > 0) {
183
- console.log(pc.green(' 1. npx hush encrypt') + pc.dim(' # Encrypt existing .env files (deletes plaintext)'));
184
- console.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
185
- console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
196
+ ctx.logger.log(pc.bold('\nNext steps:'));
197
+ if (existingEncryptedFiles.length > 0) {
198
+ ctx.logger.log(pc.green(' 1. npx hush migrate') + pc.dim(' # Convert v4 .env.encrypted to v5 .hush.encrypted'));
199
+ ctx.logger.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
200
+ ctx.logger.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
201
+ }
202
+ else if (existingEnvFiles.length > 0) {
203
+ ctx.logger.log(pc.green(' 1. Rename .env files to .hush') + pc.dim(' # mv .env .hush'));
204
+ ctx.logger.log(pc.dim(' 2. npx hush encrypt') + pc.dim(' # Encrypt .hush files'));
205
+ ctx.logger.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
186
206
  }
187
207
  else {
188
- console.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
189
- console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
208
+ ctx.logger.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
209
+ ctx.logger.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
190
210
  }
191
- console.log(pc.dim('\nGit setup:'));
192
- console.log(pc.dim(' git add hush.yaml .sops.yaml'));
193
- console.log(pc.dim(' git commit -m "chore: add Hush secrets management"'));
211
+ ctx.logger.log(pc.dim('\nGit setup:'));
212
+ ctx.logger.log(pc.dim(' git add hush.yaml .sops.yaml'));
213
+ ctx.logger.log(pc.dim(' git commit -m "chore: add Hush secrets management"'));
194
214
  }
@@ -1,7 +1,7 @@
1
- import type { Environment } from '../types.js';
1
+ import type { Environment, HushContext } from '../types.js';
2
2
  export interface InspectOptions {
3
3
  root: string;
4
4
  env: Environment;
5
5
  }
6
- export declare function inspectCommand(options: InspectOptions): Promise<void>;
6
+ export declare function inspectCommand(ctx: HushContext, options: InspectOptions): Promise<void>;
7
7
  //# sourceMappingURL=inspect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;CAClB;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAqD3E"}
1
+ {"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAU,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEpE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;CAClB;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAqD7F"}
@@ -1,25 +1,23 @@
1
- import { existsSync } from 'node:fs';
1
+ import { fs } from '../lib/fs.js';
2
2
  import { join } from 'node:path';
3
3
  import pc from 'picocolors';
4
- import { loadConfig } from '../config/loader.js';
5
4
  import { filterVarsForTarget, describeFilter } from '../core/filter.js';
6
5
  import { interpolateVars } from '../core/interpolate.js';
7
6
  import { maskVars, formatMaskedVar } from '../core/mask.js';
8
7
  import { mergeVars } from '../core/merge.js';
9
8
  import { parseEnvContent } from '../core/parse.js';
10
- import { decrypt as sopsDecrypt } from '../core/sops.js';
11
- export async function inspectCommand(options) {
9
+ export async function inspectCommand(ctx, options) {
12
10
  const { root, env } = options;
13
- const config = loadConfig(root);
11
+ const config = ctx.config.loadConfig(root);
14
12
  const sharedEncrypted = join(root, config.sources.shared + '.encrypted');
15
13
  const envEncrypted = join(root, config.sources[env] + '.encrypted');
16
14
  const varSources = [];
17
- if (existsSync(sharedEncrypted)) {
18
- const content = sopsDecrypt(sharedEncrypted);
15
+ if (fs.existsSync(sharedEncrypted)) {
16
+ const content = ctx.sops.decrypt(sharedEncrypted);
19
17
  varSources.push(parseEnvContent(content));
20
18
  }
21
- if (existsSync(envEncrypted)) {
22
- const content = sopsDecrypt(envEncrypted);
19
+ if (fs.existsSync(envEncrypted)) {
20
+ const content = ctx.sops.decrypt(envEncrypted);
23
21
  varSources.push(parseEnvContent(content));
24
22
  }
25
23
  if (varSources.length === 0) {
@@ -31,20 +29,20 @@ export async function inspectCommand(options) {
31
29
  const interpolated = interpolateVars(merged);
32
30
  const masked = maskVars(interpolated);
33
31
  const maxKeyLen = Math.max(...masked.map(v => v.key.length));
34
- console.log(pc.blue(`\nSecrets for ${env}:\n`));
32
+ ctx.logger.log(pc.blue(`\nSecrets for ${env}:\n`));
35
33
  for (const v of masked) {
36
34
  const line = formatMaskedVar(v, maxKeyLen);
37
- console.log(` ${v.isSet ? pc.green(v.key.padEnd(maxKeyLen)) : pc.yellow(v.key.padEnd(maxKeyLen))} = ${v.isSet ? pc.dim(v.masked + ` (${v.length} chars)`) : pc.yellow('(not set)')}`);
35
+ ctx.logger.log(` ${v.isSet ? pc.green(v.key.padEnd(maxKeyLen)) : pc.yellow(v.key.padEnd(maxKeyLen))} = ${v.isSet ? pc.dim(v.masked + ` (${v.length} chars)`) : pc.yellow('(not set)')}`);
38
36
  }
39
- console.log(pc.dim(`\nTotal: ${masked.length} variables\n`));
40
- console.log(pc.blue('Target distribution:\n'));
37
+ ctx.logger.log(pc.dim(`\nTotal: ${masked.length} variables\n`));
38
+ ctx.logger.log(pc.blue('Target distribution:\n'));
41
39
  for (const target of config.targets) {
42
40
  const filtered = filterVarsForTarget(interpolated, target);
43
41
  const filter = describeFilter(target);
44
- console.log(` ${pc.cyan(target.name)} ${pc.dim(`(${target.path}/)`)} - ${filtered.length} vars`);
42
+ ctx.logger.log(` ${pc.cyan(target.name)} ${pc.dim(`(${target.path}/)`)} - ${filtered.length} vars`);
45
43
  if (filter !== 'all vars') {
46
- console.log(` ${pc.dim(filter)}`);
44
+ ctx.logger.log(` ${pc.dim(filter)}`);
47
45
  }
48
46
  }
49
- console.log('');
47
+ ctx.logger.log('');
50
48
  }
@@ -1,8 +1,9 @@
1
+ import { HushContext } from '../types.js';
1
2
  export interface KeysOptions {
2
3
  root: string;
3
4
  subcommand: string;
4
5
  vault?: string;
5
6
  force?: boolean;
6
7
  }
7
- export declare function keysCommand(options: KeysOptions): Promise<void>;
8
+ export declare function keysCommand(ctx: HushContext, options: KeysOptions): Promise<void>;
8
9
  //# sourceMappingURL=keys.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/commands/keys.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAwBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6HrE"}
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/commands/keys.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAwBD,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6HvF"}
@@ -1,17 +1,15 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
1
  import { join } from 'node:path';
3
2
  import pc from 'picocolors';
4
3
  import { stringify as yamlStringify } from 'yaml';
5
- import { loadConfig } from '../config/loader.js';
6
4
  import { opAvailable, opGetKey, opStoreKey, opListKeys } from '../lib/onepassword.js';
7
5
  import { ageAvailable, ageGenerate, agePublicFromPrivate, keyExists, keySave, keyLoad, keysList, keyPath } from '../lib/age.js';
8
- function getProject(root) {
9
- const config = loadConfig(root);
6
+ function getProject(ctx, root) {
7
+ const config = ctx.config.loadConfig(root);
10
8
  if (config.project)
11
9
  return config.project;
12
10
  const pkgPath = join(root, 'package.json');
13
- if (existsSync(pkgPath)) {
14
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
11
+ if (ctx.fs.existsSync(pkgPath)) {
12
+ const pkg = JSON.parse(ctx.fs.readFileSync(pkgPath, 'utf-8'));
15
13
  if (typeof pkg.repository === 'string') {
16
14
  const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
17
15
  if (match)
@@ -23,18 +21,18 @@ function getProject(root) {
23
21
  return match[1];
24
22
  }
25
23
  }
26
- console.error(pc.red('No project identifier found.'));
27
- console.error(pc.dim('Add "project: my-project" to hush.yaml'));
28
- process.exit(1);
24
+ ctx.logger.error(pc.red('No project identifier found.'));
25
+ ctx.logger.error(pc.dim('Add "project: my-project" to hush.yaml'));
26
+ ctx.process.exit(1);
29
27
  }
30
- export async function keysCommand(options) {
28
+ export async function keysCommand(ctx, options) {
31
29
  const { root, subcommand, vault, force } = options;
32
30
  switch (subcommand) {
33
31
  case 'setup': {
34
- const project = getProject(root);
35
- console.log(pc.blue(`Setting up keys for ${pc.cyan(project)}...`));
32
+ const project = getProject(ctx, root);
33
+ ctx.logger.log(pc.blue(`Setting up keys for ${pc.cyan(project)}...`));
36
34
  if (keyExists(project)) {
37
- console.log(pc.green('Key already exists locally.'));
35
+ ctx.logger.log(pc.green('Key already exists locally.'));
38
36
  return;
39
37
  }
40
38
  if (opAvailable()) {
@@ -42,95 +40,95 @@ export async function keysCommand(options) {
42
40
  if (priv) {
43
41
  const pub = agePublicFromPrivate(priv);
44
42
  keySave(project, { private: priv, public: pub });
45
- console.log(pc.green('Pulled key from 1Password.'));
43
+ ctx.logger.log(pc.green('Pulled key from 1Password.'));
46
44
  return;
47
45
  }
48
46
  }
49
- console.log(pc.yellow('No key found. Run "hush keys generate" to create one.'));
47
+ ctx.logger.log(pc.yellow('No key found. Run "hush keys generate" to create one.'));
50
48
  break;
51
49
  }
52
50
  case 'generate': {
53
51
  if (!ageAvailable()) {
54
- console.error(pc.red('age not installed. Run: brew install age'));
55
- process.exit(1);
52
+ ctx.logger.error(pc.red('age not installed. Run: brew install age'));
53
+ ctx.process.exit(1);
56
54
  }
57
- const project = getProject(root);
55
+ const project = getProject(ctx, root);
58
56
  if (keyExists(project) && !force) {
59
- console.error(pc.yellow(`Key exists for ${project}. Use --force to overwrite.`));
60
- process.exit(1);
57
+ ctx.logger.error(pc.yellow(`Key exists for ${project}. Use --force to overwrite.`));
58
+ ctx.process.exit(1);
61
59
  }
62
- console.log(pc.blue(`Generating key for ${pc.cyan(project)}...`));
60
+ ctx.logger.log(pc.blue(`Generating key for ${pc.cyan(project)}...`));
63
61
  const key = ageGenerate();
64
62
  keySave(project, key);
65
- console.log(pc.green(`Saved to ${keyPath(project)}`));
66
- console.log(pc.dim(`Public: ${key.public}`));
63
+ ctx.logger.log(pc.green(`Saved to ${keyPath(project)}`));
64
+ ctx.logger.log(pc.dim(`Public: ${key.public}`));
67
65
  if (opAvailable()) {
68
66
  try {
69
67
  opStoreKey(project, key.private, key.public, vault);
70
- console.log(pc.green('Stored in 1Password.'));
68
+ ctx.logger.log(pc.green('Stored in 1Password.'));
71
69
  }
72
70
  catch (e) {
73
- console.warn(pc.yellow(`Could not store in 1Password: ${e.message}`));
71
+ ctx.logger.warn(pc.yellow(`Could not store in 1Password: ${e.message}`));
74
72
  }
75
73
  }
76
74
  const sopsPath = join(root, '.sops.yaml');
77
- if (!existsSync(sopsPath)) {
78
- writeFileSync(sopsPath, yamlStringify({ creation_rules: [{ encrypted_regex: '.*', age: key.public }] }));
79
- console.log(pc.green('Created .sops.yaml'));
75
+ if (!ctx.fs.existsSync(sopsPath)) {
76
+ ctx.fs.writeFileSync(sopsPath, yamlStringify({ creation_rules: [{ encrypted_regex: '.*', age: key.public }] }));
77
+ ctx.logger.log(pc.green('Created .sops.yaml'));
80
78
  }
81
79
  else {
82
- console.log(pc.yellow('.sops.yaml exists. Add this public key:'));
83
- console.log(` ${key.public}`);
80
+ ctx.logger.log(pc.yellow('.sops.yaml exists. Add this public key:'));
81
+ ctx.logger.log(` ${key.public}`);
84
82
  }
85
83
  break;
86
84
  }
87
85
  case 'pull': {
88
86
  if (!opAvailable()) {
89
- console.error(pc.red('1Password CLI not available or not signed in.'));
90
- process.exit(1);
87
+ ctx.logger.error(pc.red('1Password CLI not available or not signed in.'));
88
+ ctx.process.exit(1);
91
89
  }
92
- const project = getProject(root);
90
+ const project = getProject(ctx, root);
93
91
  const priv = opGetKey(project, vault);
94
92
  if (!priv) {
95
- console.error(pc.red(`No key in 1Password for ${project}`));
96
- process.exit(1);
93
+ ctx.logger.error(pc.red(`No key in 1Password for ${project}`));
94
+ ctx.process.exit(1);
97
95
  }
98
96
  const pub = agePublicFromPrivate(priv);
99
97
  keySave(project, { private: priv, public: pub });
100
- console.log(pc.green(`Pulled and saved to ${keyPath(project)}`));
98
+ ctx.logger.log(pc.green(`Pulled and saved to ${keyPath(project)}`));
101
99
  break;
102
100
  }
103
101
  case 'push': {
104
102
  if (!opAvailable()) {
105
- console.error(pc.red('1Password CLI not available or not signed in.'));
106
- process.exit(1);
103
+ ctx.logger.error(pc.red('1Password CLI not available or not signed in.'));
104
+ ctx.process.exit(1);
107
105
  }
108
- const project = getProject(root);
106
+ const project = getProject(ctx, root);
109
107
  const key = keyLoad(project);
110
108
  if (!key) {
111
- console.error(pc.red(`No local key for ${project}`));
112
- process.exit(1);
109
+ ctx.logger.error(pc.red(`No local key for ${project}`));
110
+ ctx.process.exit(1);
113
111
  }
114
112
  opStoreKey(project, key.private, key.public, vault);
115
- console.log(pc.green('Pushed to 1Password.'));
113
+ ctx.logger.log(pc.green('Pushed to 1Password.'));
116
114
  break;
117
115
  }
118
116
  case 'list': {
119
- console.log(pc.blue('Local keys:'));
117
+ ctx.logger.log(pc.blue('Local keys:'));
120
118
  for (const k of keysList()) {
121
- console.log(` ${pc.cyan(k.project)} ${pc.dim(k.public.slice(0, 20))}...`);
119
+ ctx.logger.log(` ${pc.cyan(k.project)} ${pc.dim(k.public.slice(0, 20))}...`);
122
120
  }
123
121
  if (opAvailable()) {
124
- console.log(pc.blue('\n1Password keys:'));
122
+ ctx.logger.log(pc.blue('\n1Password keys:'));
125
123
  for (const project of opListKeys(vault)) {
126
- console.log(` ${pc.cyan(project)}`);
124
+ ctx.logger.log(` ${pc.cyan(project)}`);
127
125
  }
128
126
  }
129
127
  break;
130
128
  }
131
129
  default:
132
- console.error(pc.red(`Unknown: hush keys ${subcommand}`));
133
- console.log(pc.dim('Commands: setup, generate, pull, push, list'));
134
- process.exit(1);
130
+ ctx.logger.error(pc.red(`Unknown: hush keys ${subcommand}`));
131
+ ctx.logger.log(pc.dim('Commands: setup, generate, pull, push, list'));
132
+ ctx.process.exit(1);
135
133
  }
136
134
  }
@@ -1,3 +1,3 @@
1
- import type { ListOptions } from '../types.js';
2
- export declare function listCommand(options: ListOptions): Promise<void>;
1
+ import type { ListOptions, HushContext } from '../types.js';
2
+ export declare function listCommand(ctx: HushContext, options: ListOptions): Promise<void>;
3
3
  //# sourceMappingURL=list.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;AAEvD,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCrE"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAU,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEpE,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCvF"}
@@ -1,35 +1,32 @@
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 { 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
- export async function listCommand(options) {
6
+ export async function listCommand(ctx, options) {
10
7
  const { root, env } = options;
11
- const config = loadConfig(root);
12
- console.log(pc.blue(`Variables for ${env}:\n`));
8
+ const config = ctx.config.loadConfig(root);
9
+ ctx.logger.log(pc.blue(`Variables for ${env}:\n`));
13
10
  const sharedEncrypted = join(root, config.sources.shared + '.encrypted');
14
11
  const envEncrypted = join(root, config.sources[env] + '.encrypted');
15
12
  const varSources = [];
16
- if (existsSync(sharedEncrypted)) {
17
- const content = sopsDecrypt(sharedEncrypted);
13
+ if (ctx.fs.existsSync(sharedEncrypted)) {
14
+ const content = ctx.sops.decrypt(sharedEncrypted);
18
15
  varSources.push(parseEnvContent(content));
19
16
  }
20
- if (existsSync(envEncrypted)) {
21
- const content = sopsDecrypt(envEncrypted);
17
+ if (ctx.fs.existsSync(envEncrypted)) {
18
+ const content = ctx.sops.decrypt(envEncrypted);
22
19
  varSources.push(parseEnvContent(content));
23
20
  }
24
21
  if (varSources.length === 0) {
25
- console.error(pc.red('No encrypted files found'));
26
- process.exit(1);
22
+ ctx.logger.error(pc.red('No encrypted files found'));
23
+ ctx.process.exit(1);
27
24
  }
28
25
  const merged = mergeVars(...varSources);
29
26
  const interpolated = interpolateVars(merged);
30
27
  for (const { key, value } of interpolated) {
31
28
  const displayValue = value.length > 50 ? value.slice(0, 47) + '...' : value;
32
- console.log(`${pc.cyan(key)}=${pc.dim(displayValue)}`);
29
+ ctx.logger.log(`${pc.cyan(key)}=${pc.dim(displayValue)}`);
33
30
  }
34
- console.log(pc.dim(`\nTotal: ${interpolated.length} variables`));
31
+ ctx.logger.log(pc.dim(`\nTotal: ${interpolated.length} variables`));
35
32
  }
@@ -0,0 +1,7 @@
1
+ import { HushContext } from '../types.js';
2
+ export interface MigrateOptions {
3
+ root: string;
4
+ dryRun: boolean;
5
+ }
6
+ export declare function migrateCommand(ctx: HushContext, options: MigrateOptions): Promise<void>;
7
+ //# sourceMappingURL=migrate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AA+DD,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA4E7F"}