@chriscode/hush 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +5 -1
- package/dist/commands/encrypt.d.ts.map +1 -1
- package/dist/commands/encrypt.js +55 -9
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +38 -4
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +192 -157
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +110 -1
- package/dist/utils/version-check.d.ts +2 -0
- package/dist/utils/version-check.d.ts.map +1 -0
- package/dist/utils/version-check.js +88 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,8 @@ import { checkCommand } from './commands/check.js';
|
|
|
15
15
|
import { skillCommand } from './commands/skill.js';
|
|
16
16
|
import { keysCommand } from './commands/keys.js';
|
|
17
17
|
import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
|
|
18
|
-
|
|
18
|
+
import { checkForUpdate } from './utils/version-check.js';
|
|
19
|
+
const VERSION = '2.5.1';
|
|
19
20
|
function printHelp() {
|
|
20
21
|
console.log(`
|
|
21
22
|
${pc.bold('hush')} - SOPS-based secrets management for monorepos
|
|
@@ -269,6 +270,9 @@ async function main() {
|
|
|
269
270
|
process.exit(0);
|
|
270
271
|
}
|
|
271
272
|
const { command, subcommand, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs } = parseArgs(args);
|
|
273
|
+
if (command !== 'run' && !json && !quiet) {
|
|
274
|
+
checkForUpdate(VERSION);
|
|
275
|
+
}
|
|
272
276
|
checkMigrationNeeded(root, command);
|
|
273
277
|
try {
|
|
274
278
|
switch (command) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../src/commands/encrypt.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../src/commands/encrypt.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AASlD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAsF3E"}
|
package/dist/commands/encrypt.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, unlinkSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { loadConfig } from '../config/loader.js';
|
|
5
|
-
import { encrypt as sopsEncrypt } from '../core/sops.js';
|
|
5
|
+
import { encrypt as sopsEncrypt, decrypt as sopsDecrypt } from '../core/sops.js';
|
|
6
|
+
import { parseEnvContent } from '../core/parse.js';
|
|
6
7
|
export async function encryptCommand(options) {
|
|
7
8
|
const { root } = options;
|
|
8
9
|
const config = loadConfig(root);
|
|
9
|
-
console.log(pc.blue('Encrypting secrets
|
|
10
|
+
console.log(pc.blue('Encrypting secrets...\n'));
|
|
10
11
|
const sourceFiles = [
|
|
11
12
|
{ key: 'shared', path: config.sources.shared },
|
|
12
13
|
{ key: 'development', path: config.sources.development },
|
|
13
14
|
{ key: 'production', path: config.sources.production },
|
|
14
15
|
];
|
|
15
|
-
|
|
16
|
+
const encryptedFiles = [];
|
|
16
17
|
for (const { key, path } of sourceFiles) {
|
|
17
18
|
const sourcePath = join(root, path);
|
|
18
19
|
const encryptedPath = sourcePath + '.encrypted';
|
|
@@ -20,15 +21,60 @@ export async function encryptCommand(options) {
|
|
|
20
21
|
console.log(pc.dim(` ${path} - not found, skipping`));
|
|
21
22
|
continue;
|
|
22
23
|
}
|
|
24
|
+
const sourceContent = readFileSync(sourcePath, 'utf-8');
|
|
25
|
+
const vars = parseEnvContent(sourceContent);
|
|
23
26
|
sopsEncrypt(sourcePath, encryptedPath);
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
console.log(pc.green(` ${path}`) + pc.dim(` -> ${path}.encrypted (${vars.length} vars)`));
|
|
28
|
+
encryptedFiles.push({
|
|
29
|
+
sourcePath,
|
|
30
|
+
encryptedPath,
|
|
31
|
+
displayPath: path,
|
|
32
|
+
originalKeyCount: vars.length,
|
|
33
|
+
});
|
|
26
34
|
}
|
|
27
|
-
if (
|
|
35
|
+
if (encryptedFiles.length === 0) {
|
|
28
36
|
console.error(pc.red('\nNo source files found to encrypt'));
|
|
29
37
|
console.error(pc.dim('Create at least .env with your secrets'));
|
|
30
38
|
process.exit(1);
|
|
31
39
|
}
|
|
32
|
-
console.log(pc.
|
|
33
|
-
|
|
40
|
+
console.log(pc.blue('\nVerifying encryption...'));
|
|
41
|
+
let allVerified = true;
|
|
42
|
+
for (const file of encryptedFiles) {
|
|
43
|
+
try {
|
|
44
|
+
const decrypted = sopsDecrypt(file.encryptedPath);
|
|
45
|
+
const decryptedVars = parseEnvContent(decrypted);
|
|
46
|
+
if (decryptedVars.length === file.originalKeyCount) {
|
|
47
|
+
console.log(pc.green(` ${file.displayPath}.encrypted - verified (${decryptedVars.length} vars)`));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.log(pc.yellow(` ${file.displayPath}.encrypted - warning: expected ${file.originalKeyCount} vars, got ${decryptedVars.length}`));
|
|
51
|
+
allVerified = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.log(pc.red(` ${file.displayPath}.encrypted - FAILED to decrypt`));
|
|
56
|
+
console.log(pc.dim(` ${error.message}`));
|
|
57
|
+
allVerified = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!allVerified) {
|
|
61
|
+
console.log(pc.yellow('\nEncryption completed but verification failed.'));
|
|
62
|
+
console.log(pc.yellow('Plaintext files have NOT been deleted. Please check your setup.'));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
console.log(pc.blue('\nCleaning up plaintext files...'));
|
|
66
|
+
for (const file of encryptedFiles) {
|
|
67
|
+
try {
|
|
68
|
+
unlinkSync(file.sourcePath);
|
|
69
|
+
console.log(pc.green(` Deleted ${file.displayPath}`));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.log(pc.yellow(` Could not delete ${file.displayPath}: ${error.message}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.log(pc.green(pc.bold(`\n✓ Encrypted ${encryptedFiles.length} file(s) and removed plaintext`)));
|
|
76
|
+
console.log(pc.dim('\nNext steps:'));
|
|
77
|
+
console.log(pc.dim(' 1. Commit the .encrypted files to git'));
|
|
78
|
+
console.log(pc.dim(' 2. Use "npx hush run -- <command>" to run with secrets'));
|
|
79
|
+
console.log(pc.dim(' 3. Use "npx hush inspect" to see what variables are set'));
|
|
34
80
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;AAyJnE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoErE"}
|
package/dist/commands/init.js
CHANGED
|
@@ -125,6 +125,17 @@ function detectTargets(root) {
|
|
|
125
125
|
}
|
|
126
126
|
return targets;
|
|
127
127
|
}
|
|
128
|
+
function findExistingPlaintextEnvFiles(root) {
|
|
129
|
+
const patterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
130
|
+
const found = [];
|
|
131
|
+
for (const pattern of patterns) {
|
|
132
|
+
const filePath = join(root, pattern);
|
|
133
|
+
if (existsSync(filePath)) {
|
|
134
|
+
found.push(pattern);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return found;
|
|
138
|
+
}
|
|
128
139
|
export async function initCommand(options) {
|
|
129
140
|
const { root } = options;
|
|
130
141
|
const existingConfig = findConfigPath(root);
|
|
@@ -132,8 +143,21 @@ export async function initCommand(options) {
|
|
|
132
143
|
console.error(pc.red(`Config already exists: ${existingConfig}`));
|
|
133
144
|
process.exit(1);
|
|
134
145
|
}
|
|
135
|
-
console.log(pc.blue('Initializing hush
|
|
146
|
+
console.log(pc.blue('Initializing hush...\n'));
|
|
147
|
+
const existingEnvFiles = findExistingPlaintextEnvFiles(root);
|
|
148
|
+
if (existingEnvFiles.length > 0) {
|
|
149
|
+
console.log(pc.bgYellow(pc.black(' EXISTING SECRETS DETECTED ')));
|
|
150
|
+
console.log(pc.yellow('\nFound existing .env files:'));
|
|
151
|
+
for (const file of existingEnvFiles) {
|
|
152
|
+
console.log(pc.yellow(` ${file}`));
|
|
153
|
+
}
|
|
154
|
+
console.log(pc.dim('\nThese will be encrypted after setup. Run "npx hush encrypt" when ready.\n'));
|
|
155
|
+
}
|
|
136
156
|
const project = getProjectFromPackageJson(root);
|
|
157
|
+
if (!project) {
|
|
158
|
+
console.log(pc.yellow('No project identifier found in package.json.'));
|
|
159
|
+
console.log(pc.dim('Tip: Add "project: my-org/my-repo" to hush.yaml after creation for key management.\n'));
|
|
160
|
+
}
|
|
137
161
|
const keyResult = await setupKey(root, project);
|
|
138
162
|
if (keyResult) {
|
|
139
163
|
createSopsConfig(root, keyResult.publicKey);
|
|
@@ -152,7 +176,17 @@ export async function initCommand(options) {
|
|
|
152
176
|
for (const target of targets) {
|
|
153
177
|
console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
|
|
154
178
|
}
|
|
155
|
-
console.log(pc.
|
|
156
|
-
|
|
157
|
-
|
|
179
|
+
console.log(pc.bold('\nNext steps:'));
|
|
180
|
+
if (existingEnvFiles.length > 0) {
|
|
181
|
+
console.log(pc.green(' 1. npx hush encrypt') + pc.dim(' # Encrypt existing .env files (deletes plaintext)'));
|
|
182
|
+
console.log(pc.dim(' 2. npx hush inspect') + pc.dim(' # Verify your secrets'));
|
|
183
|
+
console.log(pc.dim(' 3. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
console.log(pc.dim(' 1. npx hush set <KEY>') + pc.dim(' # Add secrets interactively'));
|
|
187
|
+
console.log(pc.dim(' 2. npx hush run -- <cmd>') + pc.dim(' # Run with secrets in memory'));
|
|
188
|
+
}
|
|
189
|
+
console.log(pc.dim('\nGit setup:'));
|
|
190
|
+
console.log(pc.dim(' git add hush.yaml .sops.yaml'));
|
|
191
|
+
console.log(pc.dim(' git commit -m "chore: add Hush secrets management"'));
|
|
158
192
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA2jChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
|
package/dist/commands/skill.js
CHANGED
|
@@ -6,53 +6,102 @@ import pc from 'picocolors';
|
|
|
6
6
|
const SKILL_FILES = {
|
|
7
7
|
'SKILL.md': `---
|
|
8
8
|
name: hush-secrets
|
|
9
|
-
description: Manage secrets safely using Hush CLI. Use when working with .env files, environment variables, secrets, API keys, database URLs, credentials, or configuration.
|
|
9
|
+
description: Manage secrets safely using Hush CLI. Use when working with .env files, environment variables, secrets, API keys, database URLs, credentials, or configuration. NEVER read .env files directly - always use hush commands instead to prevent exposing secrets to the LLM.
|
|
10
10
|
allowed-tools: Bash(hush:*), Bash(npx hush:*), Bash(brew:*), Bash(npm:*), Bash(pnpm:*), Bash(age-keygen:*), Read, Grep, Glob, Write, Bash(cat:*), Bash(grep:*)
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Hush - AI-Native Secrets Management
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**CRITICAL: NEVER read .env files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Hush keeps secrets **encrypted at rest**. When properly set up, all secrets are stored in \`.env.encrypted\` files and plaintext \`.env\` files should NOT exist.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
- \`hush run -- <command>\` decrypts to memory and runs your command
|
|
21
|
-
- \`hush set <KEY>\` adds secrets interactively (you invoke, user enters value)
|
|
22
|
-
- \`hush inspect\` shows what exists with masked values
|
|
23
|
-
- \`hush edit\` opens encrypted file in editor, re-encrypts on save
|
|
19
|
+
## First Step: Investigate Current State
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
**ALWAYS run this first when working with a new repo:**
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
\`\`\`bash
|
|
24
|
+
npx hush status
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
This tells you:
|
|
28
|
+
- Whether Hush is configured (\`hush.yaml\` exists)
|
|
29
|
+
- If SOPS/age are installed
|
|
30
|
+
- If encryption keys are set up
|
|
31
|
+
- **CRITICAL: If unencrypted .env files exist (security risk!)**
|
|
32
|
+
- What source files are configured
|
|
33
|
+
|
|
34
|
+
### Interpreting Status Output
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
| You See | What It Means | Action |
|
|
37
|
+
|---------|---------------|--------|
|
|
38
|
+
| \`SECURITY WARNING: Unencrypted .env files\` | Plaintext secrets exist! | Run \`npx hush encrypt\` immediately |
|
|
39
|
+
| \`No hush.yaml found\` | Hush not initialized | Run \`npx hush init\` |
|
|
40
|
+
| \`SOPS not installed\` | Missing prerequisite | \`brew install sops\` |
|
|
41
|
+
| \`age key not found\` | Missing encryption key | \`npx hush keys setup\` |
|
|
42
|
+
| \`Project: not set\` | Key management limited | Add \`project:\` to hush.yaml |
|
|
43
|
+
| \`1Password backup: not synced\` | Key not backed up | \`npx hush keys push\` |
|
|
44
|
+
|
|
45
|
+
## Decision Tree: What Do I Do?
|
|
46
|
+
|
|
47
|
+
### Scenario 1: Fresh Repo (No Hush Setup)
|
|
48
|
+
|
|
49
|
+
\`\`\`bash
|
|
50
|
+
npx hush init # Creates hush.yaml and .sops.yaml
|
|
51
|
+
npx hush encrypt # Encrypts any existing .env files, deletes plaintext
|
|
52
|
+
npx hush inspect # Verify setup
|
|
32
53
|
\`\`\`
|
|
33
|
-
|
|
54
|
+
|
|
55
|
+
### Scenario 2: Existing .env Files Found
|
|
56
|
+
|
|
57
|
+
\`\`\`bash
|
|
58
|
+
npx hush status # Check what's there
|
|
59
|
+
npx hush encrypt # Encrypt them (auto-deletes plaintext after verification)
|
|
60
|
+
npx hush inspect # Confirm everything is encrypted
|
|
34
61
|
\`\`\`
|
|
35
62
|
|
|
36
|
-
|
|
63
|
+
### Scenario 3: Hush Already Set Up (Team Member Joining)
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- \`npx hush has <KEY>\` - Check if a specific variable is set
|
|
44
|
-
- \`npx hush status\` - View configuration
|
|
65
|
+
\`\`\`bash
|
|
66
|
+
npx hush keys setup # Pull key from 1Password or prompt for setup
|
|
67
|
+
npx hush status # Verify everything works
|
|
68
|
+
npx hush inspect # See what secrets exist
|
|
69
|
+
\`\`\`
|
|
45
70
|
|
|
46
|
-
###
|
|
47
|
-
- \`hush decrypt\` / \`hush unsafe:decrypt\` - Writes unencrypted secrets to disk (defeats the purpose!)
|
|
71
|
+
### Scenario 4: Need to Add/Modify Secrets
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
\`\`\`bash
|
|
74
|
+
npx hush set <KEY> # Add interactively (you invoke, user types value)
|
|
75
|
+
npx hush edit # Edit all secrets in $EDITOR
|
|
76
|
+
npx hush inspect # Verify changes
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
### Scenario 5: Run Application with Secrets
|
|
50
80
|
|
|
51
81
|
\`\`\`bash
|
|
52
|
-
npx hush
|
|
82
|
+
npx hush run -- npm start # Development
|
|
83
|
+
npx hush run -e production -- npm build # Production
|
|
53
84
|
\`\`\`
|
|
54
85
|
|
|
55
|
-
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Commands Quick Reference
|
|
89
|
+
|
|
90
|
+
| Command | Purpose | When to Use |
|
|
91
|
+
|---------|---------|-------------|
|
|
92
|
+
| \`npx hush status\` | **Full diagnostic** | First step, always |
|
|
93
|
+
| \`npx hush inspect\` | See variables (masked) | Check what's configured |
|
|
94
|
+
| \`npx hush has <KEY>\` | Check specific variable | Verify a secret exists |
|
|
95
|
+
| \`npx hush set <KEY>\` | Add secret interactively | User needs to enter a value |
|
|
96
|
+
| \`npx hush edit\` | Edit all secrets | Bulk editing |
|
|
97
|
+
| \`npx hush run -- <cmd>\` | Run with secrets in memory | Actually use the secrets |
|
|
98
|
+
| \`npx hush init\` | Initialize Hush | First-time setup |
|
|
99
|
+
| \`npx hush encrypt\` | Encrypt .env files | After creating/modifying plaintext |
|
|
100
|
+
| \`npx hush keys setup\` | Set up encryption keys | New team member |
|
|
101
|
+
|
|
102
|
+
### Commands to AVOID:
|
|
103
|
+
- \`hush decrypt\` - Writes plaintext to disk (security risk!)
|
|
104
|
+
- \`cat .env\` - Never read plaintext .env files directly
|
|
56
105
|
|
|
57
106
|
---
|
|
58
107
|
|
|
@@ -66,14 +115,11 @@ npx hush run -e production -- npm build # Run with production secrets
|
|
|
66
115
|
npx hush run -t api -- wrangler dev # Run filtered for 'api' target
|
|
67
116
|
\`\`\`
|
|
68
117
|
|
|
69
|
-
The secrets are decrypted to memory and injected as environment variables.
|
|
70
|
-
The child process inherits them. No plaintext files are written.
|
|
71
|
-
|
|
72
118
|
---
|
|
73
119
|
|
|
74
120
|
## Checking Secrets
|
|
75
121
|
|
|
76
|
-
### See what variables exist
|
|
122
|
+
### See what variables exist
|
|
77
123
|
|
|
78
124
|
\`\`\`bash
|
|
79
125
|
npx hush inspect # Development
|
|
@@ -99,14 +145,6 @@ npx hush has DATABASE_URL # Verbose output
|
|
|
99
145
|
npx hush has API_KEY -q # Quiet: exit code only (0=set, 1=missing)
|
|
100
146
|
\`\`\`
|
|
101
147
|
|
|
102
|
-
### Read encrypted files directly
|
|
103
|
-
|
|
104
|
-
You can also just read the encrypted files:
|
|
105
|
-
\`\`\`bash
|
|
106
|
-
cat .env.encrypted # See encrypted content (safe!)
|
|
107
|
-
grep DATABASE .env.encrypted # Search for keys in encrypted file
|
|
108
|
-
\`\`\`
|
|
109
|
-
|
|
110
148
|
---
|
|
111
149
|
|
|
112
150
|
## Adding/Modifying Secrets
|
|
@@ -120,45 +158,7 @@ npx hush set DEBUG --local # Set personal local override
|
|
|
120
158
|
\`\`\`
|
|
121
159
|
|
|
122
160
|
The user will be prompted to enter the value (hidden input).
|
|
123
|
-
You never see the actual secret - just invoke the command
|
|
124
|
-
|
|
125
|
-
### Edit all secrets in editor
|
|
126
|
-
|
|
127
|
-
\`\`\`bash
|
|
128
|
-
npx hush edit # Edit shared secrets
|
|
129
|
-
npx hush edit development # Edit development secrets
|
|
130
|
-
npx hush edit local # Edit personal overrides
|
|
131
|
-
\`\`\`
|
|
132
|
-
|
|
133
|
-
---
|
|
134
|
-
|
|
135
|
-
## Common Workflows
|
|
136
|
-
|
|
137
|
-
### "Help user add DATABASE_URL"
|
|
138
|
-
\`\`\`bash
|
|
139
|
-
npx hush set DATABASE_URL
|
|
140
|
-
\`\`\`
|
|
141
|
-
Tell user: "Enter your database URL when prompted"
|
|
142
|
-
|
|
143
|
-
### "Check all required secrets"
|
|
144
|
-
\`\`\`bash
|
|
145
|
-
npx hush has DATABASE_URL -q && npx hush has API_KEY -q && echo "All configured" || echo "Some missing"
|
|
146
|
-
\`\`\`
|
|
147
|
-
|
|
148
|
-
### "Run the development server"
|
|
149
|
-
\`\`\`bash
|
|
150
|
-
npx hush run -- npm run dev
|
|
151
|
-
\`\`\`
|
|
152
|
-
|
|
153
|
-
### "Build for production"
|
|
154
|
-
\`\`\`bash
|
|
155
|
-
npx hush run -e production -- npm run build
|
|
156
|
-
\`\`\`
|
|
157
|
-
|
|
158
|
-
### "See what's in the encrypted file"
|
|
159
|
-
\`\`\`bash
|
|
160
|
-
cat .env.encrypted # Safe! Shows encrypted data only
|
|
161
|
-
\`\`\`
|
|
161
|
+
**You never see the actual secret - just invoke the command!**
|
|
162
162
|
|
|
163
163
|
---
|
|
164
164
|
|
|
@@ -796,32 +796,71 @@ targets:
|
|
|
796
796
|
|
|
797
797
|
Step-by-step examples for common workflows when working with secrets.
|
|
798
798
|
|
|
799
|
-
**
|
|
799
|
+
**CRITICAL: NEVER read .env files directly. Use hush commands instead.**
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## First-Time Setup (Most Important!)
|
|
804
|
+
|
|
805
|
+
### "Help me set up Hush for this project"
|
|
806
|
+
|
|
807
|
+
**Step 1: Check current state**
|
|
808
|
+
\`\`\`bash
|
|
809
|
+
npx hush status
|
|
810
|
+
\`\`\`
|
|
811
|
+
|
|
812
|
+
This will show:
|
|
813
|
+
- If Hush is already configured
|
|
814
|
+
- If there are unencrypted .env files (security risk!)
|
|
815
|
+
- What prerequisites are missing
|
|
816
|
+
|
|
817
|
+
**Step 2: Based on the output, follow the appropriate path:**
|
|
818
|
+
|
|
819
|
+
#### Path A: "SECURITY WARNING: Unencrypted .env files detected"
|
|
820
|
+
\`\`\`bash
|
|
821
|
+
npx hush init # If no hush.yaml exists
|
|
822
|
+
npx hush encrypt # Encrypts files and DELETES plaintext automatically
|
|
823
|
+
npx hush status # Verify the warning is gone
|
|
824
|
+
\`\`\`
|
|
825
|
+
|
|
826
|
+
#### Path B: "No hush.yaml found"
|
|
827
|
+
\`\`\`bash
|
|
828
|
+
npx hush init # Creates config and sets up keys
|
|
829
|
+
npx hush set <KEY> # Add secrets (if none exist yet)
|
|
830
|
+
\`\`\`
|
|
831
|
+
|
|
832
|
+
#### Path C: "age key not found"
|
|
833
|
+
\`\`\`bash
|
|
834
|
+
npx hush keys setup # Pull from 1Password or generate new key
|
|
835
|
+
\`\`\`
|
|
836
|
+
|
|
837
|
+
#### Path D: Everything looks good
|
|
838
|
+
\`\`\`bash
|
|
839
|
+
npx hush inspect # See what secrets are configured
|
|
840
|
+
\`\`\`
|
|
841
|
+
|
|
842
|
+
---
|
|
800
843
|
|
|
801
844
|
## Running Programs (Most Common)
|
|
802
845
|
|
|
803
846
|
### "Start the development server"
|
|
804
|
-
|
|
805
847
|
\`\`\`bash
|
|
806
|
-
hush run -- npm run dev
|
|
848
|
+
npx hush run -- npm run dev
|
|
807
849
|
\`\`\`
|
|
808
850
|
|
|
809
851
|
### "Build for production"
|
|
810
|
-
|
|
811
852
|
\`\`\`bash
|
|
812
|
-
hush run -e production -- npm run build
|
|
853
|
+
npx hush run -e production -- npm run build
|
|
813
854
|
\`\`\`
|
|
814
855
|
|
|
815
856
|
### "Run tests with secrets"
|
|
816
|
-
|
|
817
857
|
\`\`\`bash
|
|
818
|
-
hush run -- npm test
|
|
858
|
+
npx hush run -- npm test
|
|
819
859
|
\`\`\`
|
|
820
860
|
|
|
821
861
|
### "Run Wrangler for Cloudflare Worker"
|
|
822
|
-
|
|
823
862
|
\`\`\`bash
|
|
824
|
-
hush run -t api -- wrangler dev
|
|
863
|
+
npx hush run -t api -- wrangler dev
|
|
825
864
|
\`\`\`
|
|
826
865
|
|
|
827
866
|
---
|
|
@@ -829,66 +868,53 @@ hush run -t api -- wrangler dev
|
|
|
829
868
|
## Checking Secrets
|
|
830
869
|
|
|
831
870
|
### "What environment variables does this project use?"
|
|
832
|
-
|
|
833
871
|
\`\`\`bash
|
|
834
|
-
hush inspect
|
|
835
|
-
# or
|
|
836
|
-
cat .env.encrypted # Raw encrypted file (safe!)
|
|
872
|
+
npx hush inspect # Shows all variables with masked values
|
|
837
873
|
\`\`\`
|
|
838
874
|
|
|
839
875
|
### "Is the database configured?"
|
|
840
|
-
|
|
841
876
|
\`\`\`bash
|
|
842
|
-
hush has DATABASE_URL
|
|
877
|
+
npx hush has DATABASE_URL
|
|
843
878
|
\`\`\`
|
|
844
879
|
|
|
845
|
-
If "not found", help user add it
|
|
880
|
+
If "not found", help user add it:
|
|
881
|
+
\`\`\`bash
|
|
882
|
+
npx hush set DATABASE_URL
|
|
883
|
+
\`\`\`
|
|
884
|
+
Tell user: "Enter your database URL when prompted"
|
|
846
885
|
|
|
847
886
|
### "Check all required secrets"
|
|
848
|
-
|
|
849
887
|
\`\`\`bash
|
|
850
|
-
hush has DATABASE_URL -q && \\
|
|
851
|
-
hush has API_KEY -q && \\
|
|
888
|
+
npx hush has DATABASE_URL -q && \\
|
|
889
|
+
npx hush has API_KEY -q && \\
|
|
852
890
|
echo "All configured" || \\
|
|
853
891
|
echo "Some missing"
|
|
854
892
|
\`\`\`
|
|
855
893
|
|
|
856
|
-
### "Search for a key in encrypted files"
|
|
857
|
-
|
|
858
|
-
\`\`\`bash
|
|
859
|
-
grep DATABASE .env.encrypted # Safe! Shows encrypted line
|
|
860
|
-
\`\`\`
|
|
861
|
-
|
|
862
894
|
---
|
|
863
895
|
|
|
864
896
|
## Adding Secrets
|
|
865
897
|
|
|
866
898
|
### "Help me add DATABASE_URL"
|
|
867
|
-
|
|
868
899
|
\`\`\`bash
|
|
869
|
-
hush set DATABASE_URL
|
|
900
|
+
npx hush set DATABASE_URL
|
|
870
901
|
\`\`\`
|
|
871
|
-
|
|
872
902
|
Tell user: "Enter your database URL when prompted (input will be hidden)"
|
|
873
903
|
|
|
874
904
|
### "Add a production-only secret"
|
|
875
|
-
|
|
876
905
|
\`\`\`bash
|
|
877
|
-
hush set STRIPE_SECRET_KEY -e production
|
|
906
|
+
npx hush set STRIPE_SECRET_KEY -e production
|
|
878
907
|
\`\`\`
|
|
879
908
|
|
|
880
909
|
### "Add a personal local override"
|
|
881
|
-
|
|
882
910
|
\`\`\`bash
|
|
883
|
-
hush set DEBUG --local
|
|
911
|
+
npx hush set DEBUG --local
|
|
884
912
|
\`\`\`
|
|
885
913
|
|
|
886
914
|
### "Edit multiple secrets at once"
|
|
887
|
-
|
|
888
915
|
\`\`\`bash
|
|
889
|
-
hush edit
|
|
916
|
+
npx hush edit
|
|
890
917
|
\`\`\`
|
|
891
|
-
|
|
892
918
|
Tell user: "Your editor will open. Add or modify secrets, then save and close."
|
|
893
919
|
|
|
894
920
|
---
|
|
@@ -899,27 +925,22 @@ Tell user: "Your editor will open. Add or modify secrets, then save and close."
|
|
|
899
925
|
|
|
900
926
|
1. Check if it exists:
|
|
901
927
|
\`\`\`bash
|
|
902
|
-
hush has DATABASE_URL
|
|
928
|
+
npx hush has DATABASE_URL
|
|
903
929
|
\`\`\`
|
|
904
930
|
|
|
905
931
|
2. Check target distribution:
|
|
906
932
|
\`\`\`bash
|
|
907
|
-
hush inspect
|
|
933
|
+
npx hush inspect
|
|
908
934
|
\`\`\`
|
|
909
935
|
|
|
910
936
|
3. Check hush.yaml for filtering:
|
|
911
937
|
\`\`\`bash
|
|
912
|
-
cat hush.yaml
|
|
913
|
-
\`\`\`
|
|
914
|
-
|
|
915
|
-
4. Look at the encrypted file:
|
|
916
|
-
\`\`\`bash
|
|
917
|
-
grep DATABASE .env.encrypted # Safe to read!
|
|
938
|
+
cat hush.yaml # Safe - this is config, not secrets
|
|
918
939
|
\`\`\`
|
|
919
940
|
|
|
920
|
-
|
|
941
|
+
4. Try running directly:
|
|
921
942
|
\`\`\`bash
|
|
922
|
-
hush run -- env | grep DATABASE
|
|
943
|
+
npx hush run -- env | grep DATABASE
|
|
923
944
|
\`\`\`
|
|
924
945
|
|
|
925
946
|
---
|
|
@@ -928,17 +949,25 @@ Tell user: "Your editor will open. Add or modify secrets, then save and close."
|
|
|
928
949
|
|
|
929
950
|
### "New team member setup"
|
|
930
951
|
|
|
931
|
-
Guide them:
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
> 4. Start developing with \`hush run -- npm run dev\`
|
|
952
|
+
Guide them through these steps:
|
|
953
|
+
\`\`\`bash
|
|
954
|
+
# 1. Pull key from 1Password (or get from team member)
|
|
955
|
+
npx hush keys setup
|
|
936
956
|
|
|
937
|
-
|
|
957
|
+
# 2. Verify setup
|
|
958
|
+
npx hush status
|
|
959
|
+
|
|
960
|
+
# 3. Check secrets are accessible
|
|
961
|
+
npx hush inspect
|
|
938
962
|
|
|
963
|
+
# 4. Start developing
|
|
964
|
+
npx hush run -- npm run dev
|
|
965
|
+
\`\`\`
|
|
966
|
+
|
|
967
|
+
### "Someone added new secrets"
|
|
939
968
|
\`\`\`bash
|
|
940
969
|
git pull
|
|
941
|
-
hush inspect # See what's new
|
|
970
|
+
npx hush inspect # See what's new
|
|
942
971
|
\`\`\`
|
|
943
972
|
|
|
944
973
|
---
|
|
@@ -946,24 +975,50 @@ hush inspect # See what's new
|
|
|
946
975
|
## Deployment
|
|
947
976
|
|
|
948
977
|
### "Push to Cloudflare Workers"
|
|
949
|
-
|
|
950
978
|
\`\`\`bash
|
|
951
|
-
hush push --dry-run # Preview first
|
|
952
|
-
hush push # Actually push
|
|
979
|
+
npx hush push --dry-run # Preview first
|
|
980
|
+
npx hush push # Actually push
|
|
953
981
|
\`\`\`
|
|
954
982
|
|
|
955
983
|
### "Build and deploy"
|
|
956
|
-
|
|
957
984
|
\`\`\`bash
|
|
958
|
-
hush run -e production -- npm run build
|
|
959
|
-
hush push
|
|
985
|
+
npx hush run -e production -- npm run build
|
|
986
|
+
npx hush push
|
|
960
987
|
\`\`\`
|
|
961
988
|
|
|
962
989
|
---
|
|
963
990
|
|
|
964
991
|
## Understanding the Output
|
|
965
992
|
|
|
966
|
-
### hush
|
|
993
|
+
### npx hush status output explained
|
|
994
|
+
|
|
995
|
+
\`\`\`
|
|
996
|
+
SECURITY WARNING
|
|
997
|
+
Unencrypted .env files detected!
|
|
998
|
+
.env
|
|
999
|
+
.env.development
|
|
1000
|
+
|
|
1001
|
+
Config:
|
|
1002
|
+
hush.yaml
|
|
1003
|
+
Project: my-org/my-repo
|
|
1004
|
+
|
|
1005
|
+
Prerequisites:
|
|
1006
|
+
SOPS installed
|
|
1007
|
+
age key configured
|
|
1008
|
+
|
|
1009
|
+
Key Status:
|
|
1010
|
+
Local key: ~/.config/sops/age/keys/my-org-my-repo.txt
|
|
1011
|
+
1Password backup: synced
|
|
1012
|
+
\`\`\`
|
|
1013
|
+
|
|
1014
|
+
**Reading this:**
|
|
1015
|
+
- There's a security issue - plaintext files exist
|
|
1016
|
+
- The project is configured with key management
|
|
1017
|
+
- Keys are properly set up and backed up
|
|
1018
|
+
|
|
1019
|
+
**To fix:** Run \`npx hush encrypt\`
|
|
1020
|
+
|
|
1021
|
+
### npx hush inspect output explained
|
|
967
1022
|
|
|
968
1023
|
\`\`\`
|
|
969
1024
|
Secrets for development:
|
|
@@ -973,32 +1028,12 @@ Secrets for development:
|
|
|
973
1028
|
API_KEY = (not set)
|
|
974
1029
|
|
|
975
1030
|
Total: 3 variables
|
|
976
|
-
|
|
977
|
-
Target distribution:
|
|
978
|
-
|
|
979
|
-
root (.) - 3 vars
|
|
980
|
-
app (./app/) - 1 vars
|
|
981
|
-
include: EXPO_PUBLIC_*
|
|
982
|
-
api (./api/) - 2 vars
|
|
983
|
-
exclude: EXPO_PUBLIC_*
|
|
984
1031
|
\`\`\`
|
|
985
1032
|
|
|
986
1033
|
**Reading this:**
|
|
987
1034
|
- \`DATABASE_URL\` is set, starts with "post", is 45 characters (likely a postgres:// URL)
|
|
988
1035
|
- \`STRIPE_SECRET_KEY\` starts with "sk_t" (Stripe test key format)
|
|
989
1036
|
- \`API_KEY\` is not set - user needs to add it
|
|
990
|
-
- The \`app\` folder only gets \`EXPO_PUBLIC_*\` variables
|
|
991
|
-
- The \`api\` folder gets everything except \`EXPO_PUBLIC_*\`
|
|
992
|
-
|
|
993
|
-
### Reading encrypted files directly
|
|
994
|
-
|
|
995
|
-
\`\`\`bash
|
|
996
|
-
$ cat .env.encrypted
|
|
997
|
-
DATABASE_URL=ENC[AES256_GCM,data:7xH2kL9...,iv:abc...,tag:xyz...,type:str]
|
|
998
|
-
STRIPE_SECRET_KEY=ENC[AES256_GCM,data:mN3pQ8...,iv:def...,tag:uvw...,type:str]
|
|
999
|
-
\`\`\`
|
|
1000
|
-
|
|
1001
|
-
This is safe to view—the actual values are encrypted. You can see what keys exist without exposing secrets.
|
|
1002
1037
|
`,
|
|
1003
1038
|
};
|
|
1004
1039
|
function getSkillPath(location, root) {
|
|
@@ -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":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA8DjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAyHzE"}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,15 +1,91 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { findConfigPath, loadConfig } from '../config/loader.js';
|
|
5
5
|
import { describeFilter } from '../core/filter.js';
|
|
6
6
|
import { isAgeKeyConfigured, isSopsInstalled } from '../core/sops.js';
|
|
7
|
+
import { keyExists } from '../lib/age.js';
|
|
8
|
+
import { opAvailable, opListKeys } from '../lib/onepassword.js';
|
|
7
9
|
import { FORMAT_OUTPUT_FILES } from '../types.js';
|
|
10
|
+
function findPlaintextEnvFiles(root) {
|
|
11
|
+
const results = [];
|
|
12
|
+
const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
13
|
+
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
|
|
14
|
+
function scanDir(dir, relativePath = '') {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = readdirSync(dir);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (skipDirs.has(entry))
|
|
24
|
+
continue;
|
|
25
|
+
if (entry.endsWith('.encrypted'))
|
|
26
|
+
continue;
|
|
27
|
+
const fullPath = join(dir, entry);
|
|
28
|
+
const relPath = relativePath ? `${relativePath}/${entry}` : entry;
|
|
29
|
+
try {
|
|
30
|
+
if (statSync(fullPath).isDirectory()) {
|
|
31
|
+
scanDir(fullPath, relPath);
|
|
32
|
+
}
|
|
33
|
+
else if (plaintextPatterns.includes(entry)) {
|
|
34
|
+
results.push(relPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
scanDir(root);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
function getProjectFromConfig(root) {
|
|
46
|
+
const config = loadConfig(root);
|
|
47
|
+
if (config.project)
|
|
48
|
+
return config.project;
|
|
49
|
+
const pkgPath = join(root, 'package.json');
|
|
50
|
+
if (existsSync(pkgPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf-8'));
|
|
53
|
+
if (typeof pkg.repository === 'string') {
|
|
54
|
+
const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
55
|
+
if (match)
|
|
56
|
+
return match[1];
|
|
57
|
+
}
|
|
58
|
+
if (pkg.repository?.url) {
|
|
59
|
+
const match = pkg.repository.url.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
60
|
+
if (match)
|
|
61
|
+
return match[1];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
8
70
|
export async function statusCommand(options) {
|
|
9
71
|
const { root } = options;
|
|
10
72
|
const config = loadConfig(root);
|
|
11
73
|
const configPath = findConfigPath(root);
|
|
12
74
|
console.log(pc.blue('Hush Status\n'));
|
|
75
|
+
const plaintextFiles = findPlaintextEnvFiles(root);
|
|
76
|
+
if (plaintextFiles.length > 0) {
|
|
77
|
+
console.log(pc.bgRed(pc.white(pc.bold(' SECURITY WARNING '))));
|
|
78
|
+
console.log(pc.red(pc.bold('\nUnencrypted .env files detected!\n')));
|
|
79
|
+
for (const file of plaintextFiles) {
|
|
80
|
+
console.log(pc.red(` ${file}`));
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(pc.yellow('These files may expose secrets to AI assistants and version control.'));
|
|
84
|
+
console.log(pc.bold('\nTo fix:'));
|
|
85
|
+
console.log(pc.dim(' 1. Run: npx hush encrypt'));
|
|
86
|
+
console.log(pc.dim(' 2. The plaintext files will be automatically deleted after encryption'));
|
|
87
|
+
console.log(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars\n'));
|
|
88
|
+
}
|
|
13
89
|
console.log(pc.bold('Config:'));
|
|
14
90
|
if (configPath) {
|
|
15
91
|
console.log(pc.green(` ${configPath.replace(root + '/', '')}`));
|
|
@@ -17,6 +93,16 @@ export async function statusCommand(options) {
|
|
|
17
93
|
else {
|
|
18
94
|
console.log(pc.dim(' No hush.yaml found (using defaults)'));
|
|
19
95
|
}
|
|
96
|
+
const project = getProjectFromConfig(root);
|
|
97
|
+
if (configPath) {
|
|
98
|
+
if (project) {
|
|
99
|
+
console.log(pc.green(` Project: ${project}`));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
console.log(pc.yellow(' Project: not set'));
|
|
103
|
+
console.log(pc.dim(' Add "project: my-org/my-repo" to hush.yaml for key management'));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
20
106
|
console.log(pc.bold('\nPrerequisites:'));
|
|
21
107
|
console.log(isSopsInstalled()
|
|
22
108
|
? pc.green(' SOPS installed')
|
|
@@ -24,6 +110,29 @@ export async function statusCommand(options) {
|
|
|
24
110
|
console.log(isAgeKeyConfigured()
|
|
25
111
|
? pc.green(' age key configured')
|
|
26
112
|
: pc.yellow(' age key not found at ~/.config/sops/age/key.txt'));
|
|
113
|
+
if (project) {
|
|
114
|
+
const hasLocalKey = keyExists(project);
|
|
115
|
+
const has1PasswordBackup = opAvailable() && opListKeys().includes(project);
|
|
116
|
+
console.log(pc.bold('\nKey Status:'));
|
|
117
|
+
console.log(hasLocalKey
|
|
118
|
+
? pc.green(` Local key: ~/.config/sops/age/keys/${project.replace(/\//g, '-')}.txt`)
|
|
119
|
+
: pc.yellow(' Local key: not found'));
|
|
120
|
+
if (opAvailable()) {
|
|
121
|
+
console.log(has1PasswordBackup
|
|
122
|
+
? pc.green(' 1Password backup: synced')
|
|
123
|
+
: pc.yellow(' 1Password backup: not synced'));
|
|
124
|
+
if (!has1PasswordBackup && hasLocalKey) {
|
|
125
|
+
console.log(pc.dim(' Run "npx hush keys push" to backup to 1Password'));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(pc.dim(' 1Password CLI: not available'));
|
|
130
|
+
}
|
|
131
|
+
if (!hasLocalKey) {
|
|
132
|
+
console.log(pc.bold('\n To set up keys:'));
|
|
133
|
+
console.log(pc.dim(' npx hush keys setup # Pull from 1Password or generate'));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
27
136
|
console.log(pc.bold('\nSource Files:'));
|
|
28
137
|
const sources = [
|
|
29
138
|
{ key: 'shared', path: config.sources.shared },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-check.d.ts","sourceRoot":"","sources":["../../src/utils/version-check.ts"],"names":[],"mappings":"AAeA,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CA6B3D"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
const CONFIG_DIR = join(homedir(), '.config', 'hush');
|
|
7
|
+
const CACHE_FILE = join(CONFIG_DIR, 'update-check.json');
|
|
8
|
+
const CHECK_INTERVAL_MS = 1000 * 60 * 60 * 24;
|
|
9
|
+
export function checkForUpdate(currentVersion) {
|
|
10
|
+
try {
|
|
11
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
12
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
let cache = null;
|
|
15
|
+
if (existsSync(CACHE_FILE)) {
|
|
16
|
+
try {
|
|
17
|
+
cache = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (cache && cache.latestVersion && isNewer(cache.latestVersion, currentVersion)) {
|
|
23
|
+
console.error(pc.bgYellow(pc.black(' UPDATE ')) +
|
|
24
|
+
pc.yellow(` New version available: ${cache.latestVersion} (current: ${currentVersion})`));
|
|
25
|
+
console.error(pc.dim(`Run "npm install -D @chriscode/hush@latest" to update`));
|
|
26
|
+
console.error('');
|
|
27
|
+
}
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (!cache || now - cache.lastCheck > CHECK_INTERVAL_MS) {
|
|
30
|
+
spawnBackgroundCheck();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function spawnBackgroundCheck() {
|
|
37
|
+
const script = `
|
|
38
|
+
const https = require('https');
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const path = require('path');
|
|
41
|
+
|
|
42
|
+
const cacheFile = '${CACHE_FILE.replace(/\\/g, '\\\\')}';
|
|
43
|
+
|
|
44
|
+
const req = https.get('https://registry.npmjs.org/@chriscode/hush/latest', {
|
|
45
|
+
timeout: 3000,
|
|
46
|
+
headers: { 'User-Agent': 'hush-cli' }
|
|
47
|
+
}, (res) => {
|
|
48
|
+
if (res.statusCode !== 200) process.exit(0);
|
|
49
|
+
|
|
50
|
+
let data = '';
|
|
51
|
+
res.on('data', chunk => data += chunk);
|
|
52
|
+
res.on('end', () => {
|
|
53
|
+
try {
|
|
54
|
+
const json = JSON.parse(data);
|
|
55
|
+
const content = JSON.stringify({
|
|
56
|
+
lastCheck: Date.now(),
|
|
57
|
+
latestVersion: json.version
|
|
58
|
+
});
|
|
59
|
+
fs.writeFileSync(cacheFile, content);
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
req.on('error', () => {});
|
|
65
|
+
req.end();
|
|
66
|
+
`;
|
|
67
|
+
const child = spawn(process.execPath, ['-e', script], {
|
|
68
|
+
detached: true,
|
|
69
|
+
stdio: 'ignore',
|
|
70
|
+
env: { ...process.env, NO_UPDATE_NOTIFIER: '1' }
|
|
71
|
+
});
|
|
72
|
+
child.unref();
|
|
73
|
+
}
|
|
74
|
+
function isNewer(latest, current) {
|
|
75
|
+
const l = latest.split('.').map(Number);
|
|
76
|
+
const c = current.split('.').map(Number);
|
|
77
|
+
if (l[0] > c[0])
|
|
78
|
+
return true;
|
|
79
|
+
if (l[0] < c[0])
|
|
80
|
+
return false;
|
|
81
|
+
if (l[1] > c[1])
|
|
82
|
+
return true;
|
|
83
|
+
if (l[1] < c[1])
|
|
84
|
+
return false;
|
|
85
|
+
if (l[2] > c[2])
|
|
86
|
+
return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|