@chriscode/hush 1.0.0 → 2.1.0

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 (88) hide show
  1. package/dist/cli.js +210 -74
  2. package/dist/commands/check.d.ts +4 -0
  3. package/dist/commands/check.d.ts.map +1 -0
  4. package/dist/commands/check.js +249 -0
  5. package/dist/commands/decrypt.d.ts +1 -9
  6. package/dist/commands/decrypt.d.ts.map +1 -1
  7. package/dist/commands/decrypt.js +54 -88
  8. package/dist/commands/edit.d.ts +1 -7
  9. package/dist/commands/edit.d.ts.map +1 -1
  10. package/dist/commands/edit.js +13 -19
  11. package/dist/commands/encrypt.d.ts +1 -7
  12. package/dist/commands/encrypt.d.ts.map +1 -1
  13. package/dist/commands/encrypt.js +23 -19
  14. package/dist/commands/has.d.ts +9 -0
  15. package/dist/commands/has.d.ts.map +1 -0
  16. package/dist/commands/has.js +45 -0
  17. package/dist/commands/init.d.ts +3 -0
  18. package/dist/commands/init.d.ts.map +1 -0
  19. package/dist/commands/init.js +63 -0
  20. package/dist/commands/inspect.d.ts +7 -0
  21. package/dist/commands/inspect.d.ts.map +1 -0
  22. package/dist/commands/inspect.js +50 -0
  23. package/dist/commands/list.d.ts +3 -0
  24. package/dist/commands/list.d.ts.map +1 -0
  25. package/dist/commands/list.js +35 -0
  26. package/dist/commands/push.d.ts +1 -8
  27. package/dist/commands/push.d.ts.map +1 -1
  28. package/dist/commands/push.js +45 -63
  29. package/dist/commands/skill.d.ts +3 -0
  30. package/dist/commands/skill.d.ts.map +1 -0
  31. package/dist/commands/skill.js +1005 -0
  32. package/dist/commands/status.d.ts +1 -7
  33. package/dist/commands/status.d.ts.map +1 -1
  34. package/dist/commands/status.js +43 -45
  35. package/dist/config/loader.d.ts +5 -0
  36. package/dist/config/loader.d.ts.map +1 -0
  37. package/dist/config/loader.js +50 -0
  38. package/dist/core/filter.d.ts +4 -0
  39. package/dist/core/filter.d.ts.map +1 -0
  40. package/dist/core/filter.js +27 -0
  41. package/dist/core/interpolate.d.ts +6 -0
  42. package/dist/core/interpolate.d.ts.map +1 -0
  43. package/dist/core/interpolate.js +43 -0
  44. package/dist/core/mask.d.ts +11 -0
  45. package/dist/core/mask.d.ts.map +1 -0
  46. package/dist/core/mask.js +30 -0
  47. package/dist/core/merge.d.ts +3 -0
  48. package/dist/core/merge.d.ts.map +1 -0
  49. package/dist/core/merge.js +9 -0
  50. package/dist/core/parse.d.ts +3 -27
  51. package/dist/core/parse.d.ts.map +1 -1
  52. package/dist/core/parse.js +8 -77
  53. package/dist/core/sops.d.ts +1 -12
  54. package/dist/core/sops.d.ts.map +1 -1
  55. package/dist/core/sops.js +4 -25
  56. package/dist/formats/dotenv.d.ts +3 -0
  57. package/dist/formats/dotenv.d.ts.map +1 -0
  58. package/dist/formats/dotenv.js +3 -0
  59. package/dist/formats/index.d.ts +9 -0
  60. package/dist/formats/index.d.ts.map +1 -0
  61. package/dist/formats/index.js +20 -0
  62. package/dist/formats/json.d.ts +3 -0
  63. package/dist/formats/json.d.ts.map +1 -0
  64. package/dist/formats/json.js +7 -0
  65. package/dist/formats/shell.d.ts +3 -0
  66. package/dist/formats/shell.d.ts.map +1 -0
  67. package/dist/formats/shell.js +11 -0
  68. package/dist/formats/wrangler.d.ts +3 -0
  69. package/dist/formats/wrangler.d.ts.map +1 -0
  70. package/dist/formats/wrangler.js +3 -0
  71. package/dist/formats/yaml.d.ts +13 -0
  72. package/dist/formats/yaml.d.ts.map +1 -0
  73. package/dist/formats/yaml.js +50 -0
  74. package/dist/index.d.ts +17 -7
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +15 -9
  77. package/dist/lib/diff.d.ts +12 -0
  78. package/dist/lib/diff.d.ts.map +1 -0
  79. package/dist/lib/diff.js +19 -0
  80. package/dist/types.d.ts +70 -31
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/types.js +26 -6
  83. package/package.json +10 -8
  84. package/LICENSE +0 -21
  85. package/README.md +0 -194
  86. package/dist/core/discover.d.ts +0 -6
  87. package/dist/core/discover.d.ts.map +0 -1
  88. package/dist/core/discover.js +0 -81
