@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 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
- const VERSION = '2.1.0';
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
- decrypt Decrypt and distribute to targets
26
- set [file] Set/edit secrets in $EDITOR (alias: edit)
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 hush.yaml config
62
+ hush init Initialize config + generate keys
51
63
  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)
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 ((command === 'set' || command === 'edit') && !arg.startsWith('-')) {
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 production'));
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 'set':
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,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"}
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"}
@@ -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, findConfigPath } from '../config/loader.js';
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
- return checkPairs(root, pairs, requireSource, onlyChanged);
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":"AAKA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;AAqCnE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAkCrE"}
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"}
@@ -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. Create your .env files (.env, .env.development, .env.production)');
61
- console.log(' 2. Run "hush encrypt" to encrypt them');
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,8 @@
1
+ export interface KeysOptions {
2
+ root: string;
3
+ subcommand: string;
4
+ vault?: string;
5
+ force?: boolean;
6
+ }
7
+ export declare function keysCommand(options: KeysOptions): Promise<void>;
8
+ //# sourceMappingURL=keys.d.ts.map
@@ -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"}