@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
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { StatusOptions } from '../types.js';
|
|
2
|
-
export declare function statusCommand(options: StatusOptions): Promise<void>;
|
|
1
|
+
import type { HushContext, StatusOptions } from '../types.js';
|
|
2
|
+
export declare function statusCommand(ctx: HushContext, options: StatusOptions): Promise<void>;
|
|
3
3
|
//# sourceMappingURL=status.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAyC9D,wBAAsB,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAoH3F"}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,32 +1,27 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
2
|
import pc from 'picocolors';
|
|
4
|
-
import { findConfigPath, loadConfig } from '../config/loader.js';
|
|
5
3
|
import { describeFilter } from '../core/filter.js';
|
|
6
|
-
import { isAgeKeyConfigured, isSopsInstalled } from '../core/sops.js';
|
|
7
|
-
import { keyExists } from '../lib/age.js';
|
|
8
|
-
import { opInstalled } from '../lib/onepassword.js';
|
|
9
4
|
import { FORMAT_OUTPUT_FILES } from '../types.js';
|
|
10
|
-
function findRootPlaintextEnvFiles(root) {
|
|
5
|
+
function findRootPlaintextEnvFiles(ctx, root) {
|
|
11
6
|
const results = [];
|
|
12
7
|
// Only warn about .env files (legacy/output), not .hush files (Hush's source files)
|
|
13
8
|
const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
14
9
|
for (const pattern of plaintextPatterns) {
|
|
15
10
|
const filePath = join(root, pattern);
|
|
16
|
-
if (existsSync(filePath)) {
|
|
11
|
+
if (ctx.fs.existsSync(filePath)) {
|
|
17
12
|
results.push(pattern);
|
|
18
13
|
}
|
|
19
14
|
}
|
|
20
15
|
return results;
|
|
21
16
|
}
|
|
22
|
-
function getProjectFromConfig(root) {
|
|
23
|
-
const config = loadConfig(root);
|
|
17
|
+
function getProjectFromConfig(ctx, root) {
|
|
18
|
+
const config = ctx.config.loadConfig(root);
|
|
24
19
|
if (config.project)
|
|
25
20
|
return config.project;
|
|
26
21
|
const pkgPath = join(root, 'package.json');
|
|
27
|
-
if (existsSync(pkgPath)) {
|
|
22
|
+
if (ctx.fs.existsSync(pkgPath)) {
|
|
28
23
|
try {
|
|
29
|
-
const pkg = JSON.parse(
|
|
24
|
+
const pkg = JSON.parse(ctx.fs.readFileSync(pkgPath, 'utf-8'));
|
|
30
25
|
if (typeof pkg.repository === 'string') {
|
|
31
26
|
const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
32
27
|
if (match)
|
|
@@ -44,68 +39,69 @@ function getProjectFromConfig(root) {
|
|
|
44
39
|
}
|
|
45
40
|
return null;
|
|
46
41
|
}
|
|
47
|
-
export async function statusCommand(options) {
|
|
42
|
+
export async function statusCommand(ctx, options) {
|
|
48
43
|
const { root } = options;
|
|
49
|
-
const config = loadConfig(root);
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
const config = ctx.config.loadConfig(root);
|
|
45
|
+
const projectRootResult = ctx.config.findProjectRoot(root);
|
|
46
|
+
const configPath = projectRootResult ? projectRootResult.configPath : null;
|
|
47
|
+
ctx.logger.log(pc.blue('Hush Status\n'));
|
|
48
|
+
const plaintextFiles = findRootPlaintextEnvFiles(ctx, root);
|
|
53
49
|
if (plaintextFiles.length > 0) {
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
ctx.logger.log(pc.bgRed(pc.white(pc.bold(' SECURITY WARNING '))));
|
|
51
|
+
ctx.logger.log(pc.red(pc.bold('\nUnencrypted .env files detected at project root!\n')));
|
|
56
52
|
for (const file of plaintextFiles) {
|
|
57
|
-
|
|
53
|
+
ctx.logger.log(pc.red(` ${file}`));
|
|
58
54
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
ctx.logger.log('');
|
|
56
|
+
ctx.logger.log(pc.yellow('These files may expose secrets to AI assistants and version control.'));
|
|
57
|
+
ctx.logger.log(pc.bold('\nTo fix:'));
|
|
58
|
+
ctx.logger.log(pc.dim(' 1. Run: npx hush migrate (if upgrading from v4)'));
|
|
59
|
+
ctx.logger.log(pc.dim(' 2. Delete or gitignore these .env files'));
|
|
60
|
+
ctx.logger.log(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars\n'));
|
|
65
61
|
}
|
|
66
|
-
|
|
62
|
+
ctx.logger.log(pc.bold('Config:'));
|
|
67
63
|
if (configPath) {
|
|
68
|
-
|
|
64
|
+
ctx.logger.log(pc.green(` ${configPath.replace(root + '/', '')}`));
|
|
69
65
|
}
|
|
70
66
|
else {
|
|
71
|
-
|
|
67
|
+
ctx.logger.log(pc.dim(' No hush.yaml found (using defaults)'));
|
|
72
68
|
}
|
|
73
|
-
const project = getProjectFromConfig(root);
|
|
69
|
+
const project = getProjectFromConfig(ctx, root);
|
|
74
70
|
if (configPath) {
|
|
75
71
|
if (project) {
|
|
76
|
-
|
|
72
|
+
ctx.logger.log(pc.green(` Project: ${project}`));
|
|
77
73
|
}
|
|
78
74
|
else {
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
ctx.logger.log(pc.yellow(' Project: not set'));
|
|
76
|
+
ctx.logger.log(pc.dim(' Add "project: my-org/my-repo" to hush.yaml for key management'));
|
|
81
77
|
}
|
|
82
78
|
}
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
ctx.logger.log(pc.bold('\nPrerequisites:'));
|
|
80
|
+
ctx.logger.log(ctx.sops.isSopsInstalled()
|
|
85
81
|
? pc.green(' SOPS installed')
|
|
86
82
|
: pc.red(' SOPS not installed (brew install sops)'));
|
|
87
|
-
|
|
83
|
+
ctx.logger.log(ctx.age.ageAvailable()
|
|
88
84
|
? pc.green(' age key configured')
|
|
89
85
|
: pc.yellow(' age key not found at ~/.config/sops/age/key.txt'));
|
|
90
86
|
if (project) {
|
|
91
|
-
const hasLocalKey = keyExists(project);
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
const hasLocalKey = ctx.age.keyExists(project);
|
|
88
|
+
ctx.logger.log(pc.bold('\nKey Status:'));
|
|
89
|
+
ctx.logger.log(hasLocalKey
|
|
94
90
|
? pc.green(` Local key: ~/.config/sops/age/keys/${project.replace(/\//g, '-')}.txt`)
|
|
95
91
|
: pc.yellow(' Local key: not found'));
|
|
96
|
-
if (opInstalled()) {
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
if (ctx.onepassword.opInstalled()) {
|
|
93
|
+
ctx.logger.log(pc.dim(' 1Password CLI: installed'));
|
|
94
|
+
ctx.logger.log(pc.dim(' Run "npx hush keys list" to check backup status'));
|
|
99
95
|
}
|
|
100
96
|
else {
|
|
101
|
-
|
|
97
|
+
ctx.logger.log(pc.dim(' 1Password CLI: not installed'));
|
|
102
98
|
}
|
|
103
99
|
if (!hasLocalKey) {
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
ctx.logger.log(pc.bold('\n To set up keys:'));
|
|
101
|
+
ctx.logger.log(pc.dim(' npx hush keys setup # Pull from 1Password or generate'));
|
|
106
102
|
}
|
|
107
103
|
}
|
|
108
|
-
|
|
104
|
+
ctx.logger.log(pc.bold('\nSource Files:'));
|
|
109
105
|
const sources = [
|
|
110
106
|
{ key: 'shared', path: config.sources.shared },
|
|
111
107
|
{ key: 'development', path: config.sources.development },
|
|
@@ -113,25 +109,25 @@ export async function statusCommand(options) {
|
|
|
113
109
|
];
|
|
114
110
|
for (const { key, path } of sources) {
|
|
115
111
|
const encryptedPath = join(root, path + '.encrypted');
|
|
116
|
-
const exists = existsSync(encryptedPath);
|
|
112
|
+
const exists = ctx.fs.existsSync(encryptedPath);
|
|
117
113
|
const label = `${path}.encrypted`;
|
|
118
|
-
|
|
114
|
+
ctx.logger.log(exists
|
|
119
115
|
? pc.green(` ${label}`)
|
|
120
116
|
: pc.dim(` ${label} (not found)`));
|
|
121
117
|
}
|
|
122
118
|
const localEncryptedPath = join(root, config.sources.local + '.encrypted');
|
|
123
|
-
|
|
119
|
+
ctx.logger.log(ctx.fs.existsSync(localEncryptedPath)
|
|
124
120
|
? pc.green(` ${config.sources.local}.encrypted (overrides)`)
|
|
125
121
|
: pc.dim(` ${config.sources.local}.encrypted (optional, not found)`));
|
|
126
|
-
|
|
122
|
+
ctx.logger.log(pc.bold('\nTargets:'));
|
|
127
123
|
for (const target of config.targets) {
|
|
128
124
|
const filter = describeFilter(target);
|
|
129
125
|
const devOutput = FORMAT_OUTPUT_FILES[target.format].development;
|
|
130
|
-
|
|
126
|
+
ctx.logger.log(` ${pc.cyan(target.name)} ${pc.dim(target.path + '/')} ` +
|
|
131
127
|
`${pc.magenta(target.format)} -> ${devOutput}`);
|
|
132
128
|
if (filter !== 'all vars') {
|
|
133
|
-
|
|
129
|
+
ctx.logger.log(pc.dim(` ${filter}`));
|
|
134
130
|
}
|
|
135
131
|
}
|
|
136
|
-
|
|
132
|
+
ctx.logger.log('');
|
|
137
133
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Environment } from '../types.js';
|
|
1
|
+
import type { Environment, HushContext } from '../types.js';
|
|
2
2
|
export interface TemplateOptions {
|
|
3
3
|
root: string;
|
|
4
4
|
env: Environment;
|
|
5
5
|
}
|
|
6
|
-
export declare function templateCommand(options: TemplateOptions): Promise<void>;
|
|
6
|
+
export declare function templateCommand(ctx: HushContext, options: TemplateOptions): Promise<void>;
|
|
7
7
|
//# sourceMappingURL=template.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/commands/template.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/commands/template.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAsB,WAAW,EAAE,MAAM,aAAa,CAAC;AAEhF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;CAClB;AA4CD,wBAAsB,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAuF/F"}
|
|
@@ -1,31 +1,28 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import { join, relative } from 'node:path';
|
|
3
2
|
import pc from 'picocolors';
|
|
4
|
-
import { loadConfig, findProjectRoot } 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
|
-
import { decrypt as sopsDecrypt } from '../core/sops.js';
|
|
9
6
|
import { maskValue } from '../core/mask.js';
|
|
10
7
|
import { loadLocalTemplates, resolveTemplateVars } from '../core/template.js';
|
|
11
8
|
function getEncryptedPath(sourcePath) {
|
|
12
9
|
return sourcePath + '.encrypted';
|
|
13
10
|
}
|
|
14
|
-
function getDecryptedSecrets(projectRoot, env, config) {
|
|
11
|
+
function getDecryptedSecrets(ctx, projectRoot, env, config) {
|
|
15
12
|
const sharedEncrypted = join(projectRoot, getEncryptedPath(config.sources.shared));
|
|
16
13
|
const envEncrypted = join(projectRoot, getEncryptedPath(config.sources[env]));
|
|
17
14
|
const localEncrypted = join(projectRoot, getEncryptedPath(config.sources.local));
|
|
18
15
|
const varSources = [];
|
|
19
|
-
if (existsSync(sharedEncrypted)) {
|
|
20
|
-
const content =
|
|
16
|
+
if (ctx.fs.existsSync(sharedEncrypted)) {
|
|
17
|
+
const content = ctx.sops.decrypt(sharedEncrypted);
|
|
21
18
|
varSources.push(parseEnvContent(content));
|
|
22
19
|
}
|
|
23
|
-
if (existsSync(envEncrypted)) {
|
|
24
|
-
const content =
|
|
20
|
+
if (ctx.fs.existsSync(envEncrypted)) {
|
|
21
|
+
const content = ctx.sops.decrypt(envEncrypted);
|
|
25
22
|
varSources.push(parseEnvContent(content));
|
|
26
23
|
}
|
|
27
|
-
if (existsSync(localEncrypted)) {
|
|
28
|
-
const content =
|
|
24
|
+
if (ctx.fs.existsSync(localEncrypted)) {
|
|
25
|
+
const content = ctx.sops.decrypt(localEncrypted);
|
|
29
26
|
varSources.push(parseEnvContent(content));
|
|
30
27
|
}
|
|
31
28
|
if (varSources.length === 0) {
|
|
@@ -41,42 +38,42 @@ function getRootSecretsAsRecord(vars) {
|
|
|
41
38
|
}
|
|
42
39
|
return record;
|
|
43
40
|
}
|
|
44
|
-
export async function templateCommand(options) {
|
|
41
|
+
export async function templateCommand(ctx, options) {
|
|
45
42
|
const { root, env } = options;
|
|
46
43
|
const contextDir = root;
|
|
47
|
-
const projectInfo = findProjectRoot(contextDir);
|
|
44
|
+
const projectInfo = ctx.config.findProjectRoot(contextDir);
|
|
48
45
|
if (!projectInfo) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
process.exit(1);
|
|
46
|
+
ctx.logger.error('No hush.yaml found in current directory or any parent directory.');
|
|
47
|
+
ctx.logger.error(pc.dim('Run: npx hush init'));
|
|
48
|
+
ctx.process.exit(1);
|
|
52
49
|
}
|
|
53
50
|
const { projectRoot } = projectInfo;
|
|
54
|
-
const config = loadConfig(projectRoot);
|
|
55
|
-
const localTemplate = loadLocalTemplates(contextDir, env);
|
|
51
|
+
const config = ctx.config.loadConfig(projectRoot);
|
|
52
|
+
const localTemplate = loadLocalTemplates(contextDir, env, ctx.fs);
|
|
56
53
|
if (!localTemplate.hasTemplate) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
ctx.logger.log(pc.yellow('No local template found in current directory.'));
|
|
55
|
+
ctx.logger.log(pc.dim(`Looked for: .hush, .hush.${env}, .hush.local`));
|
|
56
|
+
ctx.logger.log('');
|
|
57
|
+
ctx.logger.log(pc.dim('Without a local template, hush run will inject all root secrets.'));
|
|
58
|
+
ctx.logger.log(pc.dim('Create a .hush file to define which variables this directory needs.'));
|
|
62
59
|
return;
|
|
63
60
|
}
|
|
64
|
-
const rootSecrets = getDecryptedSecrets(projectRoot, env, config);
|
|
61
|
+
const rootSecrets = getDecryptedSecrets(ctx, projectRoot, env, config);
|
|
65
62
|
const rootSecretsRecord = getRootSecretsAsRecord(rootSecrets);
|
|
66
|
-
const resolvedVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: process.env });
|
|
63
|
+
const resolvedVars = resolveTemplateVars(localTemplate.vars, rootSecretsRecord, { processEnv: ctx.process.env });
|
|
67
64
|
const relPath = relative(projectRoot, contextDir) || '.';
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
ctx.logger.log('');
|
|
66
|
+
ctx.logger.log(pc.bold(`Template: ${relPath}/`));
|
|
67
|
+
ctx.logger.log(pc.dim(`Project root: ${projectRoot}`));
|
|
68
|
+
ctx.logger.log(pc.dim(`Environment: ${env}`));
|
|
69
|
+
ctx.logger.log(pc.dim(`Files: ${localTemplate.files.join(', ')}`));
|
|
70
|
+
ctx.logger.log('');
|
|
74
71
|
const templateVarKeys = new Set(localTemplate.vars.map(v => v.key));
|
|
75
72
|
const rootSecretKeys = new Set(Object.keys(rootSecretsRecord));
|
|
76
73
|
let fromRoot = 0;
|
|
77
74
|
let fromLocal = 0;
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
ctx.logger.log(pc.bold('Variables:'));
|
|
76
|
+
ctx.logger.log('');
|
|
80
77
|
const maxKeyLen = Math.max(...resolvedVars.map(v => v.key.length), 20);
|
|
81
78
|
for (const resolved of resolvedVars) {
|
|
82
79
|
const original = localTemplate.vars.find(v => v.key === resolved.key);
|
|
@@ -89,23 +86,23 @@ export async function templateCommand(options) {
|
|
|
89
86
|
const refName = refMatch ? refMatch[1] : '';
|
|
90
87
|
const isEnvRef = refName.startsWith('env:');
|
|
91
88
|
if (isEnvRef) {
|
|
92
|
-
|
|
89
|
+
ctx.logger.log(` ${pc.cyan(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${masked}`);
|
|
93
90
|
fromLocal++;
|
|
94
91
|
}
|
|
95
92
|
else if (rootSecretKeys.has(refName)) {
|
|
96
|
-
|
|
93
|
+
ctx.logger.log(` ${pc.green(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${masked}`);
|
|
97
94
|
fromRoot++;
|
|
98
95
|
}
|
|
99
96
|
else {
|
|
100
|
-
|
|
97
|
+
ctx.logger.log(` ${pc.yellow(keyPadded)} = ${pc.dim(originalValue)} ${pc.dim('→')} ${pc.yellow('(unresolved)')}`);
|
|
101
98
|
}
|
|
102
99
|
}
|
|
103
100
|
else {
|
|
104
|
-
|
|
101
|
+
ctx.logger.log(` ${pc.white(keyPadded)} = ${masked} ${pc.dim('(literal)')}`);
|
|
105
102
|
fromLocal++;
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
ctx.logger.log('');
|
|
106
|
+
ctx.logger.log(pc.dim(`Total: ${resolvedVars.length} variables (${fromRoot} from root, ${fromLocal} local/literal)`));
|
|
107
|
+
ctx.logger.log('');
|
|
111
108
|
}
|
package/dist/commands/trace.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Environment } from '../types.js';
|
|
1
|
+
import type { Environment, HushContext } from '../types.js';
|
|
2
2
|
export interface TraceOptions {
|
|
3
3
|
root: string;
|
|
4
4
|
env: Environment;
|
|
5
5
|
key: string;
|
|
6
6
|
}
|
|
7
|
-
export declare function traceCommand(options: TraceOptions): Promise<void>;
|
|
7
|
+
export declare function traceCommand(ctx: HushContext, options: TraceOptions): Promise<void>;
|
|
8
8
|
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/commands/trace.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../../src/commands/trace.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAU,WAAW,EAAE,MAAM,aAAa,CAAC;AAEpE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,WAAW,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAuCD,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8DzF"}
|
package/dist/commands/trace.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
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 { parseEnvContent } from '../core/parse.js';
|
|
6
|
-
import { decrypt as sopsDecrypt } from '../core/sops.js';
|
|
7
4
|
function matchesPattern(key, pattern) {
|
|
8
5
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
9
6
|
return regex.test(key);
|
|
@@ -31,11 +28,11 @@ function getTargetDisposition(key, target) {
|
|
|
31
28
|
}
|
|
32
29
|
return { status: 'included' };
|
|
33
30
|
}
|
|
34
|
-
export async function traceCommand(options) {
|
|
31
|
+
export async function traceCommand(ctx, options) {
|
|
35
32
|
const { root, env, key } = options;
|
|
36
|
-
const config = loadConfig(root);
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
const config = ctx.config.loadConfig(root);
|
|
34
|
+
ctx.logger.log(pc.bold(`\nTracing variable: ${pc.cyan(key)}\n`));
|
|
35
|
+
ctx.logger.log(pc.blue('Source Status:'));
|
|
39
36
|
const sources = [
|
|
40
37
|
{ name: config.sources.shared, path: join(root, config.sources.shared + '.encrypted'), found: false },
|
|
41
38
|
{ name: config.sources.development, path: join(root, config.sources.development + '.encrypted'), found: false },
|
|
@@ -44,44 +41,44 @@ export async function traceCommand(options) {
|
|
|
44
41
|
];
|
|
45
42
|
const maxSourceLen = Math.max(...sources.map(s => s.name.length));
|
|
46
43
|
for (const source of sources) {
|
|
47
|
-
if (!existsSync(source.path)) {
|
|
48
|
-
|
|
44
|
+
if (!ctx.fs.existsSync(source.path)) {
|
|
45
|
+
ctx.logger.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.dim('(file not found)')}`);
|
|
49
46
|
continue;
|
|
50
47
|
}
|
|
51
48
|
try {
|
|
52
|
-
const content =
|
|
49
|
+
const content = ctx.sops.decrypt(source.path);
|
|
53
50
|
const vars = parseEnvContent(content);
|
|
54
51
|
const found = vars.some(v => v.key === key);
|
|
55
52
|
source.found = found;
|
|
56
53
|
if (found) {
|
|
57
|
-
|
|
54
|
+
ctx.logger.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.green('✅ Present')}`);
|
|
58
55
|
}
|
|
59
56
|
else {
|
|
60
|
-
|
|
57
|
+
ctx.logger.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.dim('❌ Not found')}`);
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
catch {
|
|
64
|
-
|
|
61
|
+
ctx.logger.log(` ${source.name.padEnd(maxSourceLen)} : ${pc.red('⚠️ Decrypt failed')}`);
|
|
65
62
|
}
|
|
66
63
|
}
|
|
67
64
|
const foundInAnySource = sources.some(s => s.found);
|
|
68
|
-
|
|
65
|
+
ctx.logger.log(pc.blue(`\nTarget Disposition (Environment: ${env}):`));
|
|
69
66
|
const maxTargetLen = Math.max(...config.targets.map(t => t.name.length));
|
|
70
67
|
for (const target of config.targets) {
|
|
71
68
|
const disposition = getTargetDisposition(key, target);
|
|
72
69
|
const targetLabel = `[${target.name}]`.padEnd(maxTargetLen + 2);
|
|
73
70
|
if (!foundInAnySource) {
|
|
74
|
-
|
|
71
|
+
ctx.logger.log(` ${targetLabel} : ${pc.yellow('⚠️ Variable not in any source')}`);
|
|
75
72
|
}
|
|
76
73
|
else if (disposition.status === 'included') {
|
|
77
|
-
|
|
74
|
+
ctx.logger.log(` ${targetLabel} : ${pc.green('✅ Included')}`);
|
|
78
75
|
}
|
|
79
76
|
else if (disposition.status === 'excluded') {
|
|
80
|
-
|
|
77
|
+
ctx.logger.log(` ${targetLabel} : ${pc.red(`🚫 Excluded`)} ${pc.dim(`(${disposition.reason})`)}`);
|
|
81
78
|
}
|
|
82
79
|
else {
|
|
83
|
-
|
|
80
|
+
ctx.logger.log(` ${targetLabel} : ${pc.red(`🚫 Not included`)} ${pc.dim(`(${disposition.reason})`)}`);
|
|
84
81
|
}
|
|
85
82
|
}
|
|
86
|
-
|
|
83
|
+
ctx.logger.log('');
|
|
87
84
|
}
|
package/dist/config/loader.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fs } from '../lib/fs.js';
|
|
2
2
|
import { join, dirname, resolve } from 'node:path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
4
|
import { DEFAULT_SOURCES, CURRENT_SCHEMA_VERSION } from '../types.js';
|
|
@@ -6,7 +6,7 @@ const CONFIG_FILENAMES = ['hush.yaml', 'hush.yml'];
|
|
|
6
6
|
export function findConfigPath(root) {
|
|
7
7
|
for (const filename of CONFIG_FILENAMES) {
|
|
8
8
|
const configPath = join(root, filename);
|
|
9
|
-
if (existsSync(configPath)) {
|
|
9
|
+
if (fs.existsSync(configPath)) {
|
|
10
10
|
return configPath;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -34,7 +34,7 @@ export function loadConfig(root) {
|
|
|
34
34
|
targets: [{ name: 'root', path: '.', format: 'dotenv' }],
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
37
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
38
38
|
const parsed = parseYaml(content);
|
|
39
39
|
return {
|
|
40
40
|
// Support both 'version' and 'schema_version' (prefer schema_version)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,eAAO,MAAM,cAAc,EAAE,WAmD5B,CAAC"}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fs } from './lib/fs.js';
|
|
2
|
+
import { spawnSync, execSync } from 'node:child_process';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { loadConfig, findProjectRoot } from './config/loader.js';
|
|
5
|
+
import { decrypt, isSopsInstalled } from './core/sops.js';
|
|
6
|
+
import { ageAvailable, ageGenerate, agePublicFromPrivate, keyExists, keySave, keyLoad, keyPath } from './lib/age.js';
|
|
7
|
+
import { opInstalled, opAvailable, opGetKey, opStoreKey } from './lib/onepassword.js';
|
|
8
|
+
import pc from 'picocolors';
|
|
9
|
+
export const defaultContext = {
|
|
10
|
+
fs,
|
|
11
|
+
path: {
|
|
12
|
+
join,
|
|
13
|
+
},
|
|
14
|
+
exec: {
|
|
15
|
+
spawnSync: (command, args, options) => {
|
|
16
|
+
// @ts-ignore - types are compatible at runtime
|
|
17
|
+
return spawnSync(command, args, options);
|
|
18
|
+
},
|
|
19
|
+
execSync: (command, options) => {
|
|
20
|
+
// @ts-ignore - types are compatible at runtime
|
|
21
|
+
return execSync(command, options);
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
logger: {
|
|
25
|
+
log: (message) => console.log(message),
|
|
26
|
+
error: (message) => console.error(pc.red(message)),
|
|
27
|
+
warn: (message) => console.warn(pc.yellow(message)),
|
|
28
|
+
info: (message) => console.info(pc.blue(message)),
|
|
29
|
+
},
|
|
30
|
+
process: {
|
|
31
|
+
cwd: () => process.cwd(),
|
|
32
|
+
exit: (code) => process.exit(code),
|
|
33
|
+
env: process.env,
|
|
34
|
+
stdin: process.stdin,
|
|
35
|
+
stdout: process.stdout,
|
|
36
|
+
},
|
|
37
|
+
config: {
|
|
38
|
+
loadConfig,
|
|
39
|
+
findProjectRoot,
|
|
40
|
+
},
|
|
41
|
+
age: {
|
|
42
|
+
ageAvailable,
|
|
43
|
+
ageGenerate,
|
|
44
|
+
keyExists,
|
|
45
|
+
keySave,
|
|
46
|
+
keyPath,
|
|
47
|
+
keyLoad,
|
|
48
|
+
agePublicFromPrivate,
|
|
49
|
+
},
|
|
50
|
+
onepassword: {
|
|
51
|
+
opInstalled,
|
|
52
|
+
opAvailable,
|
|
53
|
+
opGetKey,
|
|
54
|
+
opStoreKey,
|
|
55
|
+
},
|
|
56
|
+
sops: {
|
|
57
|
+
decrypt,
|
|
58
|
+
isSopsInstalled,
|
|
59
|
+
},
|
|
60
|
+
};
|
package/dist/core/parse.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fs } from '../lib/fs.js';
|
|
2
2
|
export function parseEnvContent(content) {
|
|
3
3
|
const vars = [];
|
|
4
4
|
const lines = content.split('\n');
|
|
@@ -21,10 +21,10 @@ export function parseEnvContent(content) {
|
|
|
21
21
|
return vars;
|
|
22
22
|
}
|
|
23
23
|
export function parseEnvFile(filePath) {
|
|
24
|
-
if (!existsSync(filePath)) {
|
|
24
|
+
if (!fs.existsSync(filePath)) {
|
|
25
25
|
return [];
|
|
26
26
|
}
|
|
27
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
27
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
28
28
|
return parseEnvContent(content);
|
|
29
29
|
}
|
|
30
30
|
export function varsToRecord(vars) {
|
package/dist/core/sops.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { fs } from '../lib/fs.js';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir, homedir } from 'node:os';
|
|
5
5
|
function getAgeKeyFile() {
|
|
@@ -7,7 +7,7 @@ function getAgeKeyFile() {
|
|
|
7
7
|
return process.env.SOPS_AGE_KEY_FILE;
|
|
8
8
|
}
|
|
9
9
|
const defaultPath = join(homedir(), '.config', 'sops', 'age', 'key.txt');
|
|
10
|
-
if (existsSync(defaultPath)) {
|
|
10
|
+
if (fs.existsSync(defaultPath)) {
|
|
11
11
|
return defaultPath;
|
|
12
12
|
}
|
|
13
13
|
return undefined;
|
|
@@ -35,7 +35,7 @@ export function isAgeKeyConfigured() {
|
|
|
35
35
|
return getAgeKeyFile() !== undefined;
|
|
36
36
|
}
|
|
37
37
|
export function decrypt(filePath) {
|
|
38
|
-
if (!existsSync(filePath)) {
|
|
38
|
+
if (!fs.existsSync(filePath)) {
|
|
39
39
|
throw new Error(`Encrypted file not found: ${filePath}`);
|
|
40
40
|
}
|
|
41
41
|
if (!isSopsInstalled()) {
|
|
@@ -59,7 +59,7 @@ export function decrypt(filePath) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
export function encrypt(inputPath, outputPath) {
|
|
62
|
-
if (!existsSync(inputPath)) {
|
|
62
|
+
if (!fs.existsSync(inputPath)) {
|
|
63
63
|
throw new Error(`Input file not found: ${inputPath}`);
|
|
64
64
|
}
|
|
65
65
|
if (!isSopsInstalled()) {
|
|
@@ -78,7 +78,7 @@ export function encrypt(inputPath, outputPath) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
export function edit(filePath) {
|
|
81
|
-
if (!existsSync(filePath)) {
|
|
81
|
+
if (!fs.existsSync(filePath)) {
|
|
82
82
|
throw new Error(`Encrypted file not found: ${filePath}`);
|
|
83
83
|
}
|
|
84
84
|
if (!isSopsInstalled()) {
|
|
@@ -98,7 +98,7 @@ export function setKey(filePath, key, value) {
|
|
|
98
98
|
throw new Error('SOPS is not installed. Install with: brew install sops');
|
|
99
99
|
}
|
|
100
100
|
let content = '';
|
|
101
|
-
if (existsSync(filePath)) {
|
|
101
|
+
if (fs.existsSync(filePath)) {
|
|
102
102
|
content = decrypt(filePath);
|
|
103
103
|
}
|
|
104
104
|
const lines = content.split('\n').filter(line => line.trim() !== '');
|
|
@@ -117,7 +117,7 @@ export function setKey(filePath, key, value) {
|
|
|
117
117
|
const newContent = updatedLines.join('\n') + '\n';
|
|
118
118
|
const tempFile = join(tmpdir(), `hush-temp-${Date.now()}.env`);
|
|
119
119
|
try {
|
|
120
|
-
writeFileSync(tempFile, newContent, 'utf-8');
|
|
120
|
+
fs.writeFileSync(tempFile, newContent, 'utf-8');
|
|
121
121
|
execSync(`sops --input-type dotenv --output-type dotenv --encrypt "${tempFile}" > "${filePath}"`, {
|
|
122
122
|
encoding: 'utf-8',
|
|
123
123
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -125,8 +125,8 @@ export function setKey(filePath, key, value) {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
finally {
|
|
128
|
-
if (existsSync(tempFile)) {
|
|
129
|
-
unlinkSync(tempFile);
|
|
128
|
+
if (fs.existsSync(tempFile)) {
|
|
129
|
+
fs.unlinkSync(tempFile);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
package/dist/core/template.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type InterpolateOptions } from './interpolate.js';
|
|
2
|
-
import type { EnvVar, Environment } from '../types.js';
|
|
2
|
+
import type { EnvVar, Environment, HushContext } from '../types.js';
|
|
3
3
|
export interface LocalTemplateResult {
|
|
4
4
|
hasTemplate: boolean;
|
|
5
5
|
templateDir: string;
|
|
6
6
|
vars: EnvVar[];
|
|
7
7
|
files: string[];
|
|
8
8
|
}
|
|
9
|
-
export declare function loadLocalTemplates(contextDir: string, env: Environment): LocalTemplateResult;
|
|
9
|
+
export declare function loadLocalTemplates(contextDir: string, env: Environment, fs: HushContext['fs']): LocalTemplateResult;
|
|
10
10
|
export declare function resolveTemplateVars(templateVars: EnvVar[], rootSecrets: Record<string, string>, options?: InterpolateOptions): EnvVar[];
|
|
11
11
|
//# sourceMappingURL=template.d.ts.map
|