@chriscode/hush 4.2.0 → 5.0.1
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 +58 -29
- package/dist/commands/check.d.ts +3 -3
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +30 -33
- 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 +107 -87
- 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 +7 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +117 -0
- 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 +186 -487
- package/dist/commands/status.d.ts +2 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +52 -55
- 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 +59 -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 +91 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -4
- 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,44 +119,65 @@ function detectTargets(root) {
|
|
|
125
119
|
}
|
|
126
120
|
return targets;
|
|
127
121
|
}
|
|
128
|
-
function findExistingPlaintextEnvFiles(root) {
|
|
122
|
+
function findExistingPlaintextEnvFiles(ctx, root) {
|
|
129
123
|
const patterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
130
124
|
const found = [];
|
|
131
125
|
for (const pattern of patterns) {
|
|
132
|
-
const filePath = join(root, pattern);
|
|
133
|
-
if (existsSync(filePath)) {
|
|
126
|
+
const filePath = ctx.path.join(root, pattern);
|
|
127
|
+
if (ctx.fs.existsSync(filePath)) {
|
|
134
128
|
found.push(pattern);
|
|
135
129
|
}
|
|
136
130
|
}
|
|
137
131
|
return found;
|
|
138
132
|
}
|
|
139
|
-
|
|
133
|
+
function findExistingEncryptedFiles(ctx, root) {
|
|
134
|
+
const patterns = ['.env.encrypted', '.env.development.encrypted', '.env.production.encrypted', '.env.local.encrypted'];
|
|
135
|
+
const found = [];
|
|
136
|
+
for (const pattern of patterns) {
|
|
137
|
+
const filePath = ctx.path.join(root, pattern);
|
|
138
|
+
if (ctx.fs.existsSync(filePath)) {
|
|
139
|
+
found.push(pattern);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return found;
|
|
143
|
+
}
|
|
144
|
+
export async function initCommand(ctx, options) {
|
|
140
145
|
const { root } = options;
|
|
141
|
-
const existingConfig =
|
|
146
|
+
const existingConfig = ctx.config.findProjectRoot(root);
|
|
142
147
|
if (existingConfig) {
|
|
143
|
-
|
|
144
|
-
process.exit(1);
|
|
148
|
+
ctx.logger.error(pc.red(`Config already exists: ${existingConfig.configPath}`));
|
|
149
|
+
ctx.process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
ctx.logger.log(pc.blue('Initializing hush...\n'));
|
|
152
|
+
const existingEncryptedFiles = findExistingEncryptedFiles(ctx, root);
|
|
153
|
+
if (existingEncryptedFiles.length > 0) {
|
|
154
|
+
ctx.logger.log(pc.bgYellow(pc.black(' V4 ENCRYPTED FILES DETECTED ')));
|
|
155
|
+
ctx.logger.log(pc.yellow('\nFound existing v4 encrypted files:'));
|
|
156
|
+
for (const file of existingEncryptedFiles) {
|
|
157
|
+
ctx.logger.log(pc.yellow(` ${file}`));
|
|
158
|
+
}
|
|
159
|
+
ctx.logger.log(pc.dim('\nRun "npx hush migrate" to convert to v5 format (.hush.encrypted).\n'));
|
|
145
160
|
}
|
|
146
|
-
|
|
147
|
-
const existingEnvFiles = findExistingPlaintextEnvFiles(root);
|
|
161
|
+
const existingEnvFiles = findExistingPlaintextEnvFiles(ctx, root);
|
|
148
162
|
if (existingEnvFiles.length > 0) {
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
ctx.logger.log(pc.bgYellow(pc.black(' PLAINTEXT .ENV FILES DETECTED ')));
|
|
164
|
+
ctx.logger.log(pc.yellow('\nFound existing .env files:'));
|
|
151
165
|
for (const file of existingEnvFiles) {
|
|
152
|
-
|
|
166
|
+
ctx.logger.log(pc.yellow(` ${file}`));
|
|
153
167
|
}
|
|
154
|
-
|
|
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'));
|
|
155
170
|
}
|
|
156
|
-
const project = getProjectFromPackageJson(root);
|
|
171
|
+
const project = getProjectFromPackageJson(ctx, root);
|
|
157
172
|
if (!project) {
|
|
158
|
-
|
|
159
|
-
|
|
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'));
|
|
160
175
|
}
|
|
161
|
-
const keyResult = await setupKey(root, project);
|
|
176
|
+
const keyResult = await setupKey(ctx, root, project);
|
|
162
177
|
if (keyResult) {
|
|
163
|
-
createSopsConfig(root, keyResult.publicKey);
|
|
178
|
+
createSopsConfig(ctx, root, keyResult.publicKey);
|
|
164
179
|
}
|
|
165
|
-
const targets = detectTargets(root);
|
|
180
|
+
const targets = detectTargets(ctx, root);
|
|
166
181
|
const config = {
|
|
167
182
|
version: 2,
|
|
168
183
|
sources: DEFAULT_SOURCES,
|
|
@@ -171,24 +186,29 @@ export async function initCommand(options) {
|
|
|
171
186
|
};
|
|
172
187
|
const yaml = stringifyYaml(config, { indent: 2 });
|
|
173
188
|
const schemaComment = '# yaml-language-server: $schema=https://unpkg.com/@chriscode/hush/schema.json\n';
|
|
174
|
-
const configPath = join(root, 'hush.yaml');
|
|
175
|
-
writeFileSync(configPath, schemaComment + yaml, 'utf-8');
|
|
176
|
-
|
|
177
|
-
|
|
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:'));
|
|
178
193
|
for (const target of targets) {
|
|
179
|
-
|
|
194
|
+
ctx.logger.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
|
|
180
195
|
}
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
ctx.logger.log(pc.bold('\nNext steps:'));
|
|
197
|
+
if (existingEncryptedFiles.length > 0) {
|
|
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'));
|
|
201
|
+
}
|
|
202
|
+
else if (existingEnvFiles.length > 0) {
|
|
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'));
|
|
186
206
|
}
|
|
187
207
|
else {
|
|
188
|
-
|
|
189
|
-
|
|
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'));
|
|
190
210
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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"'));
|
|
194
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
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrate.d.ts","sourceRoot":"","sources":["../../src/commands/migrate.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AA+DD,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA4E7F"}
|