package/dist/cli.js CHANGED
@@ -1,107 +1,243 @@
1
1
  #!/usr/bin/env node
2
- import { resolve } from 'node:path';
3
2
  import pc from 'picocolors';
4
3
  import { decryptCommand } from './commands/decrypt.js';
5
- import { editCommand } from './commands/edit.js';
6
4
  import { encryptCommand } from './commands/encrypt.js';
7
- import { pushCommand } from './commands/push.js';
5
+ import { editCommand } from './commands/edit.js';
8
6
  import { statusCommand } from './commands/status.js';
9
- const HELP = `
7
+ import { pushCommand } from './commands/push.js';
8
+ import { initCommand } from './commands/init.js';
9
+ import { listCommand } from './commands/list.js';
10
+ import { inspectCommand } from './commands/inspect.js';
11
+ import { hasCommand } from './commands/has.js';
12
+ import { checkCommand } from './commands/check.js';
13
+ import { skillCommand } from './commands/skill.js';
14
+ const VERSION = '2.1.0';
15
+ function printHelp() {
16
+ console.log(`
10
17
  ${pc.bold('hush')} - SOPS-based secrets management for monorepos
11
18
 
12
19
  ${pc.bold('Usage:')}
13
20
  hush <command> [options]
14
21
 
15
22
  ${pc.bold('Commands:')}
16
- decrypt Decrypt .env.encrypted and generate env files for all packages
17
- encrypt Encrypt .env to .env.encrypted
18
- push Push production secrets to Cloudflare Workers
19
- edit Open .env.encrypted in editor (SOPS inline edit)
20
- status Show discovered packages and their styles
21
- help Show this help message
23
+ init Initialize hush.yaml config
24
+ encrypt Encrypt source .env files
25
+ decrypt Decrypt and distribute to targets
26
+ set [file] Set/edit secrets in $EDITOR (alias: edit)
27
+ list List all variables (shows values)
28
+ inspect List all variables (masked values, AI-safe)
29
+ has <key> Check if a secret exists (exit 0 if set, 1 if not)
30
+ check Verify secrets are encrypted (for pre-commit hooks)
31
+ push Push secrets to Cloudflare Workers
32
+ status Show configuration and status
33
+ skill Install Claude Code / OpenCode skill
22
34
 
23
35
  ${pc.bold('Options:')}
24
- --env <dev|prod> Target environment (default: dev)
25
- --dry-run Don't make changes, just show what would happen
26
- --root <path> Monorepo root directory (default: cwd)
36
+ -e, --env <env> Environment: development or production (default: development)
37
+ -r, --root <dir> Root directory (default: current directory)
38
+ -q, --quiet Suppress output (has/check commands)
39
+ --dry-run Preview changes without applying (push only)
40
+ --warn Warn but exit 0 on drift (check only)
41
+ --json Output machine-readable JSON (check only)
42
+ --only-changed Only check git-modified files (check only)
43
+ --require-source Fail if source file is missing (check only)
44
+ --global Install skill to ~/.claude/skills/ (skill only)
45
+ --local Install skill to ./.claude/skills/ (skill only)
46
+ -h, --help Show this help message
47
+ -v, --version Show version number
27
48
 
