@chriscode/hush 2.1.0 → 2.3.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 +136 -21
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +67 -10
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +99 -4
- package/dist/commands/keys.d.ts +8 -0
- package/dist/commands/keys.d.ts.map +1 -0
- package/dist/commands/keys.js +136 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +77 -0
- package/dist/commands/set.d.ts +3 -0
- package/dist/commands/set.d.ts.map +1 -0
- package/dist/commands/set.js +120 -0
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +295 -223
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +12 -2
- package/dist/core/sops.d.ts +5 -0
- package/dist/core/sops.d.ts.map +1 -1
- package/dist/core/sops.js +49 -1
- package/dist/lib/age.d.ts +16 -0
- package/dist/lib/age.d.ts.map +1 -0
- package/dist/lib/age.js +61 -0
- package/dist/lib/onepassword.d.ts +6 -0
- package/dist/lib/onepassword.d.ts.map +1 -0
- package/dist/lib/onepassword.js +48 -0
- package/dist/types.d.ts +24 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,8 @@ import pc from 'picocolors';
|
|
|
3
3
|
import { decryptCommand } from './commands/decrypt.js';
|
|
4
4
|
import { encryptCommand } from './commands/encrypt.js';
|
|
5
5
|
import { editCommand } from './commands/edit.js';
|
|
6
|
+
import { setCommand } from './commands/set.js';
|
|
7
|
+
import { runCommand } from './commands/run.js';
|
|
6
8
|
import { statusCommand } from './commands/status.js';
|
|
7
9
|
import { pushCommand } from './commands/push.js';
|
|
8
10
|
import { initCommand } from './commands/init.js';
|
|
@@ -11,7 +13,9 @@ import { inspectCommand } from './commands/inspect.js';
|
|
|
11
13
|
import { hasCommand } from './commands/has.js';
|
|
12
14
|
import { checkCommand } from './commands/check.js';
|
|
13
15
|
import { skillCommand } from './commands/skill.js';
|
|
14
|
-
|
|
16
|
+
import { keysCommand } from './commands/keys.js';
|
|
17
|
+
import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
|
|
18
|
+
const VERSION = '2.3.0';
|
|
15
19
|
function printHelp() {
|
|
16
20
|
console.log(`
|
|
17
21
|
${pc.bold('hush')} - SOPS-based secrets management for monorepos
|
|
@@ -22,8 +26,9 @@ ${pc.bold('Usage:')}
|
|
|
22
26
|
${pc.bold('Commands:')}
|
|
23
27
|
init Initialize hush.yaml config
|
|
24
28
|
encrypt Encrypt source .env files
|
|
25
|
-
|
|
26
|
-
set
|
|
29
|
+
run -- <cmd> Run command with secrets in memory (AI-safe)
|
|
30
|
+
set <KEY> Set a single secret interactively (AI-safe)
|
|
31
|
+
edit [file] Edit all secrets in $EDITOR
|
|
27
32
|
list List all variables (shows values)
|
|
28
33
|
inspect List all variables (masked values, AI-safe)
|
|
29
34
|
has <key> Check if a secret exists (exit 0 if set, 1 if not)
|
|
@@ -31,40 +36,49 @@ ${pc.bold('Commands:')}
|
|
|
31
36
|
push Push secrets to Cloudflare Workers
|
|
32
37
|
status Show configuration and status
|
|
33
38
|
skill Install Claude Code / OpenCode skill
|
|
39
|
+
keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
|
|
40
|
+
|
|
41
|
+
${pc.bold('Deprecated Commands:')}
|
|
42
|
+
decrypt Write secrets to disk (unsafe - use 'run' instead)
|
|
34
43
|
|
|
35
44
|
${pc.bold('Options:')}
|
|
36
45
|
-e, --env <env> Environment: development or production (default: development)
|
|
37
46
|
-r, --root <dir> Root directory (default: current directory)
|
|
47
|
+
-t, --target <t> Target name from hush.yaml (run only)
|
|
38
48
|
-q, --quiet Suppress output (has/check commands)
|
|
39
49
|
--dry-run Preview changes without applying (push only)
|
|
40
50
|
--warn Warn but exit 0 on drift (check only)
|
|
41
51
|
--json Output machine-readable JSON (check only)
|
|
42
52
|
--only-changed Only check git-modified files (check only)
|
|
43
53
|
--require-source Fail if source file is missing (check only)
|
|
54
|
+
--allow-plaintext Allow plaintext .env files (check only, not recommended)
|
|
44
55
|
--global Install skill to ~/.claude/skills/ (skill only)
|
|
45
|
-
--local Install skill to ./.claude/skills/ (skill only)
|
|
56
|
+
--local Install skill to ./.claude/skills/ (skill/set only)
|
|
57
|
+
--gui Use macOS dialog for input (set only, for AI agents)
|
|
46
58
|
-h, --help Show this help message
|
|
47
59
|
-v, --version Show version number
|
|
48
60
|
|
|
49
61
|
${pc.bold('Examples:')}
|
|
50
|
-
hush init Initialize
|
|
62
|
+
hush init Initialize config + generate keys
|
|
51
63
|
hush encrypt Encrypt .env files
|
|
52
|
-
hush
|
|
53
|
-
hush
|
|
54
|
-
hush
|
|
55
|
-
hush set
|
|
56
|
-
hush
|
|
64
|
+
hush run -- npm start Run with secrets in memory (AI-safe!)
|
|
65
|
+
hush run -e prod -- npm build Run with production secrets
|
|
66
|
+
hush run -t api -- wrangler dev Run filtered for 'api' target
|
|
67
|
+
hush set DATABASE_URL Set a secret interactively (AI-safe)
|
|
68
|
+
hush set API_KEY --gui Set secret via macOS dialog (for AI agents)
|
|
69
|
+
hush set API_KEY -e prod Set a production secret
|
|
70
|
+
hush keys setup Pull key from 1Password or verify local
|
|
71
|
+
hush keys generate Generate new key + backup to 1Password
|
|
72
|
+
hush edit Edit all shared secrets in $EDITOR
|
|
73
|
+
hush edit development Edit development secrets in $EDITOR
|
|
74
|
+
hush edit local Edit personal local overrides
|
|
57
75
|
hush inspect List all variables (masked, AI-safe)
|
|
58
76
|
hush has DATABASE_URL Check if DATABASE_URL is set
|
|
59
77
|
hush has API_KEY -q && echo "API_KEY is configured"
|
|
60
78
|
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
79
|
hush push --dry-run Preview push to Cloudflare
|
|
64
80
|
hush status Show current status
|
|
65
81
|
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
82
|
`);
|
|
69
83
|
}
|
|
70
84
|
function parseEnvironment(value) {
|
|
@@ -75,7 +89,7 @@ function parseEnvironment(value) {
|
|
|
75
89
|
return null;
|
|
76
90
|
}
|
|
77
91
|
function parseFileKey(value) {
|
|
78
|
-
if (value === 'shared' || value === 'development' || value === 'production')
|
|
92
|
+
if (value === 'shared' || value === 'development' || value === 'production' || value === 'local')
|
|
79
93
|
return value;
|
|
80
94
|
if (value === 'dev')
|
|
81
95
|
return 'development';
|
|
@@ -85,7 +99,9 @@ function parseFileKey(value) {
|
|
|
85
99
|
}
|
|
86
100
|
function parseArgs(args) {
|
|
87
101
|
let command = '';
|
|
102
|
+
let subcommand;
|
|
88
103
|
let env = 'development';
|
|
104
|
+
let envExplicit = false;
|
|
89
105
|
let root = process.cwd();
|
|
90
106
|
let dryRun = false;
|
|
91
107
|
let quiet = false;
|
|
@@ -93,10 +109,16 @@ function parseArgs(args) {
|
|
|
93
109
|
let json = false;
|
|
94
110
|
let onlyChanged = false;
|
|
95
111
|
let requireSource = false;
|
|
112
|
+
let allowPlaintext = false;
|
|
96
113
|
let global = false;
|
|
97
114
|
let local = false;
|
|
115
|
+
let force = false;
|
|
116
|
+
let gui = false;
|
|
117
|
+
let vault;
|
|
98
118
|
let file;
|
|
99
119
|
let key;
|
|
120
|
+
let target;
|
|
121
|
+
let cmdArgs = [];
|
|
100
122
|
for (let i = 0; i < args.length; i++) {
|
|
101
123
|
const arg = args[i];
|
|
102
124
|
if (arg === '-h' || arg === '--help') {
|
|
@@ -112,6 +134,7 @@ function parseArgs(args) {
|
|
|
112
134
|
const parsed = parseEnvironment(nextArg);
|
|
113
135
|
if (parsed) {
|
|
114
136
|
env = parsed;
|
|
137
|
+
envExplicit = true;
|
|
115
138
|
}
|
|
116
139
|
else {
|
|
117
140
|
console.error(pc.red(`Invalid environment: ${nextArg}`));
|
|
@@ -148,6 +171,10 @@ function parseArgs(args) {
|
|
|
148
171
|
requireSource = true;
|
|
149
172
|
continue;
|
|
150
173
|
}
|
|
174
|
+
if (arg === '--allow-plaintext') {
|
|
175
|
+
allowPlaintext = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
151
178
|
if (arg === '--global') {
|
|
152
179
|
global = true;
|
|
153
180
|
continue;
|
|
@@ -156,28 +183,84 @@ function parseArgs(args) {
|
|
|
156
183
|
local = true;
|
|
157
184
|
continue;
|
|
158
185
|
}
|
|
186
|
+
if (arg === '--force' || arg === '-f') {
|
|
187
|
+
force = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (arg === '--gui') {
|
|
191
|
+
gui = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (arg === '--vault') {
|
|
195
|
+
vault = args[++i];
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (arg === '-t' || arg === '--target') {
|
|
199
|
+
target = args[++i];
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (arg === '--') {
|
|
203
|
+
cmdArgs = args.slice(i + 1);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
159
206
|
if (!command && !arg.startsWith('-')) {
|
|
160
207
|
command = arg;
|
|
161
208
|
continue;
|
|
162
209
|
}
|
|
163
|
-
if (
|
|
210
|
+
if (command === 'edit' && !arg.startsWith('-')) {
|
|
164
211
|
const parsed = parseFileKey(arg);
|
|
165
212
|
if (parsed) {
|
|
166
213
|
file = parsed;
|
|
167
214
|
}
|
|
168
215
|
else {
|
|
169
216
|
console.error(pc.red(`Invalid file: ${arg}`));
|
|
170
|
-
console.error(pc.dim('Use: shared, development, or
|
|
217
|
+
console.error(pc.dim('Use: shared, development, production, or local'));
|
|
171
218
|
process.exit(1);
|
|
172
219
|
}
|
|
173
220
|
continue;
|
|
174
221
|
}
|
|
222
|
+
if (command === 'set' && !arg.startsWith('-') && !key) {
|
|
223
|
+
key = arg;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
175
226
|
if (command === 'has' && !arg.startsWith('-') && !key) {
|
|
176
227
|
key = arg;
|
|
177
228
|
continue;
|
|
178
229
|
}
|
|
230
|
+
if (command === 'keys' && !arg.startsWith('-') && !subcommand) {
|
|
231
|
+
subcommand = arg;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { command, subcommand, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs };
|
|
236
|
+
}
|
|
237
|
+
function checkMigrationNeeded(root, command) {
|
|
238
|
+
const skipCommands = ['', 'help', 'version', 'init', 'skill'];
|
|
239
|
+
if (skipCommands.includes(command))
|
|
240
|
+
return;
|
|
241
|
+
const configPath = findConfigPath(root);
|
|
242
|
+
if (!configPath)
|
|
243
|
+
return;
|
|
244
|
+
const config = loadConfig(root);
|
|
245
|
+
const { needsMigration, from, to } = checkSchemaVersion(config);
|
|
246
|
+
if (needsMigration) {
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(pc.yellow('━'.repeat(60)));
|
|
249
|
+
console.log(pc.yellow(pc.bold(' Migration Required')));
|
|
250
|
+
console.log(pc.yellow('━'.repeat(60)));
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log(` Your ${pc.cyan('hush.yaml')} uses schema version ${pc.bold(String(from))}.`);
|
|
253
|
+
console.log(` Hush ${VERSION} uses schema version ${pc.bold(String(to))}.`);
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log(pc.dim(' Migration guide:'));
|
|
256
|
+
console.log(` ${pc.cyan(`https://hush-docs.pages.dev/migrations/v${from}-to-v${to}`)}`);
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(pc.dim(' Or ask your AI assistant:'));
|
|
259
|
+
console.log(pc.dim(` "Help me migrate hush.yaml from schema v${from} to v${to}"`));
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log(pc.yellow('━'.repeat(60)));
|
|
262
|
+
console.log('');
|
|
179
263
|
}
|
|
180
|
-
return { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key };
|
|
181
264
|
}
|
|
182
265
|
async function main() {
|
|
183
266
|
const args = process.argv.slice(2);
|
|
@@ -185,7 +268,8 @@ async function main() {
|
|
|
185
268
|
printHelp();
|
|
186
269
|
process.exit(0);
|
|
187
270
|
}
|
|
188
|
-
const { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key } = parseArgs(args);
|
|
271
|
+
const { command, subcommand, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs } = parseArgs(args);
|
|
272
|
+
checkMigrationNeeded(root, command);
|
|
189
273
|
try {
|
|
190
274
|
switch (command) {
|
|
191
275
|
case 'init':
|
|
@@ -195,9 +279,32 @@ async function main() {
|
|
|
195
279
|
await encryptCommand({ root });
|
|
196
280
|
break;
|
|
197
281
|
case 'decrypt':
|
|
282
|
+
console.warn(pc.yellow('⚠️ Warning: "hush decrypt" is deprecated and writes unencrypted secrets to disk.'));
|
|
283
|
+
console.warn(pc.yellow(' Use "hush run -- <command>" instead for better security.'));
|
|
284
|
+
console.warn(pc.dim(' To suppress this warning, use "hush unsafe:decrypt"'));
|
|
285
|
+
console.warn('');
|
|
198
286
|
await decryptCommand({ root, env });
|
|
199
287
|
break;
|
|
200
|
-
case '
|
|
288
|
+
case 'unsafe:decrypt':
|
|
289
|
+
console.warn(pc.red('⚠️ UNSAFE MODE: Writing unencrypted secrets to disk.'));
|
|
290
|
+
console.warn(pc.red(' These files will be readable by AI assistants and other tools.'));
|
|
291
|
+
console.warn('');
|
|
292
|
+
await decryptCommand({ root, env });
|
|
293
|
+
break;
|
|
294
|
+
case 'run':
|
|
295
|
+
await runCommand({ root, env, target, command: cmdArgs });
|
|
296
|
+
break;
|
|
297
|
+
case 'set': {
|
|
298
|
+
let setFile = 'shared';
|
|
299
|
+
if (local) {
|
|
300
|
+
setFile = 'local';
|
|
301
|
+
}
|
|
302
|
+
else if (envExplicit) {
|
|
303
|
+
setFile = env;
|
|
304
|
+
}
|
|
305
|
+
await setCommand({ root, file: setFile, key, gui });
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
201
308
|
case 'edit':
|
|
202
309
|
await editCommand({ root, file });
|
|
203
310
|
break;
|
|
@@ -215,7 +322,7 @@ async function main() {
|
|
|
215
322
|
await hasCommand({ root, env, key, quiet });
|
|
216
323
|
break;
|
|
217
324
|
case 'check':
|
|
218
|
-
await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource });
|
|
325
|
+
await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
|
|
219
326
|
break;
|
|
220
327
|
case 'push':
|
|
221
328
|
await pushCommand({ root, dryRun });
|
|
@@ -226,6 +333,14 @@ async function main() {
|
|
|
226
333
|
case 'skill':
|
|
227
334
|
await skillCommand({ root, global, local });
|
|
228
335
|
break;
|
|
336
|
+
case 'keys':
|
|
337
|
+
if (!subcommand) {
|
|
338
|
+
console.error(pc.red('Usage: hush keys <command>'));
|
|
339
|
+
console.error(pc.dim('Commands: setup, generate, pull, push, list'));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
await keysCommand({ root, subcommand, vault, force });
|
|
343
|
+
break;
|
|
229
344
|
default:
|
|
230
345
|
if (command) {
|
|
231
346
|
console.error(pc.red(`Unknown command: ${command}`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,MAAM,aAAa,CAAC;AAkF/G,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAuMD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
|
package/dist/commands/check.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { loadConfig
|
|
5
|
+
import { loadConfig } from '../config/loader.js';
|
|
6
6
|
import { parseEnvContent } from '../core/parse.js';
|
|
7
7
|
import { decrypt as sopsDecrypt, isSopsInstalled } from '../core/sops.js';
|
|
8
8
|
import { computeDiff, isInSync } from '../lib/diff.js';
|
|
@@ -42,8 +42,41 @@ function getGitChangedFiles(root) {
|
|
|
42
42
|
return new Set();
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
function findPlaintextEnvFiles(root) {
|
|
46
|
+
const results = [];
|
|
47
|
+
const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
48
|
+
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
|
|
49
|
+
function scanDir(dir, relativePath = '') {
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = readdirSync(dir);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (skipDirs.has(entry))
|
|
59
|
+
continue;
|
|
60
|
+
const fullPath = join(dir, entry);
|
|
61
|
+
const relPath = relativePath ? `${relativePath}/${entry}` : entry;
|
|
62
|
+
try {
|
|
63
|
+
if (statSync(fullPath).isDirectory()) {
|
|
64
|
+
scanDir(fullPath, relPath);
|
|
65
|
+
}
|
|
66
|
+
else if (plaintextPatterns.includes(entry)) {
|
|
67
|
+
results.push({ file: relPath, keyCount: 0 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
scanDir(root);
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
45
78
|
export async function check(options) {
|
|
46
|
-
const { root, requireSource, onlyChanged } = options;
|
|
79
|
+
const { root, requireSource, onlyChanged, allowPlaintext } = options;
|
|
47
80
|
if (!isSopsInstalled()) {
|
|
48
81
|
return {
|
|
49
82
|
status: 'error',
|
|
@@ -58,15 +91,17 @@ export async function check(options) {
|
|
|
58
91
|
}],
|
|
59
92
|
};
|
|
60
93
|
}
|
|
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
94
|
const config = loadConfig(root);
|
|
68
95
|
const pairs = getSourceEncryptedPairs(config);
|
|
69
|
-
|
|
96
|
+
const result = checkPairs(root, pairs, requireSource, onlyChanged);
|
|
97
|
+
if (!allowPlaintext) {
|
|
98
|
+
const plaintextFiles = findPlaintextEnvFiles(root);
|
|
99
|
+
if (plaintextFiles.length > 0) {
|
|
100
|
+
result.plaintextFiles = plaintextFiles;
|
|
101
|
+
result.status = 'plaintext';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
70
105
|
}
|
|
71
106
|
function checkPairs(root, pairs, requireSource, onlyChanged) {
|
|
72
107
|
const changedFiles = onlyChanged ? getGitChangedFiles(root) : null;
|
|
@@ -160,6 +195,25 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
|
|
|
160
195
|
function formatTextOutput(result) {
|
|
161
196
|
const lines = [];
|
|
162
197
|
lines.push('Checking secrets...\n');
|
|
198
|
+
if (result.plaintextFiles && result.plaintextFiles.length > 0) {
|
|
199
|
+
lines.push(pc.red(pc.bold('⚠ PLAINTEXT SECRETS DETECTED')));
|
|
200
|
+
lines.push('');
|
|
201
|
+
lines.push(pc.red('The following unencrypted .env files were found:'));
|
|
202
|
+
for (const pf of result.plaintextFiles) {
|
|
203
|
+
lines.push(pc.red(` • ${pf.file}`));
|
|
204
|
+
}
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push(pc.yellow('These files contain plaintext secrets that could be exposed to AI assistants.'));
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(pc.bold('To fix:'));
|
|
209
|
+
lines.push(pc.dim(' 1. Run: hush encrypt'));
|
|
210
|
+
lines.push(pc.dim(' 2. Delete the plaintext files (the .encrypted versions are your source of truth)'));
|
|
211
|
+
lines.push(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars'));
|
|
212
|
+
lines.push('');
|
|
213
|
+
lines.push(pc.dim('To allow plaintext files (not recommended): --allow-plaintext'));
|
|
214
|
+
lines.push('');
|
|
215
|
+
return lines.join('\n');
|
|
216
|
+
}
|
|
163
217
|
for (const file of result.files) {
|
|
164
218
|
if (file.error === 'SOPS_NOT_INSTALLED') {
|
|
165
219
|
lines.push(pc.red('Error: SOPS is not installed'));
|
|
@@ -232,6 +286,9 @@ export async function checkCommand(options) {
|
|
|
232
286
|
console.log(formatTextOutput(result));
|
|
233
287
|
}
|
|
234
288
|
}
|
|
289
|
+
if (result.status === 'plaintext' && !options.warn) {
|
|
290
|
+
process.exit(4);
|
|
291
|
+
}
|
|
235
292
|
if (result.status === 'error') {
|
|
236
293
|
const hasSopsError = result.files.some(f => f.error === 'SOPS_NOT_INSTALLED');
|
|
237
294
|
const hasDecryptError = result.files.some(f => f.error === 'DECRYPT_FAILED');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;AA2InE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCrE"}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,9 +1,99 @@
|
|
|
1
|
-
import { existsSync, readdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { stringify as stringifyYaml } from 'yaml';
|
|
5
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';
|
|
6
8
|
import { DEFAULT_SOURCES } from '../types.js';
|
|
9
|
+
function getProjectFromPackageJson(root) {
|
|
10
|
+
const pkgPath = join(root, 'package.json');
|
|
11
|
+
if (!existsSync(pkgPath))
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
15
|
+
if (typeof pkg.repository === 'string') {
|
|
16
|
+
const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
17
|
+
if (match)
|
|
18
|
+
return match[1];
|
|
19
|
+
}
|
|
20
|
+
if (pkg.repository?.url) {
|
|
21
|
+
const match = pkg.repository.url.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
22
|
+
if (match)
|
|
23
|
+
return match[1];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
async function tryExistingLocalKey(project) {
|
|
32
|
+
if (!keyExists(project))
|
|
33
|
+
return null;
|
|
34
|
+
const existing = await import('../lib/age.js').then(m => m.keyLoad(project));
|
|
35
|
+
if (!existing)
|
|
36
|
+
return null;
|
|
37
|
+
console.log(pc.green(`Using existing key for ${pc.cyan(project)}`));
|
|
38
|
+
return { publicKey: existing.public, source: 'existing' };
|
|
39
|
+
}
|
|
40
|
+
async function tryPullFrom1Password(project) {
|
|
41
|
+
if (!opAvailable())
|
|
42
|
+
return null;
|
|
43
|
+
console.log(pc.dim('Checking 1Password for existing key...'));
|
|
44
|
+
const priv = opGetKey(project);
|
|
45
|
+
if (!priv)
|
|
46
|
+
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)}`));
|
|
51
|
+
return { publicKey: pub, source: '1password' };
|
|
52
|
+
}
|
|
53
|
+
function generateAndBackupKey(project) {
|
|
54
|
+
if (!ageAvailable()) {
|
|
55
|
+
console.log(pc.yellow('age not installed. Run: brew install age'));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
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()) {
|
|
64
|
+
try {
|
|
65
|
+
opStoreKey(project, key.private, key.public);
|
|
66
|
+
console.log(pc.green('Backed up to 1Password.'));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
console.warn(pc.yellow(`Could not backup to 1Password: ${e.message}`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { publicKey: key.public, source: 'generated' };
|
|
73
|
+
}
|
|
74
|
+
async function setupKey(root, project) {
|
|
75
|
+
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'));
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return ((await tryExistingLocalKey(project)) ||
|
|
81
|
+
(await tryPullFrom1Password(project)) ||
|
|
82
|
+
generateAndBackupKey(project));
|
|
83
|
+
}
|
|
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}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const sopsConfig = stringifyYaml({
|
|
92
|
+
creation_rules: [{ encrypted_regex: '.*', age: publicKey }]
|
|
93
|
+
});
|
|
94
|
+
writeFileSync(sopsPath, sopsConfig, 'utf-8');
|
|
95
|
+
console.log(pc.green('Created .sops.yaml'));
|
|
96
|
+
}
|
|
7
97
|
function detectTargets(root) {
|
|
8
98
|
const targets = [{ name: 'root', path: '.', format: 'dotenv' }];
|
|
9
99
|
const entries = readdirSync(root, { withFileTypes: true });
|
|
@@ -43,10 +133,16 @@ export async function initCommand(options) {
|
|
|
43
133
|
process.exit(1);
|
|
44
134
|
}
|
|
45
135
|
console.log(pc.blue('Initializing hush...'));
|
|
136
|
+
const project = getProjectFromPackageJson(root);
|
|
137
|
+
const keyResult = await setupKey(root, project);
|
|
138
|
+
if (keyResult) {
|
|
139
|
+
createSopsConfig(root, keyResult.publicKey);
|
|
140
|
+
}
|
|
46
141
|
const targets = detectTargets(root);
|
|
47
142
|
const config = {
|
|
48
143
|
sources: DEFAULT_SOURCES,
|
|
49
144
|
targets,
|
|
145
|
+
...(project && { project }),
|
|
50
146
|
};
|
|
51
147
|
const yaml = stringifyYaml(config, { indent: 2 });
|
|
52
148
|
const configPath = join(root, 'hush.yaml');
|
|
@@ -57,7 +153,6 @@ export async function initCommand(options) {
|
|
|
57
153
|
console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
|
|
58
154
|
}
|
|
59
155
|
console.log(pc.dim('\nNext steps:'));
|
|
60
|
-
console.log(' 1.
|
|
61
|
-
console.log(' 2. Run "hush
|
|
62
|
-
console.log(' 3. Run "hush decrypt" to generate local env files');
|
|
156
|
+
console.log(' 1. Run "hush set <KEY>" to add secrets');
|
|
157
|
+
console.log(' 2. Run "hush run -- <command>" to run with secrets in memory');
|
|
63
158
|
}
|
|
@@ -0,0 +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"}
|