@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.
- package/dist/cli.js +58 -29
- package/dist/commands/check.d.ts +3 -3
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +30 -33
- package/dist/commands/decrypt.d.ts +2 -2
- package/dist/commands/decrypt.d.ts.map +1 -1
- package/dist/commands/decrypt.js +52 -55
- package/dist/commands/edit.d.ts +2 -2
- package/dist/commands/edit.d.ts.map +1 -1
- package/dist/commands/edit.js +10 -12
- package/dist/commands/encrypt.d.ts +2 -2
- package/dist/commands/encrypt.d.ts.map +1 -1
- package/dist/commands/encrypt.js +27 -29
- package/dist/commands/expansions.d.ts +2 -2
- package/dist/commands/expansions.d.ts.map +1 -1
- package/dist/commands/expansions.js +46 -44
- package/dist/commands/has.d.ts +2 -2
- package/dist/commands/has.d.ts.map +1 -1
- package/dist/commands/has.js +12 -15
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +107 -87
- package/dist/commands/inspect.d.ts +2 -2
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +14 -16
- package/dist/commands/keys.d.ts +2 -1
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +47 -49
- package/dist/commands/list.d.ts +2 -2
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +11 -14
- package/dist/commands/migrate.d.ts +7 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +117 -0
- package/dist/commands/push.d.ts +2 -2
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +41 -45
- package/dist/commands/resolve.d.ts +2 -2
- package/dist/commands/resolve.d.ts.map +1 -1
- package/dist/commands/resolve.js +25 -28
- package/dist/commands/run.d.ts +2 -2
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +35 -39
- package/dist/commands/set.d.ts +2 -2
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +61 -70
- package/dist/commands/skill.d.ts +2 -2
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +186 -487
- package/dist/commands/status.d.ts +2 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +52 -55
- package/dist/commands/template.d.ts +2 -2
- package/dist/commands/template.d.ts.map +1 -1
- package/dist/commands/template.js +36 -39
- package/dist/commands/trace.d.ts +2 -2
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +16 -19
- package/dist/config/loader.js +3 -3
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +59 -0
- package/dist/core/parse.js +3 -3
- package/dist/core/sops.js +9 -9
- package/dist/core/template.d.ts +2 -2
- package/dist/core/template.d.ts.map +1 -1
- package/dist/core/template.js +11 -12
- package/dist/lib/age.js +9 -9
- package/dist/lib/fs.d.ts +25 -0
- package/dist/lib/fs.d.ts.map +1 -0
- package/dist/lib/fs.js +36 -0
- package/dist/lib/onepassword.d.ts.map +1 -1
- package/dist/lib/onepassword.js +41 -4
- package/dist/types.d.ts +91 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -4
- package/dist/utils/version-check.js +5 -5
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
+
import { defaultContext } from './context.js';
|
|
4
5
|
import { encryptCommand } from './commands/encrypt.js';
|
|
5
6
|
import { decryptCommand } from './commands/decrypt.js';
|
|
6
7
|
import { editCommand } from './commands/edit.js';
|
|
@@ -19,6 +20,7 @@ import { resolveCommand } from './commands/resolve.js';
|
|
|
19
20
|
import { traceCommand } from './commands/trace.js';
|
|
20
21
|
import { templateCommand } from './commands/template.js';
|
|
21
22
|
import { expansionsCommand } from './commands/expansions.js';
|
|
23
|
+
import { migrateCommand } from './commands/migrate.js';
|
|
22
24
|
import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
|
|
23
25
|
import { checkForUpdate } from './utils/version-check.js';
|
|
24
26
|
const require = createRequire(import.meta.url);
|
|
@@ -32,9 +34,9 @@ ${pc.bold('Usage:')}
|
|
|
32
34
|
|
|
33
35
|
${pc.bold('Commands:')}
|
|
34
36
|
init Initialize hush.yaml config
|
|
35
|
-
encrypt Encrypt source .
|
|
37
|
+
encrypt Encrypt source .hush files
|
|
36
38
|
run -- <cmd> Run command with secrets in memory (AI-safe)
|
|
37
|
-
set <KEY>
|
|
39
|
+
set [VALUE] <KEY> Set a single secret (AI-safe, prompts if no value)
|
|
38
40
|
edit [file] Edit all secrets in $EDITOR
|
|
39
41
|
list List all variables (shows values)
|
|
40
42
|
inspect List all variables (masked values, AI-safe)
|
|
@@ -44,6 +46,7 @@ ${pc.bold('Commands:')}
|
|
|
44
46
|
status Show configuration and status
|
|
45
47
|
skill Install Claude Code / OpenCode skill
|
|
46
48
|
keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
|
|
49
|
+
migrate Migrate from v4 (.env.encrypted) to v5 (.hush.encrypted)
|
|
47
50
|
|
|
48
51
|
${pc.bold('Debugging Commands:')}
|
|
49
52
|
resolve <target> Show what variables a target receives (AI-safe)
|
|
@@ -72,7 +75,7 @@ ${pc.bold('Options:')}
|
|
|
72
75
|
-h, --help Show this help message
|
|
73
76
|
-v, --version Show version number
|
|
74
77
|
|
|
75
|
-
${pc.bold('Variable Expansion (
|
|
78
|
+
${pc.bold('Variable Expansion (v5+):')}
|
|
76
79
|
Subdirectory .env files can reference root secrets:
|
|
77
80
|
|
|
78
81
|
\${VAR} Pull VAR from root secrets
|
|
@@ -90,15 +93,29 @@ ${pc.bold('Variable Expansion (v4+):')}
|
|
|
90
93
|
|
|
91
94
|
Subdirectory templates are safe to commit - they contain no secrets.
|
|
92
95
|
|
|
96
|
+
${pc.bold('File Naming (v5+):')}
|
|
97
|
+
Hush uses .hush files instead of .env to avoid conflicts with other tools:
|
|
98
|
+
|
|
99
|
+
.hush Shared secrets (source file)
|
|
100
|
+
.hush.development Development secrets (source file)
|
|
101
|
+
.hush.encrypted Encrypted shared secrets (committed)
|
|
102
|
+
.hush.development.encrypted Encrypted dev secrets (committed)
|
|
103
|
+
|
|
104
|
+
Subdirectories support templates (e.g. apps/web/.hush.development)
|
|
105
|
+
|
|
106
|
+
The .env files are reserved for other tools (Wrangler, Metro, etc.).
|
|
107
|
+
|
|
93
108
|
${pc.bold('Examples:')}
|
|
94
109
|
hush init Initialize config + generate keys
|
|
95
|
-
hush
|
|
110
|
+
hush migrate Migrate v4 .env.encrypted to v5 .hush.encrypted
|
|
111
|
+
hush encrypt Encrypt .hush files
|
|
96
112
|
hush run -- npm start Run with secrets in memory (AI-safe!)
|
|
97
113
|
hush run -e prod -- npm build Run with production secrets
|
|
98
114
|
hush run -t api -- wrangler dev Run filtered for 'api' target (root secrets only)
|
|
99
115
|
cd apps/mobile && hush run -- expo start Run from subdirectory (applies template + target filters)
|
|
100
|
-
hush set DATABASE_URL Set a secret interactively (
|
|
101
|
-
hush set API_KEY
|
|
116
|
+
hush set DATABASE_URL Set a secret interactively (prompts for value)
|
|
117
|
+
hush set "myvalue" API_KEY Set a secret inline (no prompt)
|
|
118
|
+
hush set API_KEY --gui Set secret via GUI dialog (for AI agents)
|
|
102
119
|
hush set API_KEY -e prod Set a production secret
|
|
103
120
|
hush keys setup Pull key from 1Password or verify local
|
|
104
121
|
hush keys generate Generate new key + backup to 1Password
|
|
@@ -152,6 +169,7 @@ function parseArgs(args) {
|
|
|
152
169
|
let vault;
|
|
153
170
|
let file;
|
|
154
171
|
let key;
|
|
172
|
+
let value;
|
|
155
173
|
let target;
|
|
156
174
|
let cmdArgs = [];
|
|
157
175
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -258,8 +276,16 @@ function parseArgs(args) {
|
|
|
258
276
|
}
|
|
259
277
|
continue;
|
|
260
278
|
}
|
|
261
|
-
if (command === 'set' && !arg.startsWith('-')
|
|
262
|
-
key
|
|
279
|
+
if (command === 'set' && !arg.startsWith('-')) {
|
|
280
|
+
if (!key) {
|
|
281
|
+
key = arg;
|
|
282
|
+
}
|
|
283
|
+
else if (!value) {
|
|
284
|
+
// Second positional arg: shift key to value, this arg is the key
|
|
285
|
+
// Syntax: hush set <VALUE> <KEY>
|
|
286
|
+
value = key;
|
|
287
|
+
key = arg;
|
|
288
|
+
}
|
|
263
289
|
continue;
|
|
264
290
|
}
|
|
265
291
|
if (command === 'has' && !arg.startsWith('-') && !key) {
|
|
@@ -279,10 +305,10 @@ function parseArgs(args) {
|
|
|
279
305
|
continue;
|
|
280
306
|
}
|
|
281
307
|
}
|
|
282
|
-
return { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs };
|
|
308
|
+
return { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, value, target, cmdArgs };
|
|
283
309
|
}
|
|
284
310
|
function checkMigrationNeeded(root, command) {
|
|
285
|
-
const skipCommands = ['', 'help', 'version', 'init', 'skill'];
|
|
311
|
+
const skipCommands = ['', 'help', 'version', 'init', 'skill', 'migrate'];
|
|
286
312
|
if (skipCommands.includes(command))
|
|
287
313
|
return;
|
|
288
314
|
const configPath = findConfigPath(root);
|
|
@@ -315,7 +341,7 @@ async function main() {
|
|
|
315
341
|
printHelp();
|
|
316
342
|
process.exit(0);
|
|
317
343
|
}
|
|
318
|
-
const { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs } = parseArgs(args);
|
|
344
|
+
const { command, subcommand, env, envExplicit, root, dryRun, verbose, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, value, target, cmdArgs } = parseArgs(args);
|
|
319
345
|
if (command !== 'run' && !json && !quiet) {
|
|
320
346
|
checkForUpdate(VERSION);
|
|
321
347
|
}
|
|
@@ -323,16 +349,16 @@ async function main() {
|
|
|
323
349
|
try {
|
|
324
350
|
switch (command) {
|
|
325
351
|
case 'init':
|
|
326
|
-
await initCommand({ root });
|
|
352
|
+
await initCommand(defaultContext, { root });
|
|
327
353
|
break;
|
|
328
354
|
case 'encrypt':
|
|
329
|
-
await encryptCommand({ root });
|
|
355
|
+
await encryptCommand(defaultContext, { root });
|
|
330
356
|
break;
|
|
331
357
|
case 'decrypt':
|
|
332
|
-
await decryptCommand({ root, env, force });
|
|
358
|
+
await decryptCommand(defaultContext, { root, env, force });
|
|
333
359
|
break;
|
|
334
360
|
case 'run':
|
|
335
|
-
await runCommand({ root, env, target, command: cmdArgs });
|
|
361
|
+
await runCommand(defaultContext, { root, env, target, command: cmdArgs });
|
|
336
362
|
break;
|
|
337
363
|
case 'set': {
|
|
338
364
|
let setFile = 'shared';
|
|
@@ -342,36 +368,36 @@ async function main() {
|
|
|
342
368
|
else if (envExplicit) {
|
|
343
369
|
setFile = env;
|
|
344
370
|
}
|
|
345
|
-
await setCommand({ root, file: setFile, key, gui });
|
|
371
|
+
await setCommand(defaultContext, { root, file: setFile, key, value, gui });
|
|
346
372
|
break;
|
|
347
373
|
}
|
|
348
374
|
case 'edit':
|
|
349
|
-
await editCommand({ root, file });
|
|
375
|
+
await editCommand(defaultContext, { root, file });
|
|
350
376
|
break;
|
|
351
377
|
case 'list':
|
|
352
|
-
await listCommand({ root, env });
|
|
378
|
+
await listCommand(defaultContext, { root, env });
|
|
353
379
|
break;
|
|
354
380
|
case 'inspect':
|
|
355
|
-
await inspectCommand({ root, env });
|
|
381
|
+
await inspectCommand(defaultContext, { root, env });
|
|
356
382
|
break;
|
|
357
383
|
case 'has':
|
|
358
384
|
if (!key) {
|
|
359
385
|
console.error(pc.red('Usage: hush has <KEY>'));
|
|
360
386
|
process.exit(1);
|
|
361
387
|
}
|
|
362
|
-
await hasCommand({ root, env, key, quiet });
|
|
388
|
+
await hasCommand(defaultContext, { root, env, key, quiet });
|
|
363
389
|
break;
|
|
364
390
|
case 'check':
|
|
365
|
-
await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
|
|
391
|
+
await checkCommand(defaultContext, { root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
|
|
366
392
|
break;
|
|
367
393
|
case 'push':
|
|
368
|
-
await pushCommand({ root, dryRun, verbose, target });
|
|
394
|
+
await pushCommand(defaultContext, { root, dryRun, verbose, target });
|
|
369
395
|
break;
|
|
370
396
|
case 'status':
|
|
371
|
-
await statusCommand({ root });
|
|
397
|
+
await statusCommand(defaultContext, { root });
|
|
372
398
|
break;
|
|
373
399
|
case 'skill':
|
|
374
|
-
await skillCommand({ root, global, local });
|
|
400
|
+
await skillCommand(defaultContext, { root, global, local });
|
|
375
401
|
break;
|
|
376
402
|
case 'keys':
|
|
377
403
|
if (!subcommand) {
|
|
@@ -379,7 +405,7 @@ async function main() {
|
|
|
379
405
|
console.error(pc.dim('Commands: setup, generate, pull, push, list'));
|
|
380
406
|
process.exit(1);
|
|
381
407
|
}
|
|
382
|
-
await keysCommand({ root, subcommand, vault, force });
|
|
408
|
+
await keysCommand(defaultContext, { root, subcommand, vault, force });
|
|
383
409
|
break;
|
|
384
410
|
case 'resolve':
|
|
385
411
|
if (!target) {
|
|
@@ -387,7 +413,7 @@ async function main() {
|
|
|
387
413
|
console.error(pc.dim('Example: hush resolve api-workers'));
|
|
388
414
|
process.exit(1);
|
|
389
415
|
}
|
|
390
|
-
await resolveCommand({ root, env, target });
|
|
416
|
+
await resolveCommand(defaultContext, { root, env, target });
|
|
391
417
|
break;
|
|
392
418
|
case 'trace':
|
|
393
419
|
if (!key) {
|
|
@@ -395,13 +421,16 @@ async function main() {
|
|
|
395
421
|
console.error(pc.dim('Example: hush trace DATABASE_URL'));
|
|
396
422
|
process.exit(1);
|
|
397
423
|
}
|
|
398
|
-
await traceCommand({ root, env, key });
|
|
424
|
+
await traceCommand(defaultContext, { root, env, key });
|
|
399
425
|
break;
|
|
400
426
|
case 'template':
|
|
401
|
-
await templateCommand({ root, env });
|
|
427
|
+
await templateCommand(defaultContext, { root, env });
|
|
402
428
|
break;
|
|
403
429
|
case 'expansions':
|
|
404
|
-
await expansionsCommand({ root, env });
|
|
430
|
+
await expansionsCommand(defaultContext, { root, env });
|
|
431
|
+
break;
|
|
432
|
+
case 'migrate':
|
|
433
|
+
await migrateCommand(defaultContext, { root, dryRun });
|
|
405
434
|
break;
|
|
406
435
|
default:
|
|
407
436
|
if (command) {
|
package/dist/commands/check.d.ts
CHANGED
|
@@ -1,4 +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>;
|
|
1
|
+
import type { CheckOptions, CheckResult, HushContext } from '../types.js';
|
|
2
|
+
export declare function check(ctx: HushContext, options: CheckOptions): Promise<CheckResult>;
|
|
3
|
+
export declare function checkCommand(ctx: HushContext, options: CheckOptions): Promise<void>;
|
|
4
4
|
//# sourceMappingURL=check.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,WAAW,EAAE,MAAM,aAAa,CAAC;AAmF5H,wBAAsB,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BzF;AAwMD,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCzF"}
|
package/dist/commands/check.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
|
-
import { execSync } from 'node:child_process';
|
|
4
2
|
import pc from 'picocolors';
|
|
5
|
-
import { loadConfig } from '../config/loader.js';
|
|
6
3
|
import { parseEnvContent } from '../core/parse.js';
|
|
7
|
-
import { decrypt as sopsDecrypt, isSopsInstalled } from '../core/sops.js';
|
|
8
4
|
import { computeDiff, isInSync } from '../lib/diff.js';
|
|
9
5
|
function getSourceEncryptedPairs(config) {
|
|
10
6
|
const pairs = [];
|
|
@@ -31,10 +27,10 @@ function getSourceEncryptedPairs(config) {
|
|
|
31
27
|
}
|
|
32
28
|
return pairs;
|
|
33
29
|
}
|
|
34
|
-
function getGitChangedFiles(root) {
|
|
30
|
+
function getGitChangedFiles(ctx, root) {
|
|
35
31
|
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' });
|
|
32
|
+
const staged = ctx.exec.execSync('git diff --cached --name-only', { cwd: root, encoding: 'utf-8' });
|
|
33
|
+
const unstaged = ctx.exec.execSync('git diff --name-only', { cwd: root, encoding: 'utf-8' });
|
|
38
34
|
const files = [...staged.split('\n'), ...unstaged.split('\n')].filter(Boolean);
|
|
39
35
|
return new Set(files);
|
|
40
36
|
}
|
|
@@ -42,14 +38,15 @@ function getGitChangedFiles(root) {
|
|
|
42
38
|
return new Set();
|
|
43
39
|
}
|
|
44
40
|
}
|
|
45
|
-
function findPlaintextEnvFiles(root) {
|
|
41
|
+
function findPlaintextEnvFiles(ctx, root) {
|
|
46
42
|
const results = [];
|
|
43
|
+
// Only warn about .env files (legacy/output files), NOT .hush files (Hush's source files)
|
|
47
44
|
const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
48
45
|
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
|
|
49
46
|
function scanDir(dir, relativePath = '') {
|
|
50
47
|
let entries;
|
|
51
48
|
try {
|
|
52
|
-
entries = readdirSync(dir);
|
|
49
|
+
entries = ctx.fs.readdirSync(dir);
|
|
53
50
|
}
|
|
54
51
|
catch {
|
|
55
52
|
return;
|
|
@@ -60,7 +57,7 @@ function findPlaintextEnvFiles(root) {
|
|
|
60
57
|
const fullPath = join(dir, entry);
|
|
61
58
|
const relPath = relativePath ? `${relativePath}/${entry}` : entry;
|
|
62
59
|
try {
|
|
63
|
-
if (statSync(fullPath).isDirectory()) {
|
|
60
|
+
if (ctx.fs.statSync(fullPath).isDirectory()) {
|
|
64
61
|
scanDir(fullPath, relPath);
|
|
65
62
|
}
|
|
66
63
|
else if (plaintextPatterns.includes(entry)) {
|
|
@@ -75,9 +72,9 @@ function findPlaintextEnvFiles(root) {
|
|
|
75
72
|
scanDir(root);
|
|
76
73
|
return results;
|
|
77
74
|
}
|
|
78
|
-
export async function check(options) {
|
|
75
|
+
export async function check(ctx, options) {
|
|
79
76
|
const { root, requireSource, onlyChanged, allowPlaintext } = options;
|
|
80
|
-
if (!isSopsInstalled()) {
|
|
77
|
+
if (!ctx.sops.isSopsInstalled()) {
|
|
81
78
|
return {
|
|
82
79
|
status: 'error',
|
|
83
80
|
files: [{
|
|
@@ -91,11 +88,11 @@ export async function check(options) {
|
|
|
91
88
|
}],
|
|
92
89
|
};
|
|
93
90
|
}
|
|
94
|
-
const config = loadConfig(root);
|
|
91
|
+
const config = ctx.config.loadConfig(root);
|
|
95
92
|
const pairs = getSourceEncryptedPairs(config);
|
|
96
|
-
const result = checkPairs(root, pairs, requireSource, onlyChanged);
|
|
93
|
+
const result = checkPairs(ctx, root, pairs, requireSource, onlyChanged);
|
|
97
94
|
if (!allowPlaintext) {
|
|
98
|
-
const plaintextFiles = findPlaintextEnvFiles(root);
|
|
95
|
+
const plaintextFiles = findPlaintextEnvFiles(ctx, root);
|
|
99
96
|
if (plaintextFiles.length > 0) {
|
|
100
97
|
result.plaintextFiles = plaintextFiles;
|
|
101
98
|
result.status = 'plaintext';
|
|
@@ -103,8 +100,8 @@ export async function check(options) {
|
|
|
103
100
|
}
|
|
104
101
|
return result;
|
|
105
102
|
}
|
|
106
|
-
function checkPairs(root, pairs, requireSource, onlyChanged) {
|
|
107
|
-
const changedFiles = onlyChanged ? getGitChangedFiles(root) : null;
|
|
103
|
+
function checkPairs(ctx, root, pairs, requireSource, onlyChanged) {
|
|
104
|
+
const changedFiles = onlyChanged ? getGitChangedFiles(ctx, root) : null;
|
|
108
105
|
const results = [];
|
|
109
106
|
for (const { source, encrypted } of pairs) {
|
|
110
107
|
const sourcePath = join(root, source);
|
|
@@ -116,7 +113,7 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
|
|
|
116
113
|
continue;
|
|
117
114
|
}
|
|
118
115
|
}
|
|
119
|
-
if (!existsSync(sourcePath)) {
|
|
116
|
+
if (!ctx.fs.existsSync(sourcePath)) {
|
|
120
117
|
if (requireSource) {
|
|
121
118
|
results.push({
|
|
122
119
|
source,
|
|
@@ -130,8 +127,8 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
|
|
|
130
127
|
}
|
|
131
128
|
continue;
|
|
132
129
|
}
|
|
133
|
-
if (!existsSync(encryptedPath)) {
|
|
134
|
-
const sourceContent = readFileSync(sourcePath, 'utf-8');
|
|
130
|
+
if (!ctx.fs.existsSync(encryptedPath)) {
|
|
131
|
+
const sourceContent = ctx.fs.readFileSync(sourcePath, 'utf-8');
|
|
135
132
|
const sourceVars = parseEnvContent(sourceContent);
|
|
136
133
|
const allKeys = sourceVars.map(v => v.key);
|
|
137
134
|
results.push({
|
|
@@ -146,8 +143,8 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
|
|
|
146
143
|
continue;
|
|
147
144
|
}
|
|
148
145
|
try {
|
|
149
|
-
const decryptedContent =
|
|
150
|
-
const sourceContent = readFileSync(sourcePath, 'utf-8');
|
|
146
|
+
const decryptedContent = ctx.sops.decrypt(encryptedPath);
|
|
147
|
+
const sourceContent = ctx.fs.readFileSync(sourcePath, 'utf-8');
|
|
151
148
|
const sourceVars = parseEnvContent(sourceContent);
|
|
152
149
|
const encryptedVars = parseEnvContent(decryptedContent);
|
|
153
150
|
const diff = computeDiff(sourceVars, encryptedVars);
|
|
@@ -206,8 +203,8 @@ function formatTextOutput(result) {
|
|
|
206
203
|
lines.push(pc.yellow('These files contain plaintext secrets that could be exposed to AI assistants.'));
|
|
207
204
|
lines.push('');
|
|
208
205
|
lines.push(pc.bold('To fix:'));
|
|
209
|
-
lines.push(pc.dim(' 1. Run: hush
|
|
210
|
-
lines.push(pc.dim(' 2. Delete
|
|
206
|
+
lines.push(pc.dim(' 1. Run: hush migrate (if upgrading from v4)'));
|
|
207
|
+
lines.push(pc.dim(' 2. Delete or gitignore these .env files'));
|
|
211
208
|
lines.push(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars'));
|
|
212
209
|
lines.push('');
|
|
213
210
|
lines.push(pc.dim('To allow plaintext files (not recommended): --allow-plaintext'));
|
|
@@ -276,31 +273,31 @@ function formatTextOutput(result) {
|
|
|
276
273
|
function formatJsonOutput(result) {
|
|
277
274
|
return JSON.stringify(result, null, 2);
|
|
278
275
|
}
|
|
279
|
-
export async function checkCommand(options) {
|
|
280
|
-
const result = await check(options);
|
|
276
|
+
export async function checkCommand(ctx, options) {
|
|
277
|
+
const result = await check(ctx, options);
|
|
281
278
|
if (!options.quiet) {
|
|
282
279
|
if (options.json) {
|
|
283
|
-
|
|
280
|
+
ctx.logger.log(formatJsonOutput(result));
|
|
284
281
|
}
|
|
285
282
|
else {
|
|
286
|
-
|
|
283
|
+
ctx.logger.log(formatTextOutput(result));
|
|
287
284
|
}
|
|
288
285
|
}
|
|
289
286
|
if (result.status === 'plaintext' && !options.warn) {
|
|
290
|
-
process.exit(4);
|
|
287
|
+
ctx.process.exit(4);
|
|
291
288
|
}
|
|
292
289
|
if (result.status === 'error') {
|
|
293
290
|
const hasSopsError = result.files.some(f => f.error === 'SOPS_NOT_INSTALLED');
|
|
294
291
|
const hasDecryptError = result.files.some(f => f.error === 'DECRYPT_FAILED');
|
|
295
292
|
if (hasSopsError || hasDecryptError) {
|
|
296
|
-
process.exit(3);
|
|
293
|
+
ctx.process.exit(3);
|
|
297
294
|
}
|
|
298
295
|
if (result.files.some(f => f.error === 'SOURCE_MISSING')) {
|
|
299
|
-
process.exit(2);
|
|
296
|
+
ctx.process.exit(2);
|
|
300
297
|
}
|
|
301
298
|
}
|
|
302
299
|
if (result.status === 'drift' && !options.warn) {
|
|
303
|
-
process.exit(1);
|
|
300
|
+
ctx.process.exit(1);
|
|
304
301
|
}
|
|
305
|
-
process.exit(0);
|
|
302
|
+
ctx.process.exit(0);
|
|
306
303
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { DecryptOptions } from '../types.js';
|
|
2
|
-
export declare function decryptCommand(options: DecryptOptions): Promise<void>;
|
|
1
|
+
import type { DecryptOptions, HushContext } from '../types.js';
|
|
2
|
+
export declare function decryptCommand(ctx: HushContext, options: DecryptOptions): Promise<void>;
|
|
3
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":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;AAmDvE,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA8F7F"}
|
package/dist/commands/decrypt.js
CHANGED
|
@@ -1,60 +1,57 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import pc from 'picocolors';
|
|
5
|
-
import { loadConfig } from '../config/loader.js';
|
|
6
4
|
import { filterVarsForTarget } from '../core/filter.js';
|
|
7
5
|
import { interpolateVars, getUnresolvedVars } from '../core/interpolate.js';
|
|
8
6
|
import { mergeVars } from '../core/merge.js';
|
|
9
7
|
import { parseEnvContent, parseEnvFile } from '../core/parse.js';
|
|
10
|
-
import { decrypt as sopsDecrypt } from '../core/sops.js';
|
|
11
8
|
import { formatVars } from '../formats/index.js';
|
|
12
9
|
import { FORMAT_OUTPUT_FILES } from '../types.js';
|
|
13
10
|
function getEncryptedPath(sourcePath) {
|
|
14
11
|
return sourcePath + '.encrypted';
|
|
15
12
|
}
|
|
16
|
-
async function confirmDangerousOperation() {
|
|
17
|
-
if (!process.stdin.isTTY) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
async function confirmDangerousOperation(ctx) {
|
|
14
|
+
if (!ctx.process.stdin.isTTY) {
|
|
15
|
+
ctx.logger.error('\nError: decrypt --force requires interactive confirmation.');
|
|
16
|
+
ctx.logger.error('This command cannot be run in non-interactive environments.');
|
|
17
|
+
ctx.logger.error('\nUse "hush run -- <command>" instead to inject secrets into memory.');
|
|
21
18
|
return false;
|
|
22
19
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
20
|
+
ctx.logger.log('');
|
|
21
|
+
ctx.logger.log(pc.red('━'.repeat(70)));
|
|
22
|
+
ctx.logger.log(pc.red(pc.bold(' ⚠️ WARNING: WRITING PLAINTEXT SECRETS TO DISK')));
|
|
23
|
+
ctx.logger.log(pc.red('━'.repeat(70)));
|
|
24
|
+
ctx.logger.log('');
|
|
25
|
+
ctx.logger.log(pc.yellow(' This will create unencrypted .env files that:'));
|
|
26
|
+
ctx.logger.log(pc.dim(' • Can be read by AI assistants, scripts, and other tools'));
|
|
27
|
+
ctx.logger.log(pc.dim(' • May accidentally be committed to git'));
|
|
28
|
+
ctx.logger.log(pc.dim(' • Defeat the "encrypted at rest" security model'));
|
|
29
|
+
ctx.logger.log('');
|
|
30
|
+
ctx.logger.log(pc.green(' Recommended alternative:'));
|
|
31
|
+
ctx.logger.log(pc.cyan(' hush run -- <command>'));
|
|
32
|
+
ctx.logger.log(pc.dim(' Decrypts to memory only, secrets never touch disk.'));
|
|
33
|
+
ctx.logger.log('');
|
|
34
|
+
ctx.logger.log(pc.red('━'.repeat(70)));
|
|
35
|
+
ctx.logger.log('');
|
|
39
36
|
const rl = createInterface({
|
|
40
|
-
input: process.stdin,
|
|
41
|
-
output: process.stdout,
|
|
37
|
+
input: ctx.process.stdin,
|
|
38
|
+
output: ctx.process.stdout,
|
|
42
39
|
});
|
|
43
40
|
return new Promise((resolve) => {
|
|
44
41
|
rl.question(`${pc.bold('Type "yes" to proceed:')} `, (answer) => {
|
|
45
42
|
rl.close();
|
|
46
43
|
if (answer.toLowerCase() === 'yes') {
|
|
47
|
-
|
|
44
|
+
ctx.logger.log('');
|
|
48
45
|
resolve(true);
|
|
49
46
|
}
|
|
50
47
|
else {
|
|
51
|
-
|
|
48
|
+
ctx.logger.log(pc.dim('\nAborted. No files were written.'));
|
|
52
49
|
resolve(false);
|
|
53
50
|
}
|
|
54
51
|
});
|
|
55
52
|
});
|
|
56
53
|
}
|
|
57
|
-
export async function decryptCommand(options) {
|
|
54
|
+
export async function decryptCommand(ctx, options) {
|
|
58
55
|
const { root, env, force } = options;
|
|
59
56
|
if (!force) {
|
|
60
57
|
console.error(pc.red('Error: decrypt requires --force flag'));
|
|
@@ -66,64 +63,64 @@ export async function decryptCommand(options) {
|
|
|
66
63
|
console.error(pc.cyan(' hush decrypt --force'));
|
|
67
64
|
process.exit(1);
|
|
68
65
|
}
|
|
69
|
-
const confirmed = await confirmDangerousOperation();
|
|
66
|
+
const confirmed = await confirmDangerousOperation(ctx);
|
|
70
67
|
if (!confirmed) {
|
|
71
|
-
process.exit(0);
|
|
68
|
+
ctx.process.exit(0);
|
|
72
69
|
}
|
|
73
|
-
const config = loadConfig(root);
|
|
74
|
-
|
|
70
|
+
const config = ctx.config.loadConfig(root);
|
|
71
|
+
ctx.logger.log(pc.yellow(`⚠️ Writing unencrypted secrets for ${env}...`));
|
|
75
72
|
const sharedEncrypted = join(root, getEncryptedPath(config.sources.shared));
|
|
76
73
|
const envEncrypted = join(root, getEncryptedPath(config.sources[env]));
|
|
77
|
-
const localPath = join(root,
|
|
74
|
+
const localPath = join(root, config.sources.local);
|
|
78
75
|
const varSources = [];
|
|
79
|
-
if (existsSync(sharedEncrypted)) {
|
|
80
|
-
const content =
|
|
76
|
+
if (ctx.fs.existsSync(sharedEncrypted)) {
|
|
77
|
+
const content = ctx.sops.decrypt(sharedEncrypted);
|
|
81
78
|
const vars = parseEnvContent(content);
|
|
82
79
|
varSources.push(vars);
|
|
83
|
-
|
|
80
|
+
ctx.logger.log(pc.dim(` ${config.sources.shared}.encrypted: ${vars.length} vars`));
|
|
84
81
|
}
|
|
85
|
-
if (existsSync(envEncrypted)) {
|
|
86
|
-
const content =
|
|
82
|
+
if (ctx.fs.existsSync(envEncrypted)) {
|
|
83
|
+
const content = ctx.sops.decrypt(envEncrypted);
|
|
87
84
|
const vars = parseEnvContent(content);
|
|
88
85
|
varSources.push(vars);
|
|
89
|
-
|
|
86
|
+
ctx.logger.log(pc.dim(` ${config.sources[env]}.encrypted: ${vars.length} vars`));
|
|
90
87
|
}
|
|
91
|
-
if (existsSync(localPath)) {
|
|
88
|
+
if (ctx.fs.existsSync(localPath)) {
|
|
92
89
|
const vars = parseEnvFile(localPath);
|
|
93
90
|
varSources.push(vars);
|
|
94
|
-
|
|
91
|
+
ctx.logger.log(pc.dim(` ${config.sources.local}: ${vars.length} vars (overrides)`));
|
|
95
92
|
}
|
|
96
93
|
if (varSources.length === 0) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
process.exit(1);
|
|
94
|
+
ctx.logger.error(pc.red('No encrypted files found'));
|
|
95
|
+
ctx.logger.error(pc.dim(`Expected: ${sharedEncrypted}`));
|
|
96
|
+
ctx.process.exit(1);
|
|
100
97
|
}
|
|
101
98
|
const merged = mergeVars(...varSources);
|
|
102
99
|
const interpolated = interpolateVars(merged);
|
|
103
100
|
const unresolved = getUnresolvedVars(interpolated);
|
|
104
101
|
if (unresolved.length > 0) {
|
|
105
|
-
|
|
102
|
+
ctx.logger.warn(pc.yellow(` Warning: ${unresolved.length} vars have unresolved references`));
|
|
106
103
|
}
|
|
107
|
-
|
|
104
|
+
ctx.logger.log(pc.yellow(`\n⚠️ Writing to ${config.targets.length} targets:`));
|
|
108
105
|
for (const target of config.targets) {
|
|
109
106
|
const targetDir = join(root, target.path);
|
|
110
107
|
const filtered = filterVarsForTarget(interpolated, target);
|
|
111
108
|
if (filtered.length === 0) {
|
|
112
|
-
|
|
109
|
+
ctx.logger.log(pc.dim(` ${target.path}/ - no matching vars, skipped`));
|
|
113
110
|
continue;
|
|
114
111
|
}
|
|
115
112
|
const outputFilename = FORMAT_OUTPUT_FILES[target.format][env];
|
|
116
113
|
const outputPath = join(targetDir, outputFilename);
|
|
117
|
-
if (!existsSync(targetDir)) {
|
|
118
|
-
mkdirSync(targetDir, { recursive: true });
|
|
114
|
+
if (!ctx.fs.existsSync(targetDir)) {
|
|
115
|
+
ctx.fs.mkdirSync(targetDir, { recursive: true });
|
|
119
116
|
}
|
|
120
117
|
const content = formatVars(filtered, target.format);
|
|
121
|
-
writeFileSync(outputPath, content, 'utf-8');
|
|
118
|
+
ctx.fs.writeFileSync(outputPath, content, 'utf-8');
|
|
122
119
|
const relativePath = target.path === '.' ? outputFilename : `${target.path}/${outputFilename}`;
|
|
123
|
-
|
|
120
|
+
ctx.logger.log(pc.yellow(` ⚠️ ${relativePath}`) +
|
|
124
121
|
pc.dim(` (${target.format}, ${filtered.length} vars)`));
|
|
125
122
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
ctx.logger.log('');
|
|
124
|
+
ctx.logger.log(pc.yellow('⚠️ Decryption complete - plaintext secrets on disk'));
|
|
125
|
+
ctx.logger.log(pc.dim(' Delete these files when done, or use "hush run" next time.'));
|
|
129
126
|
}
|
package/dist/commands/edit.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { EditOptions } from '../types.js';
|
|
2
|
-
export declare function editCommand(options: EditOptions): Promise<void>;
|
|
1
|
+
import type { EditOptions, HushContext } from '../types.js';
|
|
2
|
+
export declare function editCommand(ctx: HushContext, options: EditOptions): Promise<void>;
|
|
3
3
|
//# sourceMappingURL=edit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../src/commands/edit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../../src/commands/edit.ts"],"names":[],"mappings":"AAGC,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI7D,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBvF"}
|
package/dist/commands/edit.js
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
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 { edit as sopsEdit } from '../core/sops.js';
|
|
6
|
-
export async function editCommand(options) {
|
|
4
|
+
export async function editCommand(ctx, options) {
|
|
7
5
|
const { root, file } = options;
|
|
8
|
-
const config = loadConfig(root);
|
|
6
|
+
const config = ctx.config.loadConfig(root);
|
|
9
7
|
const fileKey = file ?? 'shared';
|
|
10
8
|
const sourcePath = config.sources[fileKey];
|
|
11
9
|
const encryptedPath = join(root, sourcePath + '.encrypted');
|
|
12
|
-
if (!existsSync(encryptedPath)) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
process.exit(1);
|
|
10
|
+
if (!ctx.fs.existsSync(encryptedPath)) {
|
|
11
|
+
ctx.logger.error(pc.red(`Encrypted file not found: ${sourcePath}.encrypted`));
|
|
12
|
+
ctx.logger.error(pc.dim('Run "hush encrypt" first to create encrypted files'));
|
|
13
|
+
ctx.process.exit(1);
|
|
16
14
|
}
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
ctx.logger.log(pc.blue(`Editing ${sourcePath}.encrypted...`));
|
|
16
|
+
ctx.logger.log(pc.dim('Changes will be encrypted on save'));
|
|
19
17
|
sopsEdit(encryptedPath);
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
ctx.logger.log(pc.green('\nEdit complete'));
|
|
19
|
+
ctx.logger.log(pc.dim('Run "hush run -- <command>" to use updated secrets'));
|
|
22
20
|
}
|