28
49
  ${pc.bold('Examples:')}
29
- hush decrypt Decrypt for development
30
- hush decrypt --env prod Decrypt for production
31
- hush encrypt Encrypt .env file
32
- hush push Push prod secrets to Wrangler
33
- hush push --dry-run Preview what would be pushed
34
- hush edit Edit encrypted file in $EDITOR
35
- hush status Show package detection info
36
- `;
50
+ hush init Initialize hush.yaml config
51
+ hush encrypt Encrypt .env files
52
+ hush decrypt Decrypt for development
53
+ hush decrypt -e production Decrypt for production
54
+ hush set Set/edit shared secrets
55
+ hush set development Set/edit development secrets
56
+ hush list List all variables (shows values)
57
+ hush inspect List all variables (masked, AI-safe)
58
+ hush has DATABASE_URL Check if DATABASE_URL is set
59
+ hush has API_KEY -q && echo "API_KEY is configured"
60
+ hush check Verify secrets are encrypted
61
+ hush check --warn Check but don't fail on drift
62
+ hush check --json Output JSON for CI
63
+ hush push --dry-run Preview push to Cloudflare
64
+ hush status Show current status
65
+ hush skill Install Claude skill (interactive)
66
+ hush skill --global Install skill for all projects
67
+ hush skill --local Install skill for this project only
68
+ `);
69
+ }
70
+ function parseEnvironment(value) {
71
+ if (value === 'development' || value === 'dev')
72
+ return 'development';
73
+ if (value === 'production' || value === 'prod')
74
+ return 'production';
75
+ return null;
76
+ }
77
+ function parseFileKey(value) {
78
+ if (value === 'shared' || value === 'development' || value === 'production')
79
+ return value;
80
+ if (value === 'dev')
81
+ return 'development';
82
+ if (value === 'prod')
83
+ return 'production';
84
+ return null;
85
+ }
37
86
  function parseArgs(args) {
38
- const result = {
39
- command: 'help',
40
- env: 'dev',
41
- dryRun: false,
42
- root: process.cwd(),
43
- };
87
+ let command = '';
88
+ let env = 'development';
89
+ let root = process.cwd();
90
+ let dryRun = false;
91
+ let quiet = false;
92
+ let warn = false;
93
+ let json = false;
94
+ let onlyChanged = false;
95
+ let requireSource = false;
96
+ let global = false;
97
+ let local = false;
98
+ let file;
99
+ let key;
44
100
  for (let i = 0; i < args.length; i++) {
45
101
  const arg = args[i];
46
- if (arg === '--env' && args[i + 1]) {
47
- const envArg = args[i + 1];
48
- if (envArg === 'dev' || envArg === 'prod') {
49
- result.env = envArg;
102
+ if (arg === '-h' || arg === '--help') {
103
+ printHelp();
104
+ process.exit(0);
105
+ }
106
+ if (arg === '-v' || arg === '--version') {
107
+ console.log(VERSION);
108
+ process.exit(0);
109
+ }
110
+ if (arg === '-e' || arg === '--env') {
111
+ const nextArg = args[++i];
112
+ const parsed = parseEnvironment(nextArg);
113
+ if (parsed) {
114
+ env = parsed;
50
115
  }
51
116
  else {
52
- console.error(pc.red(`Invalid environment: ${envArg}`));
53
- console.error(pc.dim('Valid values: dev, prod'));
117
+ console.error(pc.red(`Invalid environment: ${nextArg}`));
118
+ console.error(pc.dim('Use: development, dev, production, or prod'));
54
119
  process.exit(1);
55
120
  }
56
- i++;
121
+ continue;
122
+ }
123
+ if (arg === '-r' || arg === '--root') {
124
+ root = args[++i];
125
+ continue;
126
+ }
127
+ if (arg === '--dry-run') {
128
+ dryRun = true;
129
+ continue;
130
+ }
131
+ if (arg === '-q' || arg === '--quiet') {
132
+ quiet = true;
133
+ continue;
134
+ }
135
+ if (arg === '--warn') {
136
+ warn = true;
137
+ continue;
57
138
  }
58
- else if (arg === '--dry-run') {
59
- result.dryRun = true;
139
+ if (arg === '--json') {
140
+ json = true;
141
+ continue;
60
142
  }
61
- else if (arg === '--root' && args[i + 1]) {
62
- result.root = resolve(args[i + 1]);
63
- i++;
143
+ if (arg === '--only-changed') {
144
+ onlyChanged = true;
145
+ continue;
64
146
  }
65
- else if (!arg.startsWith('-') && !result.command) {
66
- result.command = arg;
147
+ if (arg === '--require-source') {
148
+ requireSource = true;
149
+ continue;
67
150
  }
68
- else if (!arg.startsWith('-')) {
69
- result.command = arg;
151
+ if (arg === '--global') {
152
+ global = true;
153
+ continue;
154
+ }
155
+ if (arg === '--local') {
156
+ local = true;
157
+ continue;
158
+ }
159
+ if (!command && !arg.startsWith('-')) {
160
+ command = arg;
161
+ continue;
162
+ }
163
+ if ((command === 'set' || command === 'edit') && !arg.startsWith('-')) {
164
+ const parsed = parseFileKey(arg);
165
+ if (parsed) {
166
+ file = parsed;
167
+ }
168
+ else {
169
+ console.error(pc.red(`Invalid file: ${arg}`));
170
+ console.error(pc.dim('Use: shared, development, or production'));
171
+ process.exit(1);
172
+ }
173
+ continue;
174
+ }
175
+ if (command === 'has' && !arg.startsWith('-') && !key) {
176
+ key = arg;
177
+ continue;
70
178
  }
71
179
  }
72
- return result;
180
+ return { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key };
73
181
  }
74
182
  async function main() {
75
183
  const args = process.argv.slice(2);
76
- const parsed = parseArgs(args);
77
- switch (parsed.command) {
78
- case 'decrypt':
79
- await decryptCommand({ root: parsed.root, env: parsed.env });
80
- break;
81
- case 'encrypt':
82
- await encryptCommand({ root: parsed.root });
83
- break;
84
- case 'push':
85
- await pushCommand({ root: parsed.root, dryRun: parsed.dryRun });
86
- break;
87
- case 'edit':
88
- await editCommand({ root: parsed.root });
89
- break;
90
- case 'status':
91
- await statusCommand({ root: parsed.root });
92
- break;
93
- case 'help':
94
- case '--help':
95
- case '-h':
96
- console.log(HELP);
97
- break;
98
- default:
99
- console.error(pc.red(`Unknown command: ${parsed.command}`));
100
- console.log(HELP);
101
- process.exit(1);
184
+ if (args.length === 0) {
185
+ printHelp();
186
+ process.exit(0);
187
+ }
188
+ const { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key } = parseArgs(args);
189
+ try {
190
+ switch (command) {
191
+ case 'init':
192
+ await initCommand({ root });
193
+ break;
194
+ case 'encrypt':
195
+ await encryptCommand({ root });
196
+ break;
197
+ case 'decrypt':
198
+ await decryptCommand({ root, env });
199
+ break;
200
+ case 'set':
201
+ case 'edit':
202
+ await editCommand({ root, file });
203
+ break;
204
+ case 'list':
205
+ await listCommand({ root, env });
206
+ break;
207
+ case 'inspect':
208
+ await inspectCommand({ root, env });
209
+ break;
210
+ case 'has':
211
+ if (!key) {
212
+ console.error(pc.red('Usage: hush has <KEY>'));
213
+ process.exit(1);
214
+ }
215
+ await hasCommand({ root, env, key, quiet });
216
+ break;
217
+ case 'check':
218
+ await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource });
219
+ break;
220
+ case 'push':
221
+ await pushCommand({ root, dryRun });
222
+ break;
223
+ case 'status':
224
+ await statusCommand({ root });
225
+ break;
226
+ case 'skill':
227
+ await skillCommand({ root, global, local });
228
+ break;
229
+ default:
230
+ if (command) {
231
+ console.error(pc.red(`Unknown command: ${command}`));
232
+ }
233
+ printHelp();
234
+ process.exit(1);
235
+ }
236
+ }
237
+ catch (error) {
238
+ const err = error;
239
+ console.error(pc.red(`Error: ${err.message}`));
240
+ process.exit(1);
102
241
  }
103
242
  }
104
- main().catch((error) => {
105
- console.error(pc.red('Fatal error:'), error.message);
106
- process.exit(1);
107
- });
243
+ main();
@@ -0,0 +1,4 @@
1
+ import type { CheckOptions, CheckResult } from '../types.js';
2
+ export declare function check(options: CheckOptions): Promise<CheckResult>;
3
+ export declare function checkCommand(options: CheckOptions): Promise<void>;
4
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAc,MAAM,aAAa,CAAC;AA+C1F,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA4BvE;AAmLD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BvE"}
@@ -0,0 +1,249 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import pc from 'picocolors';
5
+ import { loadConfig, findConfigPath } from '../config/loader.js';
6
+ import { parseEnvContent } from '../core/parse.js';
7
+ import { decrypt as sopsDecrypt, isSopsInstalled } from '../core/sops.js';
8
+ import { computeDiff, isInSync } from '../lib/diff.js';
9
+ function getSourceEncryptedPairs(config) {
10
+ const pairs = [];
11
+ if (config.sources.shared) {
12
+ pairs.push({
13
+ source: config.sources.shared,
14
+ encrypted: config.sources.shared + '.encrypted',
15
+ sourceKey: 'shared',
16
+ });
17
+ }
18
+ if (config.sources.development) {
19
+ pairs.push({
20
+ source: config.sources.development,
21
+ encrypted: config.sources.development + '.encrypted',
22
+ sourceKey: 'development',
23
+ });
24
+ }
25
+ if (config.sources.production) {
26
+ pairs.push({
27
+ source: config.sources.production,
28
+ encrypted: config.sources.production + '.encrypted',
29
+ sourceKey: 'production',
30
+ });
31
+ }
32
+ return pairs;
33
+ }
34
+ function getGitChangedFiles(root) {
35
+ try {
36
+ const staged = execSync('git diff --cached --name-only', { cwd: root, encoding: 'utf-8' });
37
+ const unstaged = execSync('git diff --name-only', { cwd: root, encoding: 'utf-8' });
38
+ const files = [...staged.split('\n'), ...unstaged.split('\n')].filter(Boolean);
39
+ return new Set(files);
40
+ }
41
+ catch {
42
+ return new Set();
43
+ }
44
+ }
45
+ export async function check(options) {
46
+ const { root, requireSource, onlyChanged } = options;
47
+ if (!isSopsInstalled()) {
48
+ return {
49
+ status: 'error',
50
+ files: [{
51
+ source: '',
52
+ encrypted: '',
53
+ inSync: false,
54
+ added: [],
55
+ removed: [],
56
+ changed: [],
57
+ error: 'SOPS_NOT_INSTALLED',
58
+ }],
59
+ };
60
+ }
61
+ const configPath = findConfigPath(root);
62
+ if (!configPath) {
63
+ const config = loadConfig(root);
64
+ const pairs = getSourceEncryptedPairs(config);
65
+ return checkPairs(root, pairs, requireSource, onlyChanged);
66
+ }
67
+ const config = loadConfig(root);
68
+ const pairs = getSourceEncryptedPairs(config);
69
+ return checkPairs(root, pairs, requireSource, onlyChanged);
70
+ }
71
+ function checkPairs(root, pairs, requireSource, onlyChanged) {
72
+ const changedFiles = onlyChanged ? getGitChangedFiles(root) : null;
73
+ const results = [];
74
+ for (const { source, encrypted } of pairs) {
75
+ const sourcePath = join(root, source);
76
+ const encryptedPath = join(root, encrypted);
77
+ if (onlyChanged && changedFiles) {
78
+ const isSourceChanged = changedFiles.has(source);
79
+ const isEncryptedChanged = changedFiles.has(encrypted);
80
+ if (!isSourceChanged && !isEncryptedChanged) {
81
+ continue;
82
+ }
83
+ }
84
+ if (!existsSync(sourcePath)) {
85
+ if (requireSource) {
86
+ results.push({
87
+ source,
88
+ encrypted,
89
+ inSync: false,
90
+ added: [],
91
+ removed: [],
92
+ changed: [],
93
+ error: 'SOURCE_MISSING',
94
+ });
95
+ }
96
+ continue;
97
+ }
98
+ if (!existsSync(encryptedPath)) {
99
+ const sourceContent = readFileSync(sourcePath, 'utf-8');
100
+ const sourceVars = parseEnvContent(sourceContent);
101
+ const allKeys = sourceVars.map(v => v.key);
102
+ results.push({
103
+ source,
104
+ encrypted,
105
+ inSync: false,
106
+ added: allKeys,
107
+ removed: [],
108
+ changed: [],
109
+ error: 'ENCRYPTED_MISSING',
110
+ });
111
+ continue;
112
+ }
113
+ try {
114
+ const decryptedContent = sopsDecrypt(encryptedPath);
115
+ const sourceContent = readFileSync(sourcePath, 'utf-8');
116
+ const sourceVars = parseEnvContent(sourceContent);
117
+ const encryptedVars = parseEnvContent(decryptedContent);
118
+ const diff = computeDiff(sourceVars, encryptedVars);
119
+ results.push({
120
+ source,
121
+ encrypted,
122
+ inSync: isInSync(diff),
123
+ added: diff.added,
124
+ removed: diff.removed,
125
+ changed: diff.changed,
126
+ });
127
+ }
128
+ catch (error) {
129
+ const err = error;
130
+ if (err.message.includes('No matching age key')) {
131
+ results.push({
132
+ source,
133
+ encrypted,
134
+ inSync: false,
135
+ added: [],
136
+ removed: [],
137
+ changed: [],
138
+ error: 'DECRYPT_FAILED',
139
+ });
140
+ }
141
+ else {
142
+ throw error;
143
+ }
144
+ }
145
+ }
146
+ const hasError = results.some(r => r.error === 'SOPS_NOT_INSTALLED' || r.error === 'DECRYPT_FAILED');
147
+ const hasDrift = results.some(r => !r.inSync);
148
+ let status;
149
+ if (hasError) {
150
+ status = 'error';
151
+ }
152
+ else if (hasDrift) {
153
+ status = 'drift';
154
+ }
155
+ else {
156
+ status = 'ok';
157
+ }
158
+ return { status, files: results };
159
+ }
160
+ function formatTextOutput(result) {
161
+ const lines = [];
162
+ lines.push('Checking secrets...\n');
163
+ for (const file of result.files) {
164
+ if (file.error === 'SOPS_NOT_INSTALLED') {
165
+ lines.push(pc.red('Error: SOPS is not installed'));
166
+ lines.push(pc.dim('Run: brew install sops'));
167
+ continue;
168
+ }
169
+ if (file.error === 'SOURCE_MISSING') {
170
+ lines.push(pc.yellow(`Warning: ${file.source} not found (--require-source)`));
171
+ continue;
172
+ }
173
+ lines.push(`${file.source} ${pc.dim('->')} ${file.encrypted}`);
174
+ if (file.error === 'ENCRYPTED_MISSING') {
175
+ lines.push(pc.yellow(` Warning: ${file.encrypted} not found`));
176
+ if (file.added.length > 0) {
177
+ lines.push(` ${pc.yellow('All keys need encryption:')} ${file.added.join(', ')}`);
178
+ }
179
+ continue;
180
+ }
181
+ if (file.error === 'DECRYPT_FAILED') {
182
+ lines.push(pc.red(` Error: Failed to decrypt ${file.encrypted}`));
183
+ lines.push(pc.dim(" This usually means your age key doesn't match."));
184
+ lines.push(pc.dim(' Check: ~/.config/sops/age/key.txt'));
185
+ continue;
186
+ }
187
+ if (file.inSync) {
188
+ lines.push(pc.green(' ✓ In sync'));
189
+ }
190
+ else {
191
+ if (file.added.length > 0) {
192
+ lines.push(` ${pc.yellow('Added keys:')} ${file.added.join(', ')}`);
193
+ }
194
+ if (file.removed.length > 0) {
195
+ lines.push(` ${pc.yellow('Removed keys:')} ${file.removed.join(', ')}`);
196
+ }
197
+ if (file.changed.length > 0) {
198
+ lines.push(` ${pc.yellow('Changed keys:')} ${file.changed.join(', ')}`);
199
+ }
200
+ }
201
+ lines.push('');
202
+ }
203
+ const driftCount = result.files.filter(f => !f.inSync && !f.error).length;
204
+ const errorCount = result.files.filter(f => f.error === 'ENCRYPTED_MISSING').length;
205
+ const totalDrift = driftCount + errorCount;
206
+ if (result.status === 'error') {
207
+ const sopsError = result.files.find(f => f.error === 'SOPS_NOT_INSTALLED');
208
+ if (sopsError) {
209
+ return lines.join('\n');
210
+ }
211
+ lines.push(pc.red('✗ Errors occurred during check'));
212
+ }
213
+ else if (totalDrift > 0) {
214
+ lines.push(pc.yellow(`✗ Drift detected in ${totalDrift} file(s)`));
215
+ lines.push(pc.dim('Run: hush encrypt'));
216
+ }
217
+ else if (result.files.length > 0) {
218
+ lines.push(pc.green('✓ All secrets in sync'));
219
+ }
220
+ return lines.join('\n');
221
+ }
222
+ function formatJsonOutput(result) {
223
+ return JSON.stringify(result, null, 2);
224
+ }
225
+ export async function checkCommand(options) {
226
+ const result = await check(options);
227
+ if (!options.quiet) {
228
+ if (options.json) {
229
+ console.log(formatJsonOutput(result));
230
+ }
231
+ else {
232
+ console.log(formatTextOutput(result));
233
+ }
234
+ }
235
+ if (result.status === 'error') {
236
+ const hasSopsError = result.files.some(f => f.error === 'SOPS_NOT_INSTALLED');
237
+ const hasDecryptError = result.files.some(f => f.error === 'DECRYPT_FAILED');
238
+ if (hasSopsError || hasDecryptError) {
239
+ process.exit(3);
240
+ }
241
+ if (result.files.some(f => f.error === 'SOURCE_MISSING')) {
242
+ process.exit(2);
243
+ }
244
+ }
245
+ if (result.status === 'drift' && !options.warn) {
246
+ process.exit(1);
247
+ }
248
+ process.exit(0);
249
+ }
@@ -1,11 +1,3 @@
1
- import type { Environment } from '../types.js';
2
- interface DecryptOptions {
3
- root: string;
4
- env?: Environment;
5
- }
6
- /**
7
- * Decrypt command - decrypt .env.encrypted and generate env files for all packages
8
- */
1
+ import type { DecryptOptions } from '../types.js';
9
2
  export declare function decryptCommand(options: DecryptOptions): Promise<void>;
10
- export {};
11
3
  //# sourceMappingURL=decrypt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../src/commands/decrypt.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAmB,MAAM,aAAa,CAAC;AAEhE,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,WAAW,CAAC;CACnB;AAqCD;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAwF3E"}
1
+ {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../src/commands/decrypt.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAU,MAAM,aAAa,CAAC;AAO1D,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA2E3E"}