@chriscode/hush 5.0.0 → 5.0.2
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 +39 -26
- package/dist/commands/check.d.ts +3 -3
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +27 -31
- package/dist/commands/decrypt.d.ts +2 -2
- package/dist/commands/decrypt.d.ts.map +1 -1
- package/dist/commands/decrypt.js +52 -55
- package/dist/commands/edit.d.ts +2 -2
- package/dist/commands/edit.d.ts.map +1 -1
- package/dist/commands/edit.js +10 -12
- package/dist/commands/encrypt.d.ts +2 -2
- package/dist/commands/encrypt.d.ts.map +1 -1
- package/dist/commands/encrypt.js +27 -29
- package/dist/commands/expansions.d.ts +2 -2
- package/dist/commands/expansions.d.ts.map +1 -1
- package/dist/commands/expansions.js +46 -44
- package/dist/commands/has.d.ts +2 -2
- package/dist/commands/has.d.ts.map +1 -1
- package/dist/commands/has.js +12 -15
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +92 -100
- package/dist/commands/inspect.d.ts +2 -2
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +14 -16
- package/dist/commands/keys.d.ts +2 -1
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +47 -49
- package/dist/commands/list.d.ts +2 -2
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +11 -14
- package/dist/commands/migrate.d.ts +2 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +38 -37
- package/dist/commands/push.d.ts +2 -2
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +41 -45
- package/dist/commands/resolve.d.ts +2 -2
- package/dist/commands/resolve.d.ts.map +1 -1
- package/dist/commands/resolve.js +25 -28
- package/dist/commands/run.d.ts +2 -2
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +35 -39
- package/dist/commands/set.d.ts +2 -2
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +61 -70
- package/dist/commands/skill.d.ts +2 -2
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +149 -459
- package/dist/commands/status.d.ts +2 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +48 -52
- package/dist/commands/template.d.ts +2 -2
- package/dist/commands/template.d.ts.map +1 -1
- package/dist/commands/template.js +36 -39
- package/dist/commands/trace.d.ts +2 -2
- package/dist/commands/trace.d.ts.map +1 -1
- package/dist/commands/trace.js +16 -19
- package/dist/config/loader.js +3 -3
- package/dist/context.d.ts +3 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +60 -0
- package/dist/core/parse.js +3 -3
- package/dist/core/sops.js +9 -9
- package/dist/core/template.d.ts +2 -2
- package/dist/core/template.d.ts.map +1 -1
- package/dist/core/template.js +11 -12
- package/dist/lib/age.js +9 -9
- package/dist/lib/fs.d.ts +25 -0
- package/dist/lib/fs.d.ts.map +1 -0
- package/dist/lib/fs.js +36 -0
- package/dist/lib/onepassword.d.ts.map +1 -1
- package/dist/lib/onepassword.js +41 -4
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/version-check.js +5 -5
- package/package.json +3 -2
package/dist/commands/init.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import pc from 'picocolors';
|
|
4
2
|
import { stringify as stringifyYaml } from 'yaml';
|
|
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';
|
|
8
3
|
import { DEFAULT_SOURCES } from '../types.js';
|
|
9
|
-
function getProjectFromPackageJson(root) {
|
|
10
|
-
const pkgPath = join(root, 'package.json');
|
|
11
|
-
if (!existsSync(pkgPath))
|
|
4
|
+
function getProjectFromPackageJson(ctx, root) {
|
|
5
|
+
const pkgPath = ctx.path.join(root, 'package.json');
|
|
6
|
+
if (!ctx.fs.existsSync(pkgPath))
|
|
12
7
|
return null;
|
|
13
8
|
try {
|
|
14
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
9
|
+
const pkg = JSON.parse(ctx.fs.readFileSync(pkgPath, 'utf-8'));
|
|
15
10
|
if (typeof pkg.repository === 'string') {
|
|
16
11
|
const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
17
12
|
if (match)
|
|
@@ -28,86 +23,85 @@ function getProjectFromPackageJson(root) {
|
|
|
28
23
|
}
|
|
29
24
|
return null;
|
|
30
25
|
}
|
|
31
|
-
async function tryExistingLocalKey(project) {
|
|
32
|
-
if (!keyExists(project))
|
|
26
|
+
async function tryExistingLocalKey(ctx, project) {
|
|
27
|
+
if (!ctx.age.keyExists(project))
|
|
33
28
|
return null;
|
|
34
|
-
const existing = await
|
|
29
|
+
const existing = await ctx.age.keyLoad(project);
|
|
35
30
|
if (!existing)
|
|
36
31
|
return null;
|
|
37
|
-
|
|
32
|
+
ctx.logger.log(pc.green(`Using existing key for ${pc.cyan(project)}`));
|
|
38
33
|
return { publicKey: existing.public, source: 'existing' };
|
|
39
34
|
}
|
|
40
|
-
async function tryPullFrom1Password(project) {
|
|
41
|
-
if (!opAvailable())
|
|
35
|
+
async function tryPullFrom1Password(ctx, project) {
|
|
36
|
+
if (!ctx.onepassword.opAvailable())
|
|
42
37
|
return null;
|
|
43
|
-
|
|
44
|
-
const priv = opGetKey(project);
|
|
38
|
+
ctx.logger.log(pc.dim('Checking 1Password for existing key...'));
|
|
39
|
+
const priv = ctx.onepassword.opGetKey(project);
|
|
45
40
|
if (!priv)
|
|
46
41
|
return null;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
console.log(pc.green(`Pulled key from 1Password for ${pc.cyan(project)}`));
|
|
42
|
+
const pub = ctx.age.agePublicFromPrivate(priv);
|
|
43
|
+
ctx.age.keySave(project, { private: priv, public: pub });
|
|
44
|
+
ctx.logger.log(pc.green(`Pulled key from 1Password for ${pc.cyan(project)}`));
|
|
51
45
|
return { publicKey: pub, source: '1password' };
|
|
52
46
|
}
|
|
53
|
-
function generateAndBackupKey(project) {
|
|
54
|
-
if (!ageAvailable()) {
|
|
55
|
-
|
|
47
|
+
function generateAndBackupKey(ctx, project) {
|
|
48
|
+
if (!ctx.age.ageAvailable()) {
|
|
49
|
+
ctx.logger.log(pc.yellow('age not installed. Run: brew install age'));
|
|
56
50
|
return null;
|
|
57
51
|
}
|
|
58
|
-
|
|
59
|
-
const key = ageGenerate();
|
|
60
|
-
keySave(project, key);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (opAvailable()) {
|
|
52
|
+
ctx.logger.log(pc.blue(`Generating new key for ${pc.cyan(project)}...`));
|
|
53
|
+
const key = ctx.age.ageGenerate();
|
|
54
|
+
ctx.age.keySave(project, key);
|
|
55
|
+
ctx.logger.log(pc.green(`Saved to ${ctx.age.keyPath(project)}`));
|
|
56
|
+
ctx.logger.log(pc.dim(`Public: ${key.public}`));
|
|
57
|
+
if (ctx.onepassword.opAvailable()) {
|
|
64
58
|
try {
|
|
65
|
-
opStoreKey(project, key.private, key.public);
|
|
66
|
-
|
|
59
|
+
ctx.onepassword.opStoreKey(project, key.private, key.public);
|
|
60
|
+
ctx.logger.log(pc.green('Backed up to 1Password.'));
|
|
67
61
|
}
|
|
68
62
|
catch (e) {
|
|
69
|
-
|
|
63
|
+
ctx.logger.warn(pc.yellow(`Could not backup to 1Password: ${e.message}`));
|
|
70
64
|
}
|
|
71
65
|
}
|
|
72
66
|
return { publicKey: key.public, source: 'generated' };
|
|
73
67
|
}
|
|
74
|
-
async function setupKey(root, project) {
|
|
68
|
+
async function setupKey(ctx, root, project) {
|
|
75
69
|
if (!project) {
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
ctx.logger.log(pc.yellow('No project identifier found. Skipping key setup.'));
|
|
71
|
+
ctx.logger.log(pc.dim('Add "project: my-org/my-repo" to hush.yaml or set repository in package.json'));
|
|
78
72
|
return null;
|
|
79
73
|
}
|
|
80
|
-
return ((await tryExistingLocalKey(project)) ||
|
|
81
|
-
(await tryPullFrom1Password(project)) ||
|
|
82
|
-
generateAndBackupKey(project));
|
|
74
|
+
return ((await tryExistingLocalKey(ctx, project)) ||
|
|
75
|
+
(await tryPullFrom1Password(ctx, project)) ||
|
|
76
|
+
generateAndBackupKey(ctx, project));
|
|
83
77
|
}
|
|
84
|
-
function createSopsConfig(root, publicKey) {
|
|
85
|
-
const sopsPath = join(root, '.sops.yaml');
|
|
86
|
-
if (existsSync(sopsPath)) {
|
|
87
|
-
|
|
88
|
-
|
|
78
|
+
function createSopsConfig(ctx, root, publicKey) {
|
|
79
|
+
const sopsPath = ctx.path.join(root, '.sops.yaml');
|
|
80
|
+
if (ctx.fs.existsSync(sopsPath)) {
|
|
81
|
+
ctx.logger.log(pc.yellow('.sops.yaml already exists. Add this public key if needed:'));
|
|
82
|
+
ctx.logger.log(` ${publicKey}`);
|
|
89
83
|
return;
|
|
90
84
|
}
|
|
91
85
|
const sopsConfig = stringifyYaml({
|
|
92
86
|
creation_rules: [{ encrypted_regex: '.*', age: publicKey }]
|
|
93
87
|
});
|
|
94
|
-
writeFileSync(sopsPath, sopsConfig, 'utf-8');
|
|
95
|
-
|
|
88
|
+
ctx.fs.writeFileSync(sopsPath, sopsConfig, 'utf-8');
|
|
89
|
+
ctx.logger.log(pc.green('Created .sops.yaml'));
|
|
96
90
|
}
|
|
97
|
-
function detectTargets(root) {
|
|
91
|
+
function detectTargets(ctx, root) {
|
|
98
92
|
const targets = [{ name: 'root', path: '.', format: 'dotenv' }];
|
|
99
|
-
const entries = readdirSync(root, { withFileTypes: true });
|
|
93
|
+
const entries = ctx.fs.readdirSync(root, { withFileTypes: true });
|
|
100
94
|
for (const entry of entries) {
|
|
101
95
|
if (!entry.isDirectory())
|
|
102
96
|
continue;
|
|
103
97
|
if (entry.name.startsWith('.') || entry.name === 'node_modules')
|
|
104
98
|
continue;
|
|
105
|
-
const dirPath = join(root, entry.name);
|
|
106
|
-
const packageJsonPath = join(dirPath, 'package.json');
|
|
107
|
-
const wranglerPath = join(dirPath, 'wrangler.toml');
|
|
108
|
-
if (!existsSync(packageJsonPath))
|
|
99
|
+
const dirPath = ctx.path.join(root, entry.name);
|
|
100
|
+
const packageJsonPath = ctx.path.join(dirPath, 'package.json');
|
|
101
|
+
const wranglerPath = ctx.path.join(dirPath, 'wrangler.toml');
|
|
102
|
+
if (!ctx.fs.existsSync(packageJsonPath))
|
|
109
103
|
continue;
|
|
110
|
-
if (existsSync(wranglerPath)) {
|
|
104
|
+
if (ctx.fs.existsSync(wranglerPath)) {
|
|
111
105
|
targets.push({
|
|
112
106
|
name: entry.name,
|
|
113
107
|
path: `./${entry.name}`,
|
|
@@ -125,67 +119,65 @@ function detectTargets(root) {
|
|
|
125
119
|
}
|
|
126
120
|
return targets;
|
|
127
121
|
}
|
|
128
|
-
function findExistingPlaintextEnvFiles(root) {
|
|
129
|
-
// Look for legacy .env files that may need migration
|
|
122
|
+
function findExistingPlaintextEnvFiles(ctx, root) {
|
|
130
123
|
const patterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
131
124
|
const found = [];
|
|
132
125
|
for (const pattern of patterns) {
|
|
133
|
-
const filePath = join(root, pattern);
|
|
134
|
-
if (existsSync(filePath)) {
|
|
126
|
+
const filePath = ctx.path.join(root, pattern);
|
|
127
|
+
if (ctx.fs.existsSync(filePath)) {
|
|
135
128
|
found.push(pattern);
|
|
136
129
|
}
|
|
137
130
|
}
|
|
138
131
|
return found;
|
|
139
132
|
}
|
|
140
|
-
function findExistingEncryptedFiles(root) {
|
|
141
|
-
// Look for v4 encrypted files that need migration to v5 (.hush.encrypted)
|
|
133
|
+
function findExistingEncryptedFiles(ctx, root) {
|
|
142
134
|
const patterns = ['.env.encrypted', '.env.development.encrypted', '.env.production.encrypted', '.env.local.encrypted'];
|
|
143
135
|
const found = [];
|
|
144
136
|
for (const pattern of patterns) {
|
|
145
|
-
const filePath = join(root, pattern);
|
|
146
|
-
if (existsSync(filePath)) {
|
|
137
|
+
const filePath = ctx.path.join(root, pattern);
|
|
138
|
+
if (ctx.fs.existsSync(filePath)) {
|
|
147
139
|
found.push(pattern);
|
|
148
140
|
}
|
|
149
141
|
}
|
|
150
142
|
return found;
|
|
151
143
|
}
|
|
152
|
-
export async function initCommand(options) {
|
|
144
|
+
export async function initCommand(ctx, options) {
|
|
153
145
|
const { root } = options;
|
|
154
|
-
const existingConfig =
|
|
146
|
+
const existingConfig = ctx.config.findProjectRoot(root);
|
|
155
147
|
if (existingConfig) {
|
|
156
|
-
|
|
157
|
-
process.exit(1);
|
|
148
|
+
ctx.logger.error(pc.red(`Config already exists: ${existingConfig.configPath}`));
|
|
149
|
+
ctx.process.exit(1);
|
|
158
150
|
}
|
|
159
|
-
|
|
160
|
-
const existingEncryptedFiles = findExistingEncryptedFiles(root);
|
|
151
|
+
ctx.logger.log(pc.blue('Initializing hush...\n'));
|
|
152
|
+
const existingEncryptedFiles = findExistingEncryptedFiles(ctx, root);
|
|
161
153
|
if (existingEncryptedFiles.length > 0) {
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
ctx.logger.log(pc.bgYellow(pc.black(' V4 ENCRYPTED FILES DETECTED ')));
|
|
155
|
+
ctx.logger.log(pc.yellow('\nFound existing v4 encrypted files:'));
|
|
164
156
|
for (const file of existingEncryptedFiles) {
|
|
165
|
-
|
|
157
|
+
ctx.logger.log(pc.yellow(` ${file}`));
|
|
166
158
|
}
|
|
167
|
-
|
|
159
|
+
ctx.logger.log(pc.dim('\nRun "npx hush migrate" to convert to v5 format (.hush.encrypted).\n'));
|
|
168
160
|
}
|
|
169
|
-
const existingEnvFiles = findExistingPlaintextEnvFiles(root);
|
|
161
|
+
const existingEnvFiles = findExistingPlaintextEnvFiles(ctx, root);
|
|
170
162
|
if (existingEnvFiles.length > 0) {
|
|
171
|
-
|
|
172
|
-
|
|
163
|
+
ctx.logger.log(pc.bgYellow(pc.black(' PLAINTEXT .ENV FILES DETECTED ')));
|
|
164
|
+
ctx.logger.log(pc.yellow('\nFound existing .env files:'));
|
|
173
165
|
for (const file of existingEnvFiles) {
|
|
174
|
-
|
|
166
|
+
ctx.logger.log(pc.yellow(` ${file}`));
|
|
175
167
|
}
|
|
176
|
-
|
|
177
|
-
|
|
168
|
+
ctx.logger.log(pc.dim('\nRename these to .hush files, then run "npx hush encrypt".\n'));
|
|
169
|
+
ctx.logger.log(pc.dim('Example: mv .env .hush && mv .env.development .hush.development\n'));
|
|
178
170
|
}
|
|
179
|
-
const project = getProjectFromPackageJson(root);
|
|
171
|
+
const project = getProjectFromPackageJson(ctx, root);
|
|
180
172
|
if (!project) {
|
|
181
|
-
|
|
182
|
-
|
|
173
|
+
ctx.logger.log(pc.yellow('No project identifier found in package.json.'));
|
|
174
|
+
ctx.logger.log(pc.dim('Tip: Add "project: my-org/my-repo" to hush.yaml after creation for key management.\n'));
|
|
183
175
|
}
|
|
184
|
-
const keyResult = await setupKey(root, project);
|
|
176
|
+
const keyResult = await setupKey(ctx, root, project);
|
|
185
177
|
if (keyResult) {
|
|
186
|
-
createSopsConfig(root, keyResult.publicKey);
|
|
178
|
+
createSopsConfig(ctx, root, keyResult.publicKey);
|
|
187
179
|
}
|
|
188
|
-
const targets = detectTargets(root);
|
|
180
|
+
const targets = detectTargets(ctx, root);
|
|
189
181
|
const config = {
|
|
190
182
|
version: 2,
|
|
191
183
|
sources: DEFAULT_SOURCES,
|
|
@@ -194,29 +186,29 @@ export async function initCommand(options) {
|
|
|
194
186
|
};
|
|
195
187
|
const yaml = stringifyYaml(config, { indent: 2 });
|
|
196
188
|
const schemaComment = '# yaml-language-server: $schema=https://unpkg.com/@chriscode/hush/schema.json\n';
|
|
197
|
-
const configPath = join(root, 'hush.yaml');
|
|
198
|
-
writeFileSync(configPath, schemaComment + yaml, 'utf-8');
|
|
199
|
-
|
|
200
|
-
|
|
189
|
+
const configPath = ctx.path.join(root, 'hush.yaml');
|
|
190
|
+
ctx.fs.writeFileSync(configPath, schemaComment + yaml, 'utf-8');
|
|
191
|
+
ctx.logger.log(pc.green(`\nCreated ${configPath}`));
|
|
192
|
+
ctx.logger.log(pc.dim('\nDetected targets:'));
|
|
201
193
|
for (const target of targets) {
|
|
202
|
-
|
|
194
|
+
ctx.logger.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
|
|
203
195
|
}
|
|
204
|
-
|
|
196
|
+
ctx.logger.log(pc.bold('\nNext steps:'));
|
|
205
197
|
if (existingEncryptedFiles.length > 0) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
198
|
+
ctx.logger.log(pc.green(' 1. npx hush migrate') + pc.dim(' # Convert v4 .env.encrypted to v5 .hush.encrypted'));
|
|
199
|
+
ctx.logger.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
|
|
200
|
+
ctx.logger.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
|
|
209
201
|
}
|
|
210
202
|
else if (existingEnvFiles.length > 0) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
203
|
+
ctx.logger.log(pc.green(' 1. Rename .env files to .hush') + pc.dim(' # mv .env .hush'));
|
|
204
|
+
ctx.logger.log(pc.dim(' 2. npx hush encrypt') + pc.dim(' # Encrypt .hush files'));
|
|
205
|
+
ctx.logger.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
|
|
214
206
|
}
|
|
215
207
|
else {
|
|
216
|
-
|
|
217
|
-
|
|
208
|
+
ctx.logger.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
|
|
209
|
+
ctx.logger.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
|
|
218
210
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
211
|
+
ctx.logger.log(pc.dim('\nGit setup:'));
|
|
212
|
+
ctx.logger.log(pc.dim(' git add hush.yaml .sops.yaml'));
|
|
213
|
+
ctx.logger.log(pc.dim(' git commit -m "chore: add Hush secrets management"'));
|
|
222
214
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Environment } from '../types.js';
|
|
1
|
+
import type { Environment, HushContext } from '../types.js';
|
|
2
2
|
export interface InspectOptions {
|
|
3
3
|
root: string;
|
|
4
4
|
env: Environment;
|
|
5
5
|
}
|
|
6
|
-
export declare function inspectCommand(options: InspectOptions): Promise<void>;
|
|
6
|
+
export declare function inspectCommand(ctx: HushContext, options: InspectOptions): Promise<void>;
|
|
7
7
|
//# sourceMappingURL=inspect.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/commands/inspect.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAU,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEpE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;CAClB;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAqD7F"}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fs } from '../lib/fs.js';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { loadConfig } from '../config/loader.js';
|
|
5
4
|
import { filterVarsForTarget, describeFilter } from '../core/filter.js';
|
|
6
5
|
import { interpolateVars } from '../core/interpolate.js';
|
|
7
6
|
import { maskVars, formatMaskedVar } from '../core/mask.js';
|
|
8
7
|
import { mergeVars } from '../core/merge.js';
|
|
9
8
|
import { parseEnvContent } from '../core/parse.js';
|
|
10
|
-
|
|
11
|
-
export async function inspectCommand(options) {
|
|
9
|
+
export async function inspectCommand(ctx, options) {
|
|
12
10
|
const { root, env } = options;
|
|
13
|
-
const config = loadConfig(root);
|
|
11
|
+
const config = ctx.config.loadConfig(root);
|
|
14
12
|
const sharedEncrypted = join(root, config.sources.shared + '.encrypted');
|
|
15
13
|
const envEncrypted = join(root, config.sources[env] + '.encrypted');
|
|
16
14
|
const varSources = [];
|
|
17
|
-
if (existsSync(sharedEncrypted)) {
|
|
18
|
-
const content =
|
|
15
|
+
if (fs.existsSync(sharedEncrypted)) {
|
|
16
|
+
const content = ctx.sops.decrypt(sharedEncrypted);
|
|
19
17
|
varSources.push(parseEnvContent(content));
|
|
20
18
|
}
|
|
21
|
-
if (existsSync(envEncrypted)) {
|
|
22
|
-
const content =
|
|
19
|
+
if (fs.existsSync(envEncrypted)) {
|
|
20
|
+
const content = ctx.sops.decrypt(envEncrypted);
|
|
23
21
|
varSources.push(parseEnvContent(content));
|
|
24
22
|
}
|
|
25
23
|
if (varSources.length === 0) {
|
|
@@ -31,20 +29,20 @@ export async function inspectCommand(options) {
|
|
|
31
29
|
const interpolated = interpolateVars(merged);
|
|
32
30
|
const masked = maskVars(interpolated);
|
|
33
31
|
const maxKeyLen = Math.max(...masked.map(v => v.key.length));
|
|
34
|
-
|
|
32
|
+
ctx.logger.log(pc.blue(`\nSecrets for ${env}:\n`));
|
|
35
33
|
for (const v of masked) {
|
|
36
34
|
const line = formatMaskedVar(v, maxKeyLen);
|
|
37
|
-
|
|
35
|
+
ctx.logger.log(` ${v.isSet ? pc.green(v.key.padEnd(maxKeyLen)) : pc.yellow(v.key.padEnd(maxKeyLen))} = ${v.isSet ? pc.dim(v.masked + ` (${v.length} chars)`) : pc.yellow('(not set)')}`);
|
|
38
36
|
}
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
ctx.logger.log(pc.dim(`\nTotal: ${masked.length} variables\n`));
|
|
38
|
+
ctx.logger.log(pc.blue('Target distribution:\n'));
|
|
41
39
|
for (const target of config.targets) {
|
|
42
40
|
const filtered = filterVarsForTarget(interpolated, target);
|
|
43
41
|
const filter = describeFilter(target);
|
|
44
|
-
|
|
42
|
+
ctx.logger.log(` ${pc.cyan(target.name)} ${pc.dim(`(${target.path}/)`)} - ${filtered.length} vars`);
|
|
45
43
|
if (filter !== 'all vars') {
|
|
46
|
-
|
|
44
|
+
ctx.logger.log(` ${pc.dim(filter)}`);
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
|
-
|
|
47
|
+
ctx.logger.log('');
|
|
50
48
|
}
|
package/dist/commands/keys.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { HushContext } from '../types.js';
|
|
1
2
|
export interface KeysOptions {
|
|
2
3
|
root: string;
|
|
3
4
|
subcommand: string;
|
|
4
5
|
vault?: string;
|
|
5
6
|
force?: boolean;
|
|
6
7
|
}
|
|
7
|
-
export declare function keysCommand(options: KeysOptions): Promise<void>;
|
|
8
|
+
export declare function keysCommand(ctx: HushContext, options: KeysOptions): Promise<void>;
|
|
8
9
|
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/commands/keys.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/commands/keys.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI1C,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,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6HvF"}
|
package/dist/commands/keys.js
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
2
|
import pc from 'picocolors';
|
|
4
3
|
import { stringify as yamlStringify } from 'yaml';
|
|
5
|
-
import { loadConfig } from '../config/loader.js';
|
|
6
4
|
import { opAvailable, opGetKey, opStoreKey, opListKeys } from '../lib/onepassword.js';
|
|
7
5
|
import { ageAvailable, ageGenerate, agePublicFromPrivate, keyExists, keySave, keyLoad, keysList, keyPath } from '../lib/age.js';
|
|
8
|
-
function getProject(root) {
|
|
9
|
-
const config = loadConfig(root);
|
|
6
|
+
function getProject(ctx, root) {
|
|
7
|
+
const config = ctx.config.loadConfig(root);
|
|
10
8
|
if (config.project)
|
|
11
9
|
return config.project;
|
|
12
10
|
const pkgPath = join(root, 'package.json');
|
|
13
|
-
if (existsSync(pkgPath)) {
|
|
14
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
11
|
+
if (ctx.fs.existsSync(pkgPath)) {
|
|
12
|
+
const pkg = JSON.parse(ctx.fs.readFileSync(pkgPath, 'utf-8'));
|
|
15
13
|
if (typeof pkg.repository === 'string') {
|
|
16
14
|
const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
17
15
|
if (match)
|
|
@@ -23,18 +21,18 @@ function getProject(root) {
|
|
|
23
21
|
return match[1];
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
process.exit(1);
|
|
24
|
+
ctx.logger.error(pc.red('No project identifier found.'));
|
|
25
|
+
ctx.logger.error(pc.dim('Add "project: my-project" to hush.yaml'));
|
|
26
|
+
ctx.process.exit(1);
|
|
29
27
|
}
|
|
30
|
-
export async function keysCommand(options) {
|
|
28
|
+
export async function keysCommand(ctx, options) {
|
|
31
29
|
const { root, subcommand, vault, force } = options;
|
|
32
30
|
switch (subcommand) {
|
|
33
31
|
case 'setup': {
|
|
34
|
-
const project = getProject(root);
|
|
35
|
-
|
|
32
|
+
const project = getProject(ctx, root);
|
|
33
|
+
ctx.logger.log(pc.blue(`Setting up keys for ${pc.cyan(project)}...`));
|
|
36
34
|
if (keyExists(project)) {
|
|
37
|
-
|
|
35
|
+
ctx.logger.log(pc.green('Key already exists locally.'));
|
|
38
36
|
return;
|
|
39
37
|
}
|
|
40
38
|
if (opAvailable()) {
|
|
@@ -42,95 +40,95 @@ export async function keysCommand(options) {
|
|
|
42
40
|
if (priv) {
|
|
43
41
|
const pub = agePublicFromPrivate(priv);
|
|
44
42
|
keySave(project, { private: priv, public: pub });
|
|
45
|
-
|
|
43
|
+
ctx.logger.log(pc.green('Pulled key from 1Password.'));
|
|
46
44
|
return;
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
|
-
|
|
47
|
+
ctx.logger.log(pc.yellow('No key found. Run "hush keys generate" to create one.'));
|
|
50
48
|
break;
|
|
51
49
|
}
|
|
52
50
|
case 'generate': {
|
|
53
51
|
if (!ageAvailable()) {
|
|
54
|
-
|
|
55
|
-
process.exit(1);
|
|
52
|
+
ctx.logger.error(pc.red('age not installed. Run: brew install age'));
|
|
53
|
+
ctx.process.exit(1);
|
|
56
54
|
}
|
|
57
|
-
const project = getProject(root);
|
|
55
|
+
const project = getProject(ctx, root);
|
|
58
56
|
if (keyExists(project) && !force) {
|
|
59
|
-
|
|
60
|
-
process.exit(1);
|
|
57
|
+
ctx.logger.error(pc.yellow(`Key exists for ${project}. Use --force to overwrite.`));
|
|
58
|
+
ctx.process.exit(1);
|
|
61
59
|
}
|
|
62
|
-
|
|
60
|
+
ctx.logger.log(pc.blue(`Generating key for ${pc.cyan(project)}...`));
|
|
63
61
|
const key = ageGenerate();
|
|
64
62
|
keySave(project, key);
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
ctx.logger.log(pc.green(`Saved to ${keyPath(project)}`));
|
|
64
|
+
ctx.logger.log(pc.dim(`Public: ${key.public}`));
|
|
67
65
|
if (opAvailable()) {
|
|
68
66
|
try {
|
|
69
67
|
opStoreKey(project, key.private, key.public, vault);
|
|
70
|
-
|
|
68
|
+
ctx.logger.log(pc.green('Stored in 1Password.'));
|
|
71
69
|
}
|
|
72
70
|
catch (e) {
|
|
73
|
-
|
|
71
|
+
ctx.logger.warn(pc.yellow(`Could not store in 1Password: ${e.message}`));
|
|
74
72
|
}
|
|
75
73
|
}
|
|
76
74
|
const sopsPath = join(root, '.sops.yaml');
|
|
77
|
-
if (!existsSync(sopsPath)) {
|
|
78
|
-
writeFileSync(sopsPath, yamlStringify({ creation_rules: [{ encrypted_regex: '.*', age: key.public }] }));
|
|
79
|
-
|
|
75
|
+
if (!ctx.fs.existsSync(sopsPath)) {
|
|
76
|
+
ctx.fs.writeFileSync(sopsPath, yamlStringify({ creation_rules: [{ encrypted_regex: '.*', age: key.public }] }));
|
|
77
|
+
ctx.logger.log(pc.green('Created .sops.yaml'));
|
|
80
78
|
}
|
|
81
79
|
else {
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
ctx.logger.log(pc.yellow('.sops.yaml exists. Add this public key:'));
|
|
81
|
+
ctx.logger.log(` ${key.public}`);
|
|
84
82
|
}
|
|
85
83
|
break;
|
|
86
84
|
}
|
|
87
85
|
case 'pull': {
|
|
88
86
|
if (!opAvailable()) {
|
|
89
|
-
|
|
90
|
-
process.exit(1);
|
|
87
|
+
ctx.logger.error(pc.red('1Password CLI not available or not signed in.'));
|
|
88
|
+
ctx.process.exit(1);
|
|
91
89
|
}
|
|
92
|
-
const project = getProject(root);
|
|
90
|
+
const project = getProject(ctx, root);
|
|
93
91
|
const priv = opGetKey(project, vault);
|
|
94
92
|
if (!priv) {
|
|
95
|
-
|
|
96
|
-
process.exit(1);
|
|
93
|
+
ctx.logger.error(pc.red(`No key in 1Password for ${project}`));
|
|
94
|
+
ctx.process.exit(1);
|
|
97
95
|
}
|
|
98
96
|
const pub = agePublicFromPrivate(priv);
|
|
99
97
|
keySave(project, { private: priv, public: pub });
|
|
100
|
-
|
|
98
|
+
ctx.logger.log(pc.green(`Pulled and saved to ${keyPath(project)}`));
|
|
101
99
|
break;
|
|
102
100
|
}
|
|
103
101
|
case 'push': {
|
|
104
102
|
if (!opAvailable()) {
|
|
105
|
-
|
|
106
|
-
process.exit(1);
|
|
103
|
+
ctx.logger.error(pc.red('1Password CLI not available or not signed in.'));
|
|
104
|
+
ctx.process.exit(1);
|
|
107
105
|
}
|
|
108
|
-
const project = getProject(root);
|
|
106
|
+
const project = getProject(ctx, root);
|
|
109
107
|
const key = keyLoad(project);
|
|
110
108
|
if (!key) {
|
|
111
|
-
|
|
112
|
-
process.exit(1);
|
|
109
|
+
ctx.logger.error(pc.red(`No local key for ${project}`));
|
|
110
|
+
ctx.process.exit(1);
|
|
113
111
|
}
|
|
114
112
|
opStoreKey(project, key.private, key.public, vault);
|
|
115
|
-
|
|
113
|
+
ctx.logger.log(pc.green('Pushed to 1Password.'));
|
|
116
114
|
break;
|
|
117
115
|
}
|
|
118
116
|
case 'list': {
|
|
119
|
-
|
|
117
|
+
ctx.logger.log(pc.blue('Local keys:'));
|
|
120
118
|
for (const k of keysList()) {
|
|
121
|
-
|
|
119
|
+
ctx.logger.log(` ${pc.cyan(k.project)} ${pc.dim(k.public.slice(0, 20))}...`);
|
|
122
120
|
}
|
|
123
121
|
if (opAvailable()) {
|
|
124
|
-
|
|
122
|
+
ctx.logger.log(pc.blue('\n1Password keys:'));
|
|
125
123
|
for (const project of opListKeys(vault)) {
|
|
126
|
-
|
|
124
|
+
ctx.logger.log(` ${pc.cyan(project)}`);
|
|
127
125
|
}
|
|
128
126
|
}
|
|
129
127
|
break;
|
|
130
128
|
}
|
|
131
129
|
default:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
process.exit(1);
|
|
130
|
+
ctx.logger.error(pc.red(`Unknown: hush keys ${subcommand}`));
|
|
131
|
+
ctx.logger.log(pc.dim('Commands: setup, generate, pull, push, list'));
|
|
132
|
+
ctx.process.exit(1);
|
|
135
133
|
}
|
|
136
134
|
}
|
package/dist/commands/list.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ListOptions } from '../types.js';
|
|
2
|
-
export declare function listCommand(options: ListOptions): Promise<void>;
|
|
1
|
+
import type { ListOptions, HushContext } from '../types.js';
|
|
2
|
+
export declare function listCommand(ctx: HushContext, options: ListOptions): Promise<void>;
|
|
3
3
|
//# sourceMappingURL=list.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAU,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAEpE,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCvF"}
|
package/dist/commands/list.js
CHANGED
|
@@ -1,35 +1,32 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
2
|
import pc from 'picocolors';
|
|
4
|
-
import { loadConfig } from '../config/loader.js';
|
|
5
3
|
import { interpolateVars } from '../core/interpolate.js';
|
|
6
4
|
import { mergeVars } from '../core/merge.js';
|
|
7
5
|
import { parseEnvContent } from '../core/parse.js';
|
|
8
|
-
|
|
9
|
-
export async function listCommand(options) {
|
|
6
|
+
export async function listCommand(ctx, options) {
|
|
10
7
|
const { root, env } = options;
|
|
11
|
-
const config = loadConfig(root);
|
|
12
|
-
|
|
8
|
+
const config = ctx.config.loadConfig(root);
|
|
9
|
+
ctx.logger.log(pc.blue(`Variables for ${env}:\n`));
|
|
13
10
|
const sharedEncrypted = join(root, config.sources.shared + '.encrypted');
|
|
14
11
|
const envEncrypted = join(root, config.sources[env] + '.encrypted');
|
|
15
12
|
const varSources = [];
|
|
16
|
-
if (existsSync(sharedEncrypted)) {
|
|
17
|
-
const content =
|
|
13
|
+
if (ctx.fs.existsSync(sharedEncrypted)) {
|
|
14
|
+
const content = ctx.sops.decrypt(sharedEncrypted);
|
|
18
15
|
varSources.push(parseEnvContent(content));
|
|
19
16
|
}
|
|
20
|
-
if (existsSync(envEncrypted)) {
|
|
21
|
-
const content =
|
|
17
|
+
if (ctx.fs.existsSync(envEncrypted)) {
|
|
18
|
+
const content = ctx.sops.decrypt(envEncrypted);
|
|
22
19
|
varSources.push(parseEnvContent(content));
|
|
23
20
|
}
|
|
24
21
|
if (varSources.length === 0) {
|
|
25
|
-
|
|
26
|
-
process.exit(1);
|
|
22
|
+
ctx.logger.error(pc.red('No encrypted files found'));
|
|
23
|
+
ctx.process.exit(1);
|
|
27
24
|
}
|
|
28
25
|
const merged = mergeVars(...varSources);
|
|
29
26
|
const interpolated = interpolateVars(merged);
|
|
30
27
|
for (const { key, value } of interpolated) {
|
|
31
28
|
const displayValue = value.length > 50 ? value.slice(0, 47) + '...' : value;
|
|
32
|
-
|
|
29
|
+
ctx.logger.log(`${pc.cyan(key)}=${pc.dim(displayValue)}`);
|
|
33
30
|
}
|
|
34
|
-
|
|
31
|
+
ctx.logger.log(pc.dim(`\nTotal: ${interpolated.length} variables`));
|
|
35
32
|
}
|