@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.
- package/dist/cli.js +210 -74
- package/dist/commands/check.d.ts +4 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +249 -0
- package/dist/commands/decrypt.d.ts +1 -9
- package/dist/commands/decrypt.d.ts.map +1 -1
- package/dist/commands/decrypt.js +54 -88
- package/dist/commands/edit.d.ts +1 -7
- package/dist/commands/edit.d.ts.map +1 -1
- package/dist/commands/edit.js +13 -19
- package/dist/commands/encrypt.d.ts +1 -7
- package/dist/commands/encrypt.d.ts.map +1 -1
- package/dist/commands/encrypt.js +23 -19
- package/dist/commands/has.d.ts +9 -0
- package/dist/commands/has.d.ts.map +1 -0
- package/dist/commands/has.js +45 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +63 -0
- package/dist/commands/inspect.d.ts +7 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +50 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +35 -0
- package/dist/commands/push.d.ts +1 -8
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +45 -63
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +1005 -0
- package/dist/commands/status.d.ts +1 -7
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +43 -45
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +50 -0
- package/dist/core/filter.d.ts +4 -0
- package/dist/core/filter.d.ts.map +1 -0
- package/dist/core/filter.js +27 -0
- package/dist/core/interpolate.d.ts +6 -0
- package/dist/core/interpolate.d.ts.map +1 -0
- package/dist/core/interpolate.js +43 -0
- package/dist/core/mask.d.ts +11 -0
- package/dist/core/mask.d.ts.map +1 -0
- package/dist/core/mask.js +30 -0
- package/dist/core/merge.d.ts +3 -0
- package/dist/core/merge.d.ts.map +1 -0
- package/dist/core/merge.js +9 -0
- package/dist/core/parse.d.ts +3 -27
- package/dist/core/parse.d.ts.map +1 -1
- package/dist/core/parse.js +8 -77
- package/dist/core/sops.d.ts +1 -12
- package/dist/core/sops.d.ts.map +1 -1
- package/dist/core/sops.js +4 -25
- package/dist/formats/dotenv.d.ts +3 -0
- package/dist/formats/dotenv.d.ts.map +1 -0
- package/dist/formats/dotenv.js +3 -0
- package/dist/formats/index.d.ts +9 -0
- package/dist/formats/index.d.ts.map +1 -0
- package/dist/formats/index.js +20 -0
- package/dist/formats/json.d.ts +3 -0
- package/dist/formats/json.d.ts.map +1 -0
- package/dist/formats/json.js +7 -0
- package/dist/formats/shell.d.ts +3 -0
- package/dist/formats/shell.d.ts.map +1 -0
- package/dist/formats/shell.js +11 -0
- package/dist/formats/wrangler.d.ts +3 -0
- package/dist/formats/wrangler.d.ts.map +1 -0
- package/dist/formats/wrangler.js +3 -0
- package/dist/formats/yaml.d.ts +13 -0
- package/dist/formats/yaml.d.ts.map +1 -0
- package/dist/formats/yaml.js +50 -0
- package/dist/index.d.ts +17 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -9
- package/dist/lib/diff.d.ts +12 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +19 -0
- package/dist/types.d.ts +70 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +26 -6
- package/package.json +10 -8
- package/LICENSE +0 -21
- package/README.md +0 -194
- package/dist/core/discover.d.ts +0 -6
- package/dist/core/discover.d.ts.map +0 -1
- 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 {
|
|
5
|
+
import { editCommand } from './commands/edit.js';
|
|
8
6
|
import { statusCommand } from './commands/status.js';
|
|
9
|
-
|
|
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
|
-
|
|
17
|
-
encrypt
|
|
18
|
-
|
|
19
|
-
edit
|
|
20
|
-
|
|
21
|
-
|
|
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 <
|
|
25
|
-
|
|
26
|
-
--
|
|
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
|
|
30
|
-
hush
|
|
31
|
-
hush
|
|
32
|
-
hush
|
|
33
|
-
hush
|
|
34
|
-
hush edit
|
|
35
|
-
hush
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 === '
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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: ${
|
|
53
|
-
console.error(pc.dim('
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
139
|
+
if (arg === '--json') {
|
|
140
|
+
json = true;
|
|
141
|
+
continue;
|
|
60
142
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
143
|
+
if (arg === '--only-changed') {
|
|
144
|
+
onlyChanged = true;
|
|
145
|
+
continue;
|
|
64
146
|
}
|
|
65
|
-
|
|
66
|
-
|
|
147
|
+
if (arg === '--require-source') {
|
|
148
|
+
requireSource = true;
|
|
149
|
+
continue;
|
|
67
150
|
}
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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()
|
|
105
|
-
console.error(pc.red('Fatal error:'), error.message);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
});
|
|
243
|
+
main();
|
|
@@ -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 {
|
|
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":"
|
|
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"}
|