@chriscode/hush 2.0.0 → 2.2.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 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';
@@ -10,7 +12,8 @@ import { listCommand } from './commands/list.js';
10
12
  import { inspectCommand } from './commands/inspect.js';
11
13
  import { hasCommand } from './commands/has.js';
12
14
  import { checkCommand } from './commands/check.js';
13
- const VERSION = '2.0.0';
15
+ import { skillCommand } from './commands/skill.js';
16
+ const VERSION = '2.1.0';
14
17
  function printHelp() {
15
18
  console.log(`
16
19
  ${pc.bold('hush')} - SOPS-based secrets management for monorepos
@@ -21,43 +24,54 @@ ${pc.bold('Usage:')}
21
24
  ${pc.bold('Commands:')}
22
25
  init Initialize hush.yaml config
23
26
  encrypt Encrypt source .env files
24
- decrypt Decrypt and distribute to targets
25
- edit [file] Edit encrypted file in $EDITOR
27
+ run -- <cmd> Run command with secrets in memory (AI-safe)
28
+ set <KEY> Set a single secret interactively (AI-safe)
29
+ edit [file] Edit all secrets in $EDITOR
26
30
  list List all variables (shows values)
27
31
  inspect List all variables (masked values, AI-safe)
28
32
  has <key> Check if a secret exists (exit 0 if set, 1 if not)
29
33
  check Verify secrets are encrypted (for pre-commit hooks)
30
34
  push Push secrets to Cloudflare Workers
31
35
  status Show configuration and status
36
+ skill Install Claude Code / OpenCode skill
37
+
38
+ ${pc.bold('Deprecated Commands:')}
39
+ decrypt Write secrets to disk (unsafe - use 'run' instead)
32
40
 
33
41
  ${pc.bold('Options:')}
34
42
  -e, --env <env> Environment: development or production (default: development)
35
43
  -r, --root <dir> Root directory (default: current directory)
44
+ -t, --target <t> Target name from hush.yaml (run only)
36
45
  -q, --quiet Suppress output (has/check commands)
37
46
  --dry-run Preview changes without applying (push only)
38
47
  --warn Warn but exit 0 on drift (check only)
39
48
  --json Output machine-readable JSON (check only)
40
49
  --only-changed Only check git-modified files (check only)
41
50
  --require-source Fail if source file is missing (check only)
51
+ --global Install skill to ~/.claude/skills/ (skill only)
52
+ --local Install skill to ./.claude/skills/ (skill/set only)
42
53
  -h, --help Show this help message
43
54
  -v, --version Show version number
44
55
 
45
56
  ${pc.bold('Examples:')}
46
57
  hush init Initialize hush.yaml config
47
58
  hush encrypt Encrypt .env files
48
- hush decrypt Decrypt for development
49
- hush decrypt -e production Decrypt for production
50
- hush edit Edit shared secrets
51
- hush edit development Edit development secrets
52
- hush list List all variables (shows values)
59
+ hush run -- npm start Run with secrets in memory (AI-safe!)
60
+ hush run -e prod -- npm build Run with production secrets
61
+ hush run -t api -- wrangler dev Run filtered for 'api' target
62
+ hush set DATABASE_URL Set a secret interactively (AI-safe)
63
+ hush set API_KEY -e prod Set a production secret
64
+ hush set API_KEY --local Set a personal local override
65
+ hush edit Edit all shared secrets in $EDITOR
66
+ hush edit development Edit development secrets in $EDITOR
67
+ hush edit local Edit personal local overrides
53
68
  hush inspect List all variables (masked, AI-safe)
54
69
  hush has DATABASE_URL Check if DATABASE_URL is set
55
70
  hush has API_KEY -q && echo "API_KEY is configured"
56
71
  hush check Verify secrets are encrypted
57
- hush check --warn Check but don't fail on drift
58
- hush check --json Output JSON for CI
59
72
  hush push --dry-run Preview push to Cloudflare
60
73
  hush status Show current status
74
+ hush skill Install Claude skill (interactive)
61
75
  `);
62
76
  }
