@chriscode/hush 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +136 -21
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +67 -10
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +99 -4
- package/dist/commands/keys.d.ts +8 -0
- package/dist/commands/keys.d.ts.map +1 -0
- package/dist/commands/keys.js +136 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +77 -0
- package/dist/commands/set.d.ts +3 -0
- package/dist/commands/set.d.ts.map +1 -0
- package/dist/commands/set.js +120 -0
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +295 -223
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +12 -2
- package/dist/core/sops.d.ts +5 -0
- package/dist/core/sops.d.ts.map +1 -1
- package/dist/core/sops.js +49 -1
- package/dist/lib/age.d.ts +16 -0
- package/dist/lib/age.d.ts.map +1 -0
- package/dist/lib/age.js +61 -0
- package/dist/lib/onepassword.d.ts +6 -0
- package/dist/lib/onepassword.d.ts.map +1 -0
- package/dist/lib/onepassword.js +48 -0
- package/dist/types.d.ts +24 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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 @@
|
|
|
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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { platform } from 'node:os';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import { loadConfig } from '../config/loader.js';
|
|
7
|
+
import { setKey } from '../core/sops.js';
|
|
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) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
process.stdout.write(`Enter value for ${pc.cyan(key)}: `);
|
|
24
|
+
const stdin = process.stdin;
|
|
25
|
+
stdin.setRawMode(true);
|
|
26
|
+
stdin.resume();
|
|
27
|
+
stdin.setEncoding('utf8');
|
|
28
|
+
let value = '';
|
|
29
|
+
const onData = (char) => {
|
|
30
|
+
switch (char) {
|
|
31
|
+
case '\n':
|
|
32
|
+
case '\r':
|
|
33
|
+
case '\u0004':
|
|
34
|
+
stdin.setRawMode(false);
|
|
35
|
+
stdin.pause();
|
|
36
|
+
stdin.removeListener('data', onData);
|
|
37
|
+
process.stdout.write('\n');
|
|
38
|
+
resolve(value);
|
|
39
|
+
break;
|
|
40
|
+
case '\u0003':
|
|
41
|
+
stdin.setRawMode(false);
|
|
42
|
+
stdin.pause();
|
|
43
|
+
stdin.removeListener('data', onData);
|
|
44
|
+
process.stdout.write('\n');
|
|
45
|
+
reject(new Error('Cancelled'));
|
|
46
|
+
break;
|
|
47
|
+
case '\u007F':
|
|
48
|
+
case '\b':
|
|
49
|
+
if (value.length > 0) {
|
|
50
|
+
value = value.slice(0, -1);
|
|
51
|
+
process.stdout.write('\b \b');
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
value += char;
|
|
56
|
+
process.stdout.write('\u2022');
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
stdin.on('data', onData);
|
|
60
|
+
});
|
|
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
|
+
}
|
|
84
|
+
export async function setCommand(options) {
|
|
85
|
+
const { root, file, key, gui } = options;
|
|
86
|
+
const config = loadConfig(root);
|
|
87
|
+
const fileKey = file ?? 'shared';
|
|
88
|
+
const sourcePath = config.sources[fileKey];
|
|
89
|
+
const encryptedPath = join(root, sourcePath + '.encrypted');
|
|
90
|
+
if (!key) {
|
|
91
|
+
console.error(pc.red('Usage: hush set <KEY> [-e environment]'));
|
|
92
|
+
console.error(pc.dim('Example: hush set DATABASE_URL'));
|
|
93
|
+
console.error(pc.dim(' hush set API_KEY -e production'));
|
|
94
|
+
console.error(pc.dim('\nTo edit all secrets in an editor, use: hush edit'));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
if (!existsSync(encryptedPath) && !existsSync(join(root, '.sops.yaml'))) {
|
|
98
|
+
console.error(pc.red('Hush is not initialized in this directory'));
|
|
99
|
+
console.error(pc.dim('Run "hush init" first, then "hush encrypt"'));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const value = await promptForValue(key, gui ?? false);
|
|
104
|
+
if (!value) {
|
|
105
|
+
console.error(pc.yellow('No value entered, aborting'));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
setKey(encryptedPath, key, value);
|
|
109
|
+
const envLabel = fileKey === 'shared' ? '' : ` in ${fileKey}`;
|
|
110
|
+
console.log(pc.green(`\n${key} set${envLabel} (${value.length} chars, encrypted)`));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
const err = error;
|
|
114
|
+
if (err.message === 'Cancelled') {
|
|
115
|
+
console.log(pc.yellow('Cancelled'));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -1 +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;
|
|
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"}
|