@chriscode/hush 2.2.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
@@ -13,7 +13,9 @@ import { inspectCommand } from './commands/inspect.js';
13
13
  import { hasCommand } from './commands/has.js';
14
14
  import { checkCommand } from './commands/check.js';
15
15
  import { skillCommand } from './commands/skill.js';
16
- 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';
17
19
  function printHelp() {
18
20
  console.log(`
19
21
  ${pc.bold('hush')} - SOPS-based secrets management for monorepos
@@ -34,6 +36,7 @@ ${pc.bold('Commands:')}
34
36
  push Push secrets to Cloudflare Workers
35
37
  status Show configuration and status
36
38
  skill Install Claude Code / OpenCode skill
39
+ keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
37
40
 
38
41
  ${pc.bold('Deprecated Commands:')}
39
42
  decrypt Write secrets to disk (unsafe - use 'run' instead)
@@ -48,20 +51,24 @@ ${pc.bold('Options:')}
48
51
  --json Output machine-readable JSON (check only)
49
52
  --only-changed Only check git-modified files (check only)
50
53
  --require-source Fail if source file is missing (check only)
54
+ --allow-plaintext Allow plaintext .env files (check only, not recommended)
51
55
  --global Install skill to ~/.claude/skills/ (skill only)
52
56
  --local Install skill to ./.claude/skills/ (skill/set only)
57
+ --gui Use macOS dialog for input (set only, for AI agents)
53
58
  -h, --help Show this help message
54
59
  -v, --version Show version number
55
60
 
56
61
  ${pc.bold('Examples:')}
57
- hush init Initialize hush.yaml config
62
+ hush init Initialize config + generate keys
58
63
  hush encrypt Encrypt .env files
59
64
  hush run -- npm start Run with secrets in memory (AI-safe!)
60
65
  hush run -e prod -- npm build Run with production secrets
61
66
  hush run -t api -- wrangler dev Run filtered for 'api' target
62
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)
63
69
  hush set API_KEY -e prod Set a production secret
64
- hush set API_KEY --local Set a personal local override
70
+ hush keys setup Pull key from 1Password or verify local
71
+ hush keys generate Generate new key + backup to 1Password
65
72
  hush edit Edit all shared secrets in $EDITOR
66
73
  hush edit development Edit development secrets in $EDITOR
67
74
  hush edit local Edit personal local overrides
@@ -92,6 +99,7 @@ function parseFileKey(value) {
92
99
  }
93
100
  function parseArgs(args) {
94
101
  let command = '';
102
+ let subcommand;
95
103
  let env = 'development';
96
104
  let envExplicit = false;
97
105
  let root = process.cwd();
@@ -101,8 +109,12 @@ function parseArgs(args) {
101
109
  let json = false;
102
110
  let onlyChanged = false;
103
111
  let requireSource = false;
112
+ let allowPlaintext = false;
104
113
  let global = false;
105
114
  let local = false;
115
+ let force = false;
116
+ let gui = false;
117
+ let vault;
106
118
  let file;
107
119
  let key;
108
120
  let target;
@@ -159,6 +171,10 @@ function parseArgs(args) {
159
171
  requireSource = true;
160
172
  continue;
161
173
  }
174
+ if (arg === '--allow-plaintext') {
175
+ allowPlaintext = true;
176
+ continue;
177
+ }
162
178
  if (arg === '--global') {
163
179
  global = true;
164
180
  continue;
@@ -167,6 +183,18 @@ function parseArgs(args) {
167
183
  local = true;
168
184
  continue;
169
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
+ }
170
198
  if (arg === '-t' || arg === '--target') {
171
199
  target = args[++i];
172
200
  continue;
@@ -199,8 +227,40 @@ function parseArgs(args) {
199
227
  key = arg;
200
228
  continue;
201
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('');
202
263
  }
203
- return { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs };
204
264
  }
205
265
  async function main() {
206
266
  const args = process.argv.slice(2);
@@ -208,7 +268,8 @@ async function main() {
208
268
  printHelp();
209
269
  process.exit(0);
210
270
  }
211
- const { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs } = 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);
212
273
  try {
213
274
  switch (command) {
214
275
  case 'init':
@@ -241,7 +302,7 @@ async function main() {
241
302
  else if (envExplicit) {
242
303
  setFile = env;
243
304
  }
244
- await setCommand({ root, file: setFile, key });
305
+ await setCommand({ root, file: setFile, key, gui });
245
306
  break;
246
307
  }
247
308
  case 'edit':
@@ -261,7 +322,7 @@ async function main() {
261
322
  await hasCommand({ root, env, key, quiet });
262
323
  break;
263
324
  case 'check':
264
- await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource });
325
+ await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
265
326
  break;
266
327
  case 'push':
267
328
  await pushCommand({ root, dryRun });
@@ -272,6 +333,14 @@ async function main() {
272
333
  case 'skill':
273
334
  await skillCommand({ root, global, local });
274
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;
275
344
  default:
276
345
  if (command) {
277
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"}
@@ -0,0 +1,136 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import pc from 'picocolors';
4
+ import { stringify as yamlStringify } from 'yaml';
5
+ import { loadConfig } from '../config/loader.js';
6
+ import { opAvailable, opGetKey, opStoreKey, opListKeys } from '../lib/onepassword.js';
7
+ import { ageAvailable, ageGenerate, agePublicFromPrivate, keyExists, keySave, keyLoad, keysList, keyPath } from '../lib/age.js';
8
+ function getProject(root) {
9
+ const config = loadConfig(root);
10
+ if (config.project)
11
+ return config.project;
12
+ const pkgPath = join(root, 'package.json');
13
+ if (existsSync(pkgPath)) {
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
+ console.error(pc.red('No project identifier found.'));
27
+ console.error(pc.dim('Add "project: my-project" to hush.yaml'));
28
+ process.exit(1);
29
+ }
30
+ export async function keysCommand(options) {
31
+ const { root, subcommand, vault, force } = options;
32
+ switch (subcommand) {
33
+ case 'setup': {
34
+ const project = getProject(root);
35
+ console.log(pc.blue(`Setting up keys for ${pc.cyan(project)}...`));
36
+ if (keyExists(project)) {
37
+ console.log(pc.green('Key already exists locally.'));
38
+ return;
39
+ }
40
+ if (opAvailable()) {
41
+ const priv = opGetKey(project, vault);
42
+ if (priv) {
43
+ const pub = agePublicFromPrivate(priv);
44
+ keySave(project, { private: priv, public: pub });
45
+ console.log(pc.green('Pulled key from 1Password.'));
46
+ return;
47
+ }
48
+ }
49
+ console.log(pc.yellow('No key found. Run "hush keys generate" to create one.'));
50
+ break;
51
+ }
52
+ case 'generate': {
53
+ if (!ageAvailable()) {
54
+ console.error(pc.red('age not installed. Run: brew install age'));
55
+ process.exit(1);
56
+ }
57
+ const project = getProject(root);
58
+ if (keyExists(project) && !force) {
59
+ console.error(pc.yellow(`Key exists for ${project}. Use --force to overwrite.`));
60
+ process.exit(1);
61
+ }
62
+ console.log(pc.blue(`Generating key for ${pc.cyan(project)}...`));
63
+ const key = ageGenerate();
64
+ keySave(project, key);
65
+ console.log(pc.green(`Saved to ${keyPath(project)}`));
66
+ console.log(pc.dim(`Public: ${key.public}`));
67
+ if (opAvailable()) {
68
+ try {
69
+ opStoreKey(project, key.private, key.public, vault);
70
+ console.log(pc.green('Stored in 1Password.'));
71
+ }
72
+ catch (e) {
73
+ console.warn(pc.yellow(`Could not store in 1Password: ${e.message}`));
74
+ }
75
+ }
76
+ const sopsPath = join(root, '.sops.yaml');
77
+ if (!existsSync(sopsPath)) {
78
+ writeFileSync(sopsPath, yamlStringify({ creation_rules: [{ encrypted_regex: '.*', age: key.public }] }));
79
+ console.log(pc.green('Created .sops.yaml'));
80
+ }
81
+ else {
82
+ console.log(pc.yellow('.sops.yaml exists. Add this public key:'));
83
+ console.log(` ${key.public}`);
84
+ }
85
+ break;
86
+ }
87
+ case 'pull': {
88
+ if (!opAvailable()) {
89
+ console.error(pc.red('1Password CLI not available or not signed in.'));
90
+ process.exit(1);
91
+ }
92
+ const project = getProject(root);
93
+ const priv = opGetKey(project, vault);
94
+ if (!priv) {
95
+ console.error(pc.red(`No key in 1Password for ${project}`));
96
+ process.exit(1);
97
+ }
98
+ const pub = agePublicFromPrivate(priv);
99
+ keySave(project, { private: priv, public: pub });
100
+ console.log(pc.green(`Pulled and saved to ${keyPath(project)}`));
101
+ break;
102
+ }
103
+ case 'push': {
104
+ if (!opAvailable()) {
105
+ console.error(pc.red('1Password CLI not available or not signed in.'));
106
+ process.exit(1);
107
+ }
108
+ const project = getProject(root);
109
+ const key = keyLoad(project);
110
+ if (!key) {
111
+ console.error(pc.red(`No local key for ${project}`));
112
+ process.exit(1);
113
+ }
114
+ opStoreKey(project, key.private, key.public, vault);
115
+ console.log(pc.green('Pushed to 1Password.'));
116
+ break;
117
+ }
118
+ case 'list': {
119
+ console.log(pc.blue('Local keys:'));
120
+ for (const k of keysList()) {
121
+ console.log(` ${pc.cyan(k.project)} ${pc.dim(k.public.slice(0, 20))}...`);
122
+ }
123
+ if (opAvailable()) {
124
+ console.log(pc.blue('\n1Password keys:'));
125
+ for (const project of opListKeys(vault)) {
126
+ console.log(` ${pc.cyan(project)}`);
127
+ }
128
+ }
129
+ break;
130
+ }
131
+ default:
132
+ console.error(pc.red(`Unknown: hush keys ${subcommand}`));
133
+ console.log(pc.dim('Commands: setup, generate, pull, push, list'));
134
+ process.exit(1);
135
+ }
136
+ }
@@ -1 +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"}
1
+ {"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAyF9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
@@ -1,14 +1,25 @@
1
+ import { execSync } from 'node:child_process';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { join } from 'node:path';
4
+ import { platform } from 'node:os';
3
5
  import pc from 'picocolors';
4
6
  import { loadConfig } from '../config/loader.js';
5
7
  import { setKey } from '../core/sops.js';
6
- function promptForValue(key) {
8
+ function promptViaMacOSDialog(key) {
9
+ try {
10
+ const script = `display dialog "Enter value for ${key}:" default answer "" with hidden answer with title "Hush - Set Secret"`;
11
+ const result = execSync(`osascript -e '${script}' -e 'text returned of result'`, {
12
+ encoding: 'utf-8',
13
+ stdio: ['pipe', 'pipe', 'pipe'],
14
+ });
15
+ return result.trim();
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ function promptViaTTY(key) {
7
22
  return new Promise((resolve, reject) => {
8
- if (!process.stdin.isTTY) {
9
- reject(new Error('Interactive input requires a terminal (TTY)'));
10
- return;
11
- }
12
23
  process.stdout.write(`Enter value for ${pc.cyan(key)}: `);
13
24
  const stdin = process.stdin;
14
25
  stdin.setRawMode(true);
@@ -19,21 +30,21 @@ function promptForValue(key) {
19
30
  switch (char) {
20
31
  case '\n':
21
32
  case '\r':
22
- case '\u0004': // Ctrl+D
33
+ case '\u0004':
23
34
  stdin.setRawMode(false);
24
35
  stdin.pause();
25
36
  stdin.removeListener('data', onData);
26
37
  process.stdout.write('\n');
27
38
  resolve(value);
28
39
  break;
29
- case '\u0003': // Ctrl+C
40
+ case '\u0003':
30
41
  stdin.setRawMode(false);
31
42
  stdin.pause();
32
43
  stdin.removeListener('data', onData);
33
44
  process.stdout.write('\n');
34
45
  reject(new Error('Cancelled'));
35
46
  break;
36
- case '\u007F': // Backspace
47
+ case '\u007F':
37
48
  case '\b':
38
49
  if (value.length > 0) {
39
50
  value = value.slice(0, -1);
@@ -42,14 +53,36 @@ function promptForValue(key) {
42
53
  break;
43
54
  default:
44
55
  value += char;
45
- process.stdout.write('\u2022'); // Bullet character for hidden input
56
+ process.stdout.write('\u2022');
46
57
  }
47
58
  };
48
59
  stdin.on('data', onData);
49
60
  });
50
61
  }
62
+ async function promptForValue(key, forceGui) {
63
+ if (forceGui && platform() === 'darwin') {
64
+ console.log(pc.dim('Opening dialog for secret input...'));
65
+ const value = promptViaMacOSDialog(key);
66
+ if (value !== null) {
67
+ return value;
68
+ }
69
+ throw new Error('Dialog cancelled or failed');
70
+ }
71
+ if (process.stdin.isTTY) {
72
+ return promptViaTTY(key);
73
+ }
74
+ if (platform() === 'darwin') {
75
+ console.log(pc.dim('Opening dialog for secret input...'));
76
+ const value = promptViaMacOSDialog(key);
77
+ if (value !== null) {
78
+ return value;
79
+ }
80
+ throw new Error('Dialog cancelled or failed');
81
+ }
82
+ throw new Error('Interactive input requires a terminal (TTY) or macOS');
83
+ }
51
84
  export async function setCommand(options) {
52
- const { root, file, key } = options;
85
+ const { root, file, key, gui } = options;
53
86
  const config = loadConfig(root);
54
87
  const fileKey = file ?? 'shared';
55
88
  const sourcePath = config.sources[fileKey];
@@ -67,7 +100,7 @@ export async function setCommand(options) {
67
100
  process.exit(1);
68
101
  }
69
102
  try {
70
- const value = await promptForValue(key);
103
+ const value = await promptForValue(key, gui ?? false);
71
104
  if (!value) {
72
105
  console.error(pc.yellow('No value entered, aborting'));
73
106
  process.exit(1);
@@ -1,5 +1,10 @@
1
1
  import type { HushConfig } from '../types.js';
2
2
  export declare function findConfigPath(root: string): string | null;
3
3
  export declare function loadConfig(root: string): HushConfig;
4
+ export declare function checkSchemaVersion(config: HushConfig): {
5
+ needsMigration: boolean;
6
+ from: number;
7
+ to: number;
8
+ };
4
9
  export declare function validateConfig(config: HushConfig): string[];
5
10
  //# sourceMappingURL=loader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,aAAa,CAAC;AAKtD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ1D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAiBnD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAuB3D"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK9C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ1D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAmBnD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAO5G;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAuB3D"}
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { parse as parseYaml } from 'yaml';
4
- import { DEFAULT_SOURCES } from '../types.js';
4
+ import { DEFAULT_SOURCES, CURRENT_SCHEMA_VERSION } from '../types.js';
5
5
  const CONFIG_FILENAMES = ['hush.yaml', 'hush.yml'];
6
6
  export function findConfigPath(root) {
7
7
  for (const filename of CONFIG_FILENAMES) {
@@ -23,10 +23,20 @@ export function loadConfig(root) {
23
23
  const content = readFileSync(configPath, 'utf-8');
24
24
  const parsed = parseYaml(content);
25
25
  return {
26
+ schema_version: parsed.schema_version,
27
+ project: parsed.project,
26
28
  sources: { ...DEFAULT_SOURCES, ...parsed.sources },
27
29
  targets: parsed.targets ?? [{ name: 'root', path: '.', format: 'dotenv' }],
28
30
  };
29
31
  }
32
+ export function checkSchemaVersion(config) {
33
+ const configVersion = config.schema_version ?? 1;
34
+ return {
35
+ needsMigration: configVersion < CURRENT_SCHEMA_VERSION,
36
+ from: configVersion,
37
+ to: CURRENT_SCHEMA_VERSION,
38
+ };
39
+ }
30
40
  export function validateConfig(config) {
31
41
  const errors = [];
32
42
  if (!config.sources.shared) {
@@ -42,7 +52,7 @@ export function validateConfig(config) {
42
52
  if (!target.format) {
43
53
  errors.push(`Target "${target.name}" must have a format`);
44
54
  }
45
- if (!['dotenv', 'wrangler', 'json', 'shell'].includes(target.format)) {
55
+ if (!['dotenv', 'wrangler', 'json', 'shell', 'yaml'].includes(target.format)) {
46
56
  errors.push(`Target "${target.name}" has invalid format "${target.format}"`);
47
57
  }
48
58
  }
@@ -0,0 +1,16 @@
1
+ export interface AgeKey {
2
+ private: string;
3
+ public: string;
4
+ }
5
+ export declare function ageAvailable(): boolean;
6
+ export declare function ageGenerate(): AgeKey;
7
+ export declare function agePublicFromPrivate(privateKey: string): string;
8
+ export declare function keyPath(project: string): string;
9
+ export declare function keyExists(project: string): boolean;
10
+ export declare function keySave(project: string, key: AgeKey): void;
11
+ export declare function keyLoad(project: string): AgeKey | null;
12
+ export declare function keysList(): {
13
+ project: string;
14
+ public: string;
15
+ }[];
16
+ //# sourceMappingURL=age.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"age.d.ts","sourceRoot":"","sources":["../../src/lib/age.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAID,wBAAgB,YAAY,IAAI,OAAO,CAOtC;AAED,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAI1D;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAStD;AAED,wBAAgB,QAAQ,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,CAYhE"}
@@ -0,0 +1,61 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ const KEYS_DIR = join(homedir(), '.config', 'sops', 'age', 'keys');
6
+ export function ageAvailable() {
7
+ try {
8
+ execSync('which age-keygen', { stdio: 'pipe' });
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export function ageGenerate() {
16
+ const output = execSync('age-keygen', { encoding: 'utf-8' });
17
+ const pub = output.match(/public key: (age1[a-z0-9]+)/)?.[1];
18
+ const priv = output.match(/(AGE-SECRET-KEY-[A-Z0-9]+)/)?.[1];
19
+ if (!pub || !priv)
20
+ throw new Error('Failed to generate age key');
21
+ return { private: priv, public: pub };
22
+ }
23
+ export function agePublicFromPrivate(privateKey) {
24
+ return execSync(`echo "${privateKey}" | age-keygen -y`, {
25
+ encoding: 'utf-8',
26
+ shell: '/bin/bash',
27
+ }).trim();
28
+ }
29
+ export function keyPath(project) {
30
+ return join(KEYS_DIR, `${project.replace(/\//g, '-')}.txt`);
31
+ }
32
+ export function keyExists(project) {
33
+ return existsSync(keyPath(project));
34
+ }
35
+ export function keySave(project, key) {
36
+ const path = keyPath(project);
37
+ mkdirSync(dirname(path), { recursive: true });
38
+ writeFileSync(path, `# project: ${project}\n# public key: ${key.public}\n${key.private}\n`, { mode: 0o600 });
39
+ }
40
+ export function keyLoad(project) {
41
+ const path = keyPath(project);
42
+ if (!existsSync(path))
43
+ return null;
44
+ const content = readFileSync(path, 'utf-8');
45
+ const pub = content.match(/# public key: (age1[a-z0-9]+)/)?.[1];
46
+ const priv = content.match(/(AGE-SECRET-KEY-[A-Z0-9]+)/)?.[1];
47
+ return pub && priv ? { private: priv, public: pub } : null;
48
+ }
49
+ export function keysList() {
50
+ if (!existsSync(KEYS_DIR))
51
+ return [];
52
+ return readdirSync(KEYS_DIR)
53
+ .filter(f => f.endsWith('.txt'))
54
+ .map(f => {
55
+ const content = readFileSync(join(KEYS_DIR, f), 'utf-8');
56
+ const project = content.match(/# project: (.+)/)?.[1] ?? content.match(/# repo: (.+)/)?.[1];
57
+ const pub = content.match(/# public key: (age1[a-z0-9]+)/)?.[1];
58
+ return project && pub ? { project, public: pub } : null;
59
+ })
60
+ .filter((k) => k !== null);
61
+ }
@@ -0,0 +1,6 @@
1
+ export declare const OP_ITEM_PREFIX = "SOPS Key - ";
2
+ export declare function opAvailable(): boolean;
3
+ export declare function opGetKey(project: string, vault?: string): string | null;
4
+ export declare function opStoreKey(project: string, privateKey: string, publicKey: string, vault?: string): void;
5
+ export declare function opListKeys(vault?: string): string[];
6
+ //# sourceMappingURL=onepassword.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"onepassword.d.ts","sourceRoot":"","sources":["../../src/lib/onepassword.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAE5C,wBAAgB,WAAW,IAAI,OAAO,CAOrC;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWvE;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAcvG;AAED,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAcnD"}
@@ -0,0 +1,48 @@
1
+ import { execSync, spawnSync } from 'node:child_process';
2
+ export const OP_ITEM_PREFIX = 'SOPS Key - ';
3
+ export function opAvailable() {
4
+ try {
5
+ execSync('op whoami', { stdio: 'pipe' });
6
+ return true;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export function opGetKey(project, vault) {
13
+ try {
14
+ const vaultArgs = vault ? ['--vault', vault] : [];
15
+ const result = execSync(['op', 'item', 'get', `${OP_ITEM_PREFIX}${project}`, ...vaultArgs, '--fields', 'password', '--reveal'].join(' '), { encoding: 'utf-8', stdio: 'pipe' });
16
+ return result.trim() || null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function opStoreKey(project, privateKey, publicKey, vault) {
23
+ const args = [
24
+ 'item', 'create',
25
+ '--category', 'password',
26
+ '--title', `${OP_ITEM_PREFIX}${project}`,
27
+ ...(vault ? ['--vault', vault] : []),
28
+ `password=${privateKey}`,
29
+ `public_key[text]=${publicKey}`,
30
+ ];
31
+ const result = spawnSync('op', args, { stdio: 'pipe', encoding: 'utf-8' });
32
+ if (result.status !== 0) {
33
+ throw new Error(result.stderr || 'Failed to store in 1Password');
34
+ }
35
+ }
36
+ export function opListKeys(vault) {
37
+ try {
38
+ const vaultArgs = vault ? ['--vault', vault] : [];
39
+ const result = execSync(['op', 'item', 'list', '--categories', 'password', ...vaultArgs, '--format', 'json'].join(' '), { encoding: 'utf-8', stdio: 'pipe' });
40
+ const items = JSON.parse(result);
41
+ return items
42
+ .filter(i => i.title.startsWith(OP_ITEM_PREFIX))
43
+ .map(i => i.title.replace(OP_ITEM_PREFIX, ''));
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ }
package/dist/types.d.ts CHANGED
@@ -14,9 +14,12 @@ export interface SourceFiles {
14
14
  local: string;
15
15
  }
16
16
  export interface HushConfig {
17
+ schema_version?: number;
18
+ project?: string;
17
19
  sources: SourceFiles;
18
20
  targets: Target[];
19
21
  }
22
+ export declare const CURRENT_SCHEMA_VERSION = 2;
20
23
  export interface EnvVar {
21
24
  key: string;
22
25
  value: string;
@@ -36,6 +39,7 @@ export interface SetOptions {
36
39
  root: string;
37
40
  file?: 'shared' | 'development' | 'production' | 'local';
38
41
  key?: string;
42
+ gui?: boolean;
39
43
  }
40
44
  export interface RunOptions {
41
45
  root: string;
@@ -64,6 +68,7 @@ export interface CheckOptions {
64
68
  quiet: boolean;
65
69
  onlyChanged: boolean;
66
70
  requireSource: boolean;
71
+ allowPlaintext?: boolean;
67
72
  }
68
73
  export type CheckErrorType = 'SOURCE_MISSING' | 'ENCRYPTED_MISSING' | 'DECRYPT_FAILED' | 'SOPS_NOT_INSTALLED';
69
74
  export interface CheckFileResult {
@@ -75,9 +80,14 @@ export interface CheckFileResult {
75
80
  changed: string[];
76
81
  error?: CheckErrorType;
77
82
  }
83
+ export interface PlaintextFileResult {
84
+ file: string;
85
+ keyCount: number;
86
+ }
78
87
  export interface CheckResult {
79
- status: 'ok' | 'drift' | 'error';
88
+ status: 'ok' | 'drift' | 'error' | 'plaintext';
80
89
  files: CheckFileResult[];
90
+ plaintextFiles?: PlaintextFileResult[];
81
91
  }
82
92
  export interface SkillOptions {
83
93
  root: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js CHANGED
@@ -1,3 +1,4 @@
1
+ export const CURRENT_SCHEMA_VERSION = 2;
1
2
  export const DEFAULT_SOURCES = {
2
3
  shared: '.env',
3
4
  development: '.env.development',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriscode/hush",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,14 @@
12
12
  "types": "./dist/index.d.ts"
13
13
  }
14
14
  },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "dev": "tsc --watch",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "prepublishOnly": "pnpm build && pnpm test",
21
+ "type-check": "tsc --noEmit"
22
+ },
15
23
  "keywords": [
16
24
  "secrets",
17
25
  "sops",
@@ -53,12 +61,5 @@
53
61
  ],
54
62
  "publishConfig": {
55
63
  "access": "public"
56
- },
57
- "scripts": {
58
- "build": "tsc",
59
- "dev": "tsc --watch",
60
- "test": "vitest run",
61
- "test:watch": "vitest",
62
- "type-check": "tsc --noEmit"
63
64
  }
64
- }
65
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Chris Hasson
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.