63
77
  function parseEnvironment(value) {
@@ -68,7 +82,7 @@ function parseEnvironment(value) {
68
82
  return null;
69
83
  }
70
84
  function parseFileKey(value) {
71
- if (value === 'shared' || value === 'development' || value === 'production')
85
+ if (value === 'shared' || value === 'development' || value === 'production' || value === 'local')
72
86
  return value;
73
87
  if (value === 'dev')
74
88
  return 'development';
@@ -79,6 +93,7 @@ function parseFileKey(value) {
79
93
  function parseArgs(args) {
80
94
  let command = '';
81
95
  let env = 'development';
96
+ let envExplicit = false;
82
97
  let root = process.cwd();
83
98
  let dryRun = false;
84
99
  let quiet = false;
@@ -86,8 +101,12 @@ function parseArgs(args) {
86
101
  let json = false;
87
102
  let onlyChanged = false;
88
103
  let requireSource = false;
104
+ let global = false;
105
+ let local = false;
89
106
  let file;
90
107
  let key;
108
+ let target;
109
+ let cmdArgs = [];
91
110
  for (let i = 0; i < args.length; i++) {
92
111
  const arg = args[i];
93
112
  if (arg === '-h' || arg === '--help') {
@@ -103,6 +122,7 @@ function parseArgs(args) {
103
122
  const parsed = parseEnvironment(nextArg);
104
123
  if (parsed) {
105
124
  env = parsed;
125
+ envExplicit = true;
106
126
  }
107
127
  else {
108
128
  console.error(pc.red(`Invalid environment: ${nextArg}`));
@@ -139,6 +159,22 @@ function parseArgs(args) {
139
159
  requireSource = true;
140
160
  continue;
141
161
  }
162
+ if (arg === '--global') {
163
+ global = true;
164
+ continue;
165
+ }
166
+ if (arg === '--local') {
167
+ local = true;
168
+ continue;
169
+ }
170
+ if (arg === '-t' || arg === '--target') {
171
+ target = args[++i];
172
+ continue;
173
+ }
174
+ if (arg === '--') {
175
+ cmdArgs = args.slice(i + 1);
176
+ break;
177
+ }
142
178
  if (!command && !arg.startsWith('-')) {
143
179
  command = arg;
144
180
  continue;
@@ -150,17 +186,21 @@ function parseArgs(args) {
150
186
  }
151
187
  else {
152
188
  console.error(pc.red(`Invalid file: ${arg}`));
153
- console.error(pc.dim('Use: shared, development, or production'));
189
+ console.error(pc.dim('Use: shared, development, production, or local'));
154
190
  process.exit(1);
155
191
  }
156
192
  continue;
157
193
  }
194
+ if (command === 'set' && !arg.startsWith('-') && !key) {
195
+ key = arg;
196
+ continue;
197
+ }
158
198
  if (command === 'has' && !arg.startsWith('-') && !key) {
159
199
  key = arg;
160
200
  continue;
161
201
  }
162
202
  }
163
- return { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, file, key };
203
+ return { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs };
164
204
  }
165
205
  async function main() {
166
206
  const args = process.argv.slice(2);
@@ -168,7 +208,7 @@ async function main() {
168
208
  printHelp();
169
209
  process.exit(0);
170
210
  }
171
- const { command, env, root, dryRun, quiet, warn, json, onlyChanged, requireSource, file, key } = parseArgs(args);
211
+ const { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs } = parseArgs(args);
172
212
  try {
173
213
  switch (command) {
174
214
  case 'init':
@@ -178,8 +218,32 @@ async function main() {
178
218
  await encryptCommand({ root });
179
219
  break;
180
220
  case 'decrypt':
221
+ console.warn(pc.yellow('⚠️ Warning: "hush decrypt" is deprecated and writes unencrypted secrets to disk.'));
222
+ console.warn(pc.yellow(' Use "hush run -- <command>" instead for better security.'));
223
+ console.warn(pc.dim(' To suppress this warning, use "hush unsafe:decrypt"'));
224
+ console.warn('');
225
+ await decryptCommand({ root, env });
226
+ break;
227
+ case 'unsafe:decrypt':
228
+ console.warn(pc.red('⚠️ UNSAFE MODE: Writing unencrypted secrets to disk.'));
229
+ console.warn(pc.red(' These files will be readable by AI assistants and other tools.'));
230
+ console.warn('');
181
231
  await decryptCommand({ root, env });
182
232
  break;
233
+ case 'run':
234
+ await runCommand({ root, env, target, command: cmdArgs });
235
+ break;
236
+ case 'set': {
237
+ let setFile = 'shared';
238
+ if (local) {
239
+ setFile = 'local';
240
+ }
241
+ else if (envExplicit) {
242
+ setFile = env;
243
+ }
244
+ await setCommand({ root, file: setFile, key });
245
+ break;
246
+ }
183
247
  case 'edit':
184
248
  await editCommand({ root, file });
185
249
  break;
@@ -205,6 +269,9 @@ async function main() {
205
269
  case 'status':
206
270
  await statusCommand({ root });
207
271
  break;
272
+ case 'skill':
273
+ await skillCommand({ root, global, local });
274
+ break;
208
275
  default:
209
276
  if (command) {
210
277
  console.error(pc.red(`Unknown command: ${command}`));
@@ -0,0 +1,3 @@
1
+ import type { RunOptions } from '../types.js';
2
+ export declare function runCommand(options: RunOptions): Promise<void>;
3
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAoC/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDnE"}
@@ -0,0 +1,77 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import pc from 'picocolors';
5
+ import { loadConfig } from '../config/loader.js';
6
+ import { filterVarsForTarget } from '../core/filter.js';
7
+ import { interpolateVars, getUnresolvedVars } from '../core/interpolate.js';
8
+ import { mergeVars } from '../core/merge.js';
9
+ import { parseEnvContent } from '../core/parse.js';
10
+ import { decrypt as sopsDecrypt } from '../core/sops.js';
11
+ function getEncryptedPath(sourcePath) {
12
+ return sourcePath + '.encrypted';
13
+ }
14
+ function getDecryptedSecrets(root, env, config) {
15
+ const sharedEncrypted = join(root, getEncryptedPath(config.sources.shared));
16
+ const envEncrypted = join(root, getEncryptedPath(config.sources[env]));
17
+ const localEncrypted = join(root, getEncryptedPath(config.sources.local));
18
+ const varSources = [];
19
+ if (existsSync(sharedEncrypted)) {
20
+ const content = sopsDecrypt(sharedEncrypted);
21
+ varSources.push(parseEnvContent(content));
22
+ }
23
+ if (existsSync(envEncrypted)) {
24
+ const content = sopsDecrypt(envEncrypted);
25
+ varSources.push(parseEnvContent(content));
26
+ }
27
+ if (existsSync(localEncrypted)) {
28
+ const content = sopsDecrypt(localEncrypted);
29
+ varSources.push(parseEnvContent(content));
30
+ }
31
+ if (varSources.length === 0) {
32
+ throw new Error(`No encrypted files found. Expected: ${sharedEncrypted}`);
33
+ }
34
+ const merged = mergeVars(...varSources);
35
+ return interpolateVars(merged);
36
+ }
37
+ export async function runCommand(options) {
38
+ const { root, env, target, command } = options;
39
+ if (!command || command.length === 0) {
40
+ console.error(pc.red('Usage: hush run -- <command>'));
41
+ console.error(pc.dim('Example: hush run -- npm start'));
42
+ console.error(pc.dim(' hush run -e production -- npm run build'));
43
+ console.error(pc.dim(' hush run --target api -- wrangler dev'));
44
+ process.exit(1);
45
+ }
46
+ const config = loadConfig(root);
47
+ let vars = getDecryptedSecrets(root, env, config);
48
+ if (target) {
49
+ const targetConfig = config.targets.find(t => t.name === target);
50
+ if (!targetConfig) {
51
+ console.error(pc.red(`Target "${target}" not found in hush.yaml`));
52
+ console.error(pc.dim(`Available targets: ${config.targets.map(t => t.name).join(', ')}`));
53
+ process.exit(1);
54
+ }
55
+ vars = filterVarsForTarget(vars, targetConfig);
56
+ }
57
+ const unresolved = getUnresolvedVars(vars);
58
+ if (unresolved.length > 0) {
59
+ console.warn(pc.yellow(`Warning: ${unresolved.length} vars have unresolved references`));
60
+ }
61
+ const childEnv = {
62
+ ...process.env,
63
+ ...Object.fromEntries(vars.map(v => [v.key, v.value])),
64
+ };
65
+ const [cmd, ...args] = command;
66
+ const result = spawnSync(cmd, args, {
67
+ stdio: 'inherit',
68
+ env: childEnv,
69
+ shell: true,
70
+ cwd: root,
71
+ });
72
+ if (result.error) {
73
+ console.error(pc.red(`Failed to execute: ${result.error.message}`));
74
+ process.exit(1);
75
+ }
76
+ process.exit(result.status ?? 1);
77
+ }
@@ -0,0 +1,3 @@
1
+ import type { SetOptions } from '../types.js';
2
+ export declare function setCommand(options: SetOptions): Promise<void>;
3
+ //# sourceMappingURL=set.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAuD9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
@@ -0,0 +1,87 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { loadConfig } from '../config/loader.js';
5
+ import { setKey } from '../core/sops.js';
6
+ function promptForValue(key) {
7
+ return new Promise((resolve, reject) => {
8
+ if (!process.stdin.isTTY) {
9
+ reject(new Error('Interactive input requires a terminal (TTY)'));
10
+ return;
11
+ }
12
+ process.stdout.write(`Enter value for ${pc.cyan(key)}: `);
13
+ const stdin = process.stdin;
14
+ stdin.setRawMode(true);
15
+ stdin.resume();
16
+ stdin.setEncoding('utf8');
17
+ let value = '';
18
+ const onData = (char) => {
19
+ switch (char) {
20
+ case '\n':
21
+ case '\r':
22
+ case '\u0004': // Ctrl+D
23
+ stdin.setRawMode(false);
24
+ stdin.pause();
25
+ stdin.removeListener('data', onData);
26
+ process.stdout.write('\n');
27
+ resolve(value);
28
+ break;
29
+ case '\u0003': // Ctrl+C
30
+ stdin.setRawMode(false);
31
+ stdin.pause();
32
+ stdin.removeListener('data', onData);
33
+ process.stdout.write('\n');
34
+ reject(new Error('Cancelled'));
35
+ break;
36
+ case '\u007F': // Backspace
37
+ case '\b':
38
+ if (value.length > 0) {
39
+ value = value.slice(0, -1);
40
+ process.stdout.write('\b \b');
41
+ }
42
+ break;
43
+ default:
44
+ value += char;
45
+ process.stdout.write('\u2022'); // Bullet character for hidden input
46
+ }
47
+ };
48
+ stdin.on('data', onData);
49
+ });
50
+ }
51
+ export async function setCommand(options) {
52
+ const { root, file, key } = options;
53
+ const config = loadConfig(root);
54
+ const fileKey = file ?? 'shared';
55
+ const sourcePath = config.sources[fileKey];
56
+ const encryptedPath = join(root, sourcePath + '.encrypted');
57
+ if (!key) {
58
+ console.error(pc.red('Usage: hush set <KEY> [-e environment]'));
59
+ console.error(pc.dim('Example: hush set DATABASE_URL'));
60
+ console.error(pc.dim(' hush set API_KEY -e production'));
61
+ console.error(pc.dim('\nTo edit all secrets in an editor, use: hush edit'));
62
+ process.exit(1);
63
+ }
64
+ if (!existsSync(encryptedPath) && !existsSync(join(root, '.sops.yaml'))) {
65
+ console.error(pc.red('Hush is not initialized in this directory'));
66
+ console.error(pc.dim('Run "hush init" first, then "hush encrypt"'));
67
+ process.exit(1);
68
+ }
69
+ try {
70
+ const value = await promptForValue(key);
71
+ if (!value) {
72
+ console.error(pc.yellow('No value entered, aborting'));
73
+ process.exit(1);
74
+ }
75
+ setKey(encryptedPath, key, value);
76
+ const envLabel = fileKey === 'shared' ? '' : ` in ${fileKey}`;
77
+ console.log(pc.green(`\n${key} set${envLabel} (${value.length} chars, encrypted)`));
78
+ }
79
+ catch (error) {
80
+ const err = error;
81
+ if (err.message === 'Cancelled') {
82
+ console.log(pc.yellow('Cancelled'));
83
+ process.exit(1);
84
+ }
85
+ throw err;
86
+ }
87
+ }
@@ -0,0 +1,3 @@
1
+ import type { SkillOptions } from '../types.js';
2
+ export declare function skillCommand(options: SkillOptions): Promise<void>;
3
+ //# sourceMappingURL=skill.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAwhChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}