@redredchen01/env-manager 0.2.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/CHANGELOG.md +24 -0
- package/README.md +45 -0
- package/SKILL.md +52 -0
- package/bin/env-manager.sh +7 -0
- package/cli/commands/decrypt.sh +70 -0
- package/cli/commands/diff.sh +136 -0
- package/cli/commands/encrypt.sh +75 -0
- package/cli/commands/init.sh +95 -0
- package/cli/commands/show.sh +90 -0
- package/cli/commands/sync.sh +92 -0
- package/cli/commands/validate.sh +121 -0
- package/cli/main.sh +22 -0
- package/cli/utils/common.sh +2 -0
- package/package.json +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Initial release: Skill Foundry v2 CLI toolkit
|
|
8
|
+
|
|
9
|
+
- cli-core: dispatcher, logger, config (XDG), version framework
|
|
10
|
+
- api-testing: HTTP requests, assertions, collections, environments, mock server
|
|
11
|
+
- dep-audit: outdated, vulnerabilities, licenses, dependency tree
|
|
12
|
+
- docker-mgmt: container lifecycle, compose, build, logs
|
|
13
|
+
- env-manager: .env management, encryption, diff, sync, validation
|
|
14
|
+
- git-workflow: branches, rebase, cherry-pick, merge-check, stash
|
|
15
|
+
- log-parser: filter, tail, stats, highlight, extract, JSON parsing
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Updated dependencies
|
|
20
|
+
- @redredchen01/cli-core@0.2.0
|
|
21
|
+
|
|
22
|
+
All notable changes to @redredchen01/env-manager will be documented in this file.
|
|
23
|
+
|
|
24
|
+
Generated by [git-cliff](https://github.com/orhun/git-cliff).
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# @foundry/env-manager
|
|
2
|
+
|
|
3
|
+
> CLI toolkit for .env file management — diff, validate, sync, encrypt/decrypt
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@foundry/env-manager)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @foundry/env-manager
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
env-manager show # view vars (secrets masked)
|
|
18
|
+
env-manager validate # check for missing vars
|
|
19
|
+
env-manager diff .env .env.prod # compare environments
|
|
20
|
+
env-manager sync # update .env.example
|
|
21
|
+
env-manager encrypt # encrypt .env → .env.enc
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
| Command | Description |
|
|
27
|
+
|---------|-------------|
|
|
28
|
+
| `show` | Display .env with secret masking |
|
|
29
|
+
| `diff` | Compare two .env files |
|
|
30
|
+
| `validate` | Check against .env.example |
|
|
31
|
+
| `sync` | Generate .env.example from .env |
|
|
32
|
+
| `encrypt` | Encrypt .env (AES-256-CBC) |
|
|
33
|
+
| `decrypt` | Decrypt .env.enc |
|
|
34
|
+
| `init` | Create .env from template |
|
|
35
|
+
|
|
36
|
+
## As Claude Skill
|
|
37
|
+
|
|
38
|
+
> "Show me the env vars in this project"
|
|
39
|
+
> "Am I missing any environment variables?"
|
|
40
|
+
> "What's different between staging and production env?"
|
|
41
|
+
> "Encrypt the .env file for safe storage"
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: env-manager
|
|
3
|
+
description: Use when the user needs help with .env files — comparing environments, checking for missing variables, syncing .env to .env.example, encrypting secrets, or viewing env vars safely. Triggers on ".env", "environment variable", "missing env", "env diff", "encrypt secrets", "env example", "validate env", "sync env", "show env", "env config".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# env-manager
|
|
7
|
+
|
|
8
|
+
CLI toolkit for .env file management. Compare environments, validate completeness, sync to .env.example, encrypt/decrypt secrets, and safely view variables.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
When the user works with .env files or environment configuration, use these commands. Sensitive values are automatically masked unless `--reveal` is used.
|
|
13
|
+
|
|
14
|
+
## CLI Commands
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
env-manager <command> [options]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
| Command | Description |
|
|
21
|
+
|---------|-------------|
|
|
22
|
+
| `show` | Display .env with automatic secret masking |
|
|
23
|
+
| `diff` | Compare two .env files (added/removed/changed) |
|
|
24
|
+
| `validate` | Check .env against .env.example for missing vars |
|
|
25
|
+
| `sync` | Generate .env.example from .env (strip values) |
|
|
26
|
+
| `encrypt` | Encrypt .env with AES-256-CBC (OpenSSL) |
|
|
27
|
+
| `decrypt` | Decrypt .env.enc back to .env |
|
|
28
|
+
| `init` | Create .env from .env.example |
|
|
29
|
+
|
|
30
|
+
## Common Workflows
|
|
31
|
+
|
|
32
|
+
### "What env vars do I have?"
|
|
33
|
+
```bash
|
|
34
|
+
env-manager show # masked secrets
|
|
35
|
+
env-manager show .env.production # specific file
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### "Am I missing any env vars?"
|
|
39
|
+
```bash
|
|
40
|
+
env-manager validate # check against .env.example
|
|
41
|
+
env-manager validate --no-empty # also flag empty values
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### "What's different between staging and prod?"
|
|
45
|
+
```bash
|
|
46
|
+
env-manager diff .env.staging .env.production --values
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### "Update .env.example after adding new vars"
|
|
50
|
+
```bash
|
|
51
|
+
env-manager sync # .env → .env.example (strip values)
|
|
52
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Command: decrypt — 解密 .env.enc 檔案
|
|
3
|
+
# Part of @foundry/env-manager
|
|
4
|
+
|
|
5
|
+
# shellcheck disable=SC2034
|
|
6
|
+
COMMAND_NAME="decrypt"
|
|
7
|
+
# shellcheck disable=SC2034
|
|
8
|
+
COMMAND_DESC="Decrypt an encrypted .env file (AES-256-CBC)"
|
|
9
|
+
|
|
10
|
+
show_help() {
|
|
11
|
+
cat << 'EOF'
|
|
12
|
+
Usage: env-manager decrypt <file> [options]
|
|
13
|
+
|
|
14
|
+
Decrypt a .env.enc file back to .env (AES-256-CBC, OpenSSL).
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
file Encrypted file (e.g., .env.enc)
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
-o, --output FILE Output file (default: strip .enc suffix)
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
env-manager decrypt .env.enc
|
|
25
|
+
env-manager decrypt secrets.enc -o .env.production
|
|
26
|
+
EOF
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
run() {
|
|
30
|
+
local file="" output=""
|
|
31
|
+
|
|
32
|
+
while [ $# -gt 0 ]; do
|
|
33
|
+
case "$1" in
|
|
34
|
+
--help|-h) show_help; return 0 ;;
|
|
35
|
+
--output|-o) shift; output="${1:?--output requires a file}" ;;
|
|
36
|
+
-*) echo "Error: unknown option '$1'" >&2; return 1 ;;
|
|
37
|
+
*) file="$1" ;;
|
|
38
|
+
esac
|
|
39
|
+
shift
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
[ -z "$file" ] && { echo "Error: encrypted file required." >&2; return 1; }
|
|
43
|
+
[ ! -f "$file" ] && { echo "Error: '$file' not found." >&2; return 1; }
|
|
44
|
+
|
|
45
|
+
if ! command -v openssl >/dev/null 2>&1; then
|
|
46
|
+
echo "Error: openssl is not installed." >&2
|
|
47
|
+
return 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Auto-detect output name
|
|
51
|
+
if [ -z "$output" ]; then
|
|
52
|
+
output="${file%.enc}"
|
|
53
|
+
[ "$output" = "$file" ] && output="${file}.decrypted"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
echo "Decrypting: $file → $output"
|
|
57
|
+
echo "Enter passphrase:"
|
|
58
|
+
|
|
59
|
+
if openssl enc -aes-256-cbc -d -salt -pbkdf2 -in "$file" -out "$output" 2>/dev/null; then
|
|
60
|
+
echo ""
|
|
61
|
+
echo "Decrypted: $output"
|
|
62
|
+
local count
|
|
63
|
+
count="$(grep -c '=' "$output" 2>/dev/null || echo 0)"
|
|
64
|
+
echo "($count variables)"
|
|
65
|
+
else
|
|
66
|
+
echo "Error: decryption failed (wrong passphrase?)." >&2
|
|
67
|
+
[ -f "$output" ] && rm "$output"
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Command: diff — 比較兩個 .env 檔案
|
|
3
|
+
# Part of @foundry/env-manager
|
|
4
|
+
|
|
5
|
+
# shellcheck disable=SC2034
|
|
6
|
+
COMMAND_NAME="diff"
|
|
7
|
+
# shellcheck disable=SC2034
|
|
8
|
+
COMMAND_DESC="Compare two .env files and show differences"
|
|
9
|
+
|
|
10
|
+
show_help() {
|
|
11
|
+
cat << 'EOF'
|
|
12
|
+
Usage: env-manager diff <file1> <file2> [options]
|
|
13
|
+
|
|
14
|
+
Compare two .env files. Shows keys that are added, removed, or have different values.
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
file1 First .env file (e.g., .env)
|
|
18
|
+
file2 Second .env file (e.g., .env.production)
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--keys-only Only compare keys (ignore values)
|
|
22
|
+
--values Also show differing values (masked by default)
|
|
23
|
+
-h, --help Show this help
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
env-manager diff .env .env.example
|
|
27
|
+
env-manager diff .env .env.production --values
|
|
28
|
+
env-manager diff .env.staging .env.production --keys-only
|
|
29
|
+
EOF
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
run() {
|
|
33
|
+
local file1="" file2="" keys_only=false show_values=false
|
|
34
|
+
|
|
35
|
+
while [ $# -gt 0 ]; do
|
|
36
|
+
case "$1" in
|
|
37
|
+
--help|-h) show_help; return 0 ;;
|
|
38
|
+
--keys-only) keys_only=true ;;
|
|
39
|
+
--values) show_values=true ;;
|
|
40
|
+
-*) echo "Error: unknown option '$1'" >&2; return 1 ;;
|
|
41
|
+
*)
|
|
42
|
+
if [ -z "$file1" ]; then file1="$1"
|
|
43
|
+
elif [ -z "$file2" ]; then file2="$1"
|
|
44
|
+
else echo "Error: too many arguments." >&2; return 1
|
|
45
|
+
fi ;;
|
|
46
|
+
esac
|
|
47
|
+
shift
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
[ -z "$file1" ] || [ -z "$file2" ] && { echo "Error: two files required." >&2; return 1; }
|
|
51
|
+
[ ! -f "$file1" ] && { echo "Error: '$file1' not found." >&2; return 1; }
|
|
52
|
+
[ ! -f "$file2" ] && { echo "Error: '$file2' not found." >&2; return 1; }
|
|
53
|
+
|
|
54
|
+
echo "Comparing: $file1 ↔ $file2"
|
|
55
|
+
echo ""
|
|
56
|
+
|
|
57
|
+
# Extract keys from each file
|
|
58
|
+
local keys1 keys2
|
|
59
|
+
keys1="$(_extract_keys "$file1")"
|
|
60
|
+
keys2="$(_extract_keys "$file2")"
|
|
61
|
+
|
|
62
|
+
local only_in_1="" only_in_2="" in_both="" changed=""
|
|
63
|
+
|
|
64
|
+
# Keys only in file1
|
|
65
|
+
while IFS= read -r key; do
|
|
66
|
+
[ -z "$key" ] && continue
|
|
67
|
+
if ! echo "$keys2" | grep -qx "$key"; then
|
|
68
|
+
only_in_1="${only_in_1}${key}\n"
|
|
69
|
+
else
|
|
70
|
+
in_both="${in_both}${key}\n"
|
|
71
|
+
fi
|
|
72
|
+
done <<< "$keys1"
|
|
73
|
+
|
|
74
|
+
# Keys only in file2
|
|
75
|
+
while IFS= read -r key; do
|
|
76
|
+
[ -z "$key" ] && continue
|
|
77
|
+
if ! echo "$keys1" | grep -qx "$key"; then
|
|
78
|
+
only_in_2="${only_in_2}${key}\n"
|
|
79
|
+
fi
|
|
80
|
+
done <<< "$keys2"
|
|
81
|
+
|
|
82
|
+
# Value differences (for keys in both)
|
|
83
|
+
if [ "$keys_only" = false ]; then
|
|
84
|
+
while IFS= read -r key; do
|
|
85
|
+
[ -z "$key" ] && continue
|
|
86
|
+
local v1 v2
|
|
87
|
+
v1="$(_get_value "$file1" "$key")"
|
|
88
|
+
v2="$(_get_value "$file2" "$key")"
|
|
89
|
+
if [ "$v1" != "$v2" ]; then
|
|
90
|
+
changed="${changed}${key}\n"
|
|
91
|
+
fi
|
|
92
|
+
done <<< "$(echo -e "$in_both")"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Output
|
|
96
|
+
if [ -n "$only_in_1" ]; then
|
|
97
|
+
echo "Only in $file1:"
|
|
98
|
+
echo -e "$only_in_1" | while IFS= read -r k; do
|
|
99
|
+
[ -n "$k" ] && echo " - $k"
|
|
100
|
+
done
|
|
101
|
+
echo ""
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [ -n "$only_in_2" ]; then
|
|
105
|
+
echo "Only in $file2:"
|
|
106
|
+
echo -e "$only_in_2" | while IFS= read -r k; do
|
|
107
|
+
[ -n "$k" ] && echo " + $k"
|
|
108
|
+
done
|
|
109
|
+
echo ""
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
if [ -n "$changed" ]; then
|
|
113
|
+
echo "Different values:"
|
|
114
|
+
echo -e "$changed" | while IFS= read -r k; do
|
|
115
|
+
[ -z "$k" ] && continue
|
|
116
|
+
if [ "$show_values" = true ]; then
|
|
117
|
+
echo " ~ $k: $(_get_value "$file1" "$k") → $(_get_value "$file2" "$k")"
|
|
118
|
+
else
|
|
119
|
+
echo " ~ $k"
|
|
120
|
+
fi
|
|
121
|
+
done
|
|
122
|
+
echo ""
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if [ -z "$only_in_1" ] && [ -z "$only_in_2" ] && [ -z "$changed" ]; then
|
|
126
|
+
echo "Files are identical (same keys and values)."
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_extract_keys() {
|
|
131
|
+
grep -v '^#' "$1" | grep -v '^$' | grep '=' | sed 's/=.*//' | sort
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_get_value() {
|
|
135
|
+
grep "^${2}=" "$1" 2>/dev/null | head -1 | sed "s/^${2}=//"
|
|
136
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Command: encrypt — 加密 .env 檔案
|
|
3
|
+
# Part of @foundry/env-manager
|
|
4
|
+
|
|
5
|
+
# shellcheck disable=SC2034
|
|
6
|
+
COMMAND_NAME="encrypt"
|
|
7
|
+
# shellcheck disable=SC2034
|
|
8
|
+
COMMAND_DESC="Encrypt .env file with a passphrase (AES-256-CBC)"
|
|
9
|
+
|
|
10
|
+
show_help() {
|
|
11
|
+
cat << 'EOF'
|
|
12
|
+
Usage: env-manager encrypt [file] [options]
|
|
13
|
+
|
|
14
|
+
Encrypt a .env file using AES-256-CBC (OpenSSL). Produces a .env.enc file.
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
file File to encrypt (default: .env)
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
-o, --output FILE Output file (default: {input}.enc)
|
|
21
|
+
--delete Delete original after encryption
|
|
22
|
+
-h, --help Show this help
|
|
23
|
+
|
|
24
|
+
Prerequisites:
|
|
25
|
+
OpenSSL must be installed (pre-installed on macOS and most Linux).
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
env-manager encrypt
|
|
29
|
+
env-manager encrypt .env.production
|
|
30
|
+
env-manager encrypt .env -o secrets.enc
|
|
31
|
+
env-manager encrypt --delete
|
|
32
|
+
EOF
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
run() {
|
|
36
|
+
local file=".env" output="" delete_original=false
|
|
37
|
+
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--help|-h) show_help; return 0 ;;
|
|
41
|
+
--output|-o) shift; output="${1:?--output requires a file}" ;;
|
|
42
|
+
--delete) delete_original=true ;;
|
|
43
|
+
-*) echo "Error: unknown option '$1'" >&2; return 1 ;;
|
|
44
|
+
*) file="$1" ;;
|
|
45
|
+
esac
|
|
46
|
+
shift
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
[ ! -f "$file" ] && { echo "Error: '$file' not found." >&2; return 1; }
|
|
50
|
+
|
|
51
|
+
if ! command -v openssl >/dev/null 2>&1; then
|
|
52
|
+
echo "Error: openssl is not installed." >&2
|
|
53
|
+
return 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
[ -z "$output" ] && output="${file}.enc"
|
|
57
|
+
|
|
58
|
+
echo "Encrypting: $file → $output"
|
|
59
|
+
echo "Enter passphrase (you'll need this to decrypt):"
|
|
60
|
+
|
|
61
|
+
if openssl enc -aes-256-cbc -salt -pbkdf2 -in "$file" -out "$output" 2>/dev/null; then
|
|
62
|
+
echo ""
|
|
63
|
+
echo "Encrypted: $output"
|
|
64
|
+
if [ "$delete_original" = true ]; then
|
|
65
|
+
rm "$file"
|
|
66
|
+
echo "Deleted original: $file"
|
|
67
|
+
fi
|
|
68
|
+
echo ""
|
|
69
|
+
echo "To decrypt: env-manager decrypt $output"
|
|
70
|
+
else
|
|
71
|
+
echo "Error: encryption failed." >&2
|
|
72
|
+
[ -f "$output" ] && rm "$output"
|
|
73
|
+
return 1
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Command: init — 初始化 .env 檔案
|
|
3
|
+
# Part of @foundry/env-manager
|
|
4
|
+
|
|
5
|
+
# shellcheck disable=SC2034
|
|
6
|
+
COMMAND_NAME="init"
|
|
7
|
+
# shellcheck disable=SC2034
|
|
8
|
+
COMMAND_DESC="Initialize .env from .env.example with interactive prompts"
|
|
9
|
+
|
|
10
|
+
show_help() {
|
|
11
|
+
cat << 'EOF'
|
|
12
|
+
Usage: env-manager init [options]
|
|
13
|
+
|
|
14
|
+
Create .env from .env.example. Copies the template and optionally prompts for values.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--example FILE Template file (default: .env.example)
|
|
18
|
+
--output FILE Output file (default: .env)
|
|
19
|
+
--force Overwrite existing .env
|
|
20
|
+
--no-prompt Copy as-is without prompting
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
env-manager init
|
|
25
|
+
env-manager init --example .env.template
|
|
26
|
+
env-manager init --force
|
|
27
|
+
env-manager init --no-prompt
|
|
28
|
+
EOF
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
run() {
|
|
32
|
+
local example=".env.example" output=".env" force=false no_prompt=false
|
|
33
|
+
|
|
34
|
+
while [ $# -gt 0 ]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--help|-h) show_help; return 0 ;;
|
|
37
|
+
--example) shift; example="${1:?--example requires a file}" ;;
|
|
38
|
+
--output) shift; output="${1:?--output requires a file}" ;;
|
|
39
|
+
--force) force=true ;;
|
|
40
|
+
--no-prompt) no_prompt=true ;;
|
|
41
|
+
*) echo "Error: unknown option '$1'" >&2; return 1 ;;
|
|
42
|
+
esac
|
|
43
|
+
shift
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
[ ! -f "$example" ] && { echo "Error: '$example' not found." >&2; return 1; }
|
|
47
|
+
|
|
48
|
+
if [ -f "$output" ] && [ "$force" = false ]; then
|
|
49
|
+
echo "Error: '$output' already exists. Use --force to overwrite." >&2
|
|
50
|
+
return 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [ "$no_prompt" = true ]; then
|
|
54
|
+
cp "$example" "$output"
|
|
55
|
+
local count
|
|
56
|
+
count="$(grep -c '=' "$output" 2>/dev/null || echo 0)"
|
|
57
|
+
echo "Created: $output ($count variables from $example)"
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# Interactive: copy with prompts for empty values
|
|
62
|
+
echo "Initializing $output from $example"
|
|
63
|
+
echo "(Press Enter to keep default, or type a new value)"
|
|
64
|
+
echo ""
|
|
65
|
+
|
|
66
|
+
local result=""
|
|
67
|
+
while IFS= read -r line; do
|
|
68
|
+
case "$line" in
|
|
69
|
+
""|\#*)
|
|
70
|
+
result="${result}${line}\n"
|
|
71
|
+
continue
|
|
72
|
+
;;
|
|
73
|
+
esac
|
|
74
|
+
|
|
75
|
+
local key="${line%%=*}"
|
|
76
|
+
local value="${line#*=}"
|
|
77
|
+
|
|
78
|
+
if [ -z "$value" ] || [ "$value" = '""' ] || [ "$value" = "''" ]; then
|
|
79
|
+
printf " %s = " "$key"
|
|
80
|
+
local new_value
|
|
81
|
+
read -r new_value < /dev/tty 2>/dev/null || new_value=""
|
|
82
|
+
if [ -n "$new_value" ]; then
|
|
83
|
+
result="${result}${key}=${new_value}\n"
|
|
84
|
+
else
|
|
85
|
+
result="${result}${line}\n"
|
|
86
|
+
fi
|
|
87
|
+
else
|
|
88
|
+
result="${result}${line}\n"
|
|
89
|
+
fi
|
|
90
|
+
done < "$example"
|
|
91
|
+
|
|
92
|
+
echo -e "$result" > "$output"
|
|
93
|
+
echo ""
|
|
94
|
+
echo "Created: $output"
|
|
95
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Command: show — 顯示 .env 內容(遮蔽 secrets)
|
|
3
|
+
# Part of @foundry/env-manager
|
|
4
|
+
|
|
5
|
+
# shellcheck disable=SC2034
|
|
6
|
+
COMMAND_NAME="show"
|
|
7
|
+
# shellcheck disable=SC2034
|
|
8
|
+
COMMAND_DESC="Display .env contents with secret masking"
|
|
9
|
+
|
|
10
|
+
show_help() {
|
|
11
|
+
cat << 'EOF'
|
|
12
|
+
Usage: env-manager show [file] [options]
|
|
13
|
+
|
|
14
|
+
Display .env file contents with automatic secret masking.
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
file Path to .env file (default: .env)
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--reveal Show actual values (no masking)
|
|
21
|
+
--keys Show only keys, no values
|
|
22
|
+
--sort Sort alphabetically
|
|
23
|
+
-h, --help Show this help
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
env-manager show
|
|
27
|
+
env-manager show .env.production
|
|
28
|
+
env-manager show --reveal
|
|
29
|
+
env-manager show --keys
|
|
30
|
+
EOF
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
SECRET_PATTERNS="PASSWORD|SECRET|KEY|TOKEN|API_KEY|PRIVATE|CREDENTIAL|AUTH"
|
|
34
|
+
|
|
35
|
+
run() {
|
|
36
|
+
local file=".env" reveal=false keys_only=false do_sort=false
|
|
37
|
+
|
|
38
|
+
while [ $# -gt 0 ]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
--help|-h) show_help; return 0 ;;
|
|
41
|
+
--reveal) reveal=true ;;
|
|
42
|
+
--keys) keys_only=true ;;
|
|
43
|
+
--sort) do_sort=true ;;
|
|
44
|
+
-*) echo "Error: unknown option '$1'" >&2; return 1 ;;
|
|
45
|
+
*) file="$1" ;;
|
|
46
|
+
esac
|
|
47
|
+
shift
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
if [ ! -f "$file" ]; then
|
|
51
|
+
echo "Error: '$file' not found." >&2
|
|
52
|
+
return 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
local lines
|
|
56
|
+
if [ "$do_sort" = true ]; then
|
|
57
|
+
lines="$(sort "$file")"
|
|
58
|
+
else
|
|
59
|
+
lines="$(cat "$file")"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
local count=0
|
|
63
|
+
while IFS= read -r line; do
|
|
64
|
+
# Skip empty lines and comments
|
|
65
|
+
case "$line" in
|
|
66
|
+
""|\#*) echo "$line"; continue ;;
|
|
67
|
+
esac
|
|
68
|
+
|
|
69
|
+
local key value
|
|
70
|
+
key="${line%%=*}"
|
|
71
|
+
value="${line#*=}"
|
|
72
|
+
count=$((count + 1))
|
|
73
|
+
|
|
74
|
+
if [ "$keys_only" = true ]; then
|
|
75
|
+
echo "$key"
|
|
76
|
+
elif [ "$reveal" = true ]; then
|
|
77
|
+
echo "$line"
|
|
78
|
+
else
|
|
79
|
+
# Mask values for sensitive keys
|
|
80
|
+
if echo "$key" | grep -qiE "$SECRET_PATTERNS"; then
|
|
81
|
+
echo "${key}=********"
|
|
82
|
+
else
|
|
83
|
+
echo "$line"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
done <<< "$lines"
|
|
87
|
+
|
|
88
|
+
echo ""
|
|
89
|
+
echo "($count variables in $file)"
|
|
90
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Command: sync — 同步 .env 到 .env.example
|
|
3
|
+
# Part of @foundry/env-manager
|
|
4
|
+
|
|
5
|
+
# shellcheck disable=SC2034
|
|
6
|
+
COMMAND_NAME="sync"
|
|
7
|
+
# shellcheck disable=SC2034
|
|
8
|
+
COMMAND_DESC="Sync .env keys to .env.example (strip values)"
|
|
9
|
+
|
|
10
|
+
show_help() {
|
|
11
|
+
cat << 'EOF'
|
|
12
|
+
Usage: env-manager sync [options]
|
|
13
|
+
|
|
14
|
+
Generate or update .env.example from .env, keeping keys but stripping values.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--source FILE Source file (default: .env)
|
|
18
|
+
--target FILE Target file (default: .env.example)
|
|
19
|
+
--preserve Keep existing values in target (only add new keys)
|
|
20
|
+
--dry-run Show what would change without writing
|
|
21
|
+
-h, --help Show this help
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
env-manager sync
|
|
25
|
+
env-manager sync --source .env.production --target .env.prod.example
|
|
26
|
+
env-manager sync --preserve
|
|
27
|
+
env-manager sync --dry-run
|
|
28
|
+
EOF
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
run() {
|
|
32
|
+
local source=".env" target=".env.example" preserve=false dry_run=false
|
|
33
|
+
|
|
34
|
+
while [ $# -gt 0 ]; do
|
|
35
|
+
case "$1" in
|
|
36
|
+
--help|-h) show_help; return 0 ;;
|
|
37
|
+
--source) shift; source="${1:?--source requires a file}" ;;
|
|
38
|
+
--target) shift; target="${1:?--target requires a file}" ;;
|
|
39
|
+
--preserve) preserve=true ;;
|
|
40
|
+
--dry-run) dry_run=true ;;
|
|
41
|
+
*) echo "Error: unknown option '$1'" >&2; return 1 ;;
|
|
42
|
+
esac
|
|
43
|
+
shift
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
[ ! -f "$source" ] && { echo "Error: '$source' not found." >&2; return 1; }
|
|
47
|
+
|
|
48
|
+
echo "Syncing: $source → $target"
|
|
49
|
+
echo ""
|
|
50
|
+
|
|
51
|
+
local output="" added=0 total=0
|
|
52
|
+
|
|
53
|
+
while IFS= read -r line; do
|
|
54
|
+
case "$line" in
|
|
55
|
+
""|\#*)
|
|
56
|
+
output="${output}${line}\n"
|
|
57
|
+
continue
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
60
|
+
|
|
61
|
+
local key="${line%%=*}"
|
|
62
|
+
total=$((total + 1))
|
|
63
|
+
|
|
64
|
+
if [ "$preserve" = true ] && [ -f "$target" ]; then
|
|
65
|
+
local existing
|
|
66
|
+
existing="$(grep "^${key}=" "$target" 2>/dev/null | head -1)"
|
|
67
|
+
if [ -n "$existing" ]; then
|
|
68
|
+
output="${output}${existing}\n"
|
|
69
|
+
continue
|
|
70
|
+
fi
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Strip value, keep key with empty value
|
|
74
|
+
output="${output}${key}=\n"
|
|
75
|
+
|
|
76
|
+
# Check if this is a new key
|
|
77
|
+
if [ -f "$target" ] && ! grep -q "^${key}=" "$target" 2>/dev/null; then
|
|
78
|
+
added=$((added + 1))
|
|
79
|
+
fi
|
|
80
|
+
done < "$source"
|
|
81
|
+
|
|
82
|
+
if [ "$dry_run" = true ]; then
|
|
83
|
+
echo "Would write to $target ($total keys, $added new):"
|
|
84
|
+
echo ""
|
|
85
|
+
echo -e "$output"
|
|
86
|
+
echo "(Dry run — no file written)"
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
echo -e "$output" > "$target"
|
|
91
|
+
echo "Written: $target ($total keys, $added new)"
|
|
92
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Command: validate — 驗證 .env 完整性
|
|
3
|
+
# Part of @foundry/env-manager
|
|
4
|
+
|
|
5
|
+
# shellcheck disable=SC2034
|
|
6
|
+
COMMAND_NAME="validate"
|
|
7
|
+
# shellcheck disable=SC2034
|
|
8
|
+
COMMAND_DESC="Validate .env against .env.example for missing/extra variables"
|
|
9
|
+
|
|
10
|
+
show_help() {
|
|
11
|
+
cat << 'EOF'
|
|
12
|
+
Usage: env-manager validate [file] [options]
|
|
13
|
+
|
|
14
|
+
Check .env against .env.example to find missing or extra variables.
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
file Path to .env file (default: .env)
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--example FILE Reference file (default: .env.example)
|
|
21
|
+
--strict Fail on extra variables (not just missing)
|
|
22
|
+
--no-empty Fail on empty values
|
|
23
|
+
-h, --help Show this help
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
env-manager validate
|
|
27
|
+
env-manager validate .env.production
|
|
28
|
+
env-manager validate --example .env.required --strict
|
|
29
|
+
env-manager validate --no-empty
|
|
30
|
+
EOF
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
run() {
|
|
34
|
+
local file=".env" example=".env.example" strict=false no_empty=false
|
|
35
|
+
|
|
36
|
+
while [ $# -gt 0 ]; do
|
|
37
|
+
case "$1" in
|
|
38
|
+
--help|-h) show_help; return 0 ;;
|
|
39
|
+
--example) shift; example="${1:?--example requires a file}" ;;
|
|
40
|
+
--strict) strict=true ;;
|
|
41
|
+
--no-empty) no_empty=true ;;
|
|
42
|
+
-*) echo "Error: unknown option '$1'" >&2; return 1 ;;
|
|
43
|
+
*) file="$1" ;;
|
|
44
|
+
esac
|
|
45
|
+
shift
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
[ ! -f "$file" ] && { echo "Error: '$file' not found." >&2; return 1; }
|
|
49
|
+
[ ! -f "$example" ] && { echo "Error: '$example' not found. Create it or use --example." >&2; return 1; }
|
|
50
|
+
|
|
51
|
+
echo "Validating: $file against $example"
|
|
52
|
+
echo ""
|
|
53
|
+
|
|
54
|
+
local env_keys example_keys
|
|
55
|
+
env_keys="$(_extract_keys "$file")"
|
|
56
|
+
example_keys="$(_extract_keys "$example")"
|
|
57
|
+
|
|
58
|
+
local errors=0 warnings=0
|
|
59
|
+
|
|
60
|
+
# Missing keys (in example but not in env)
|
|
61
|
+
local missing=""
|
|
62
|
+
while IFS= read -r key; do
|
|
63
|
+
[ -z "$key" ] && continue
|
|
64
|
+
if ! echo "$env_keys" | grep -qx "$key"; then
|
|
65
|
+
missing="${missing} ✗ ${key}\n"
|
|
66
|
+
errors=$((errors + 1))
|
|
67
|
+
fi
|
|
68
|
+
done <<< "$example_keys"
|
|
69
|
+
|
|
70
|
+
if [ -n "$missing" ]; then
|
|
71
|
+
echo "Missing (required but not set):"
|
|
72
|
+
echo -e "$missing"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Extra keys (in env but not in example)
|
|
76
|
+
local extra=""
|
|
77
|
+
while IFS= read -r key; do
|
|
78
|
+
[ -z "$key" ] && continue
|
|
79
|
+
if ! echo "$example_keys" | grep -qx "$key"; then
|
|
80
|
+
extra="${extra} ? ${key}\n"
|
|
81
|
+
[ "$strict" = true ] && errors=$((errors + 1)) || warnings=$((warnings + 1))
|
|
82
|
+
fi
|
|
83
|
+
done <<< "$env_keys"
|
|
84
|
+
|
|
85
|
+
if [ -n "$extra" ]; then
|
|
86
|
+
if [ "$strict" = true ]; then
|
|
87
|
+
echo "Extra (not in $example — strict mode):"
|
|
88
|
+
else
|
|
89
|
+
echo "Extra (not in $example — informational):"
|
|
90
|
+
fi
|
|
91
|
+
echo -e "$extra"
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Empty values check
|
|
95
|
+
if [ "$no_empty" = true ]; then
|
|
96
|
+
local empty=""
|
|
97
|
+
while IFS= read -r line; do
|
|
98
|
+
case "$line" in ""|\#*) continue ;; esac
|
|
99
|
+
local key="${line%%=*}" value="${line#*=}"
|
|
100
|
+
if [ -z "$value" ] || [ "$value" = '""' ] || [ "$value" = "''" ]; then
|
|
101
|
+
empty="${empty} ⚠ ${key} (empty)\n"
|
|
102
|
+
errors=$((errors + 1))
|
|
103
|
+
fi
|
|
104
|
+
done < "$file"
|
|
105
|
+
|
|
106
|
+
if [ -n "$empty" ]; then
|
|
107
|
+
echo "Empty values:"
|
|
108
|
+
echo -e "$empty"
|
|
109
|
+
fi
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# Summary
|
|
113
|
+
echo "Result: $errors errors, $warnings warnings"
|
|
114
|
+
[ "$errors" -gt 0 ] && return 1
|
|
115
|
+
echo "✓ Validation passed."
|
|
116
|
+
return 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_extract_keys() {
|
|
120
|
+
grep -v '^#' "$1" | grep -v '^$' | grep '=' | sed 's/=.*//' | sort
|
|
121
|
+
}
|
package/cli/main.sh
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SKILL_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
# Source cli-core libraries
|
|
7
|
+
# shellcheck source=/dev/null
|
|
8
|
+
CLI_CORE="${SKILL_ROOT}/../cli-core/lib"
|
|
9
|
+
if [ -d "$CLI_CORE" ]; then
|
|
10
|
+
source "${CLI_CORE}/dispatcher.sh"
|
|
11
|
+
source "${CLI_CORE}/logger.sh"
|
|
12
|
+
source "${CLI_CORE}/config.sh"
|
|
13
|
+
source "${CLI_CORE}/version.sh"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
export SKILL_NAME="env-manager"
|
|
17
|
+
export VERSION
|
|
18
|
+
VERSION=$(foundry::version::get "$SKILL_ROOT" 2>/dev/null || echo "0.0.0")
|
|
19
|
+
export COMMANDS_DIR="${SKILL_ROOT}/cli/commands"
|
|
20
|
+
|
|
21
|
+
foundry::logger::parse_flags "$@"
|
|
22
|
+
foundry::dispatcher::dispatch "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@redredchen01/env-manager",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI toolkit for .env file management — diff, validate, sync, encrypt/decrypt",
|
|
5
|
+
"bin": {
|
|
6
|
+
"env-manager": "./bin/env-manager.sh"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"cli/",
|
|
11
|
+
"scripts/",
|
|
12
|
+
"references/",
|
|
13
|
+
"assets/",
|
|
14
|
+
"SKILL.md",
|
|
15
|
+
"README.md",
|
|
16
|
+
"CHANGELOG.md"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"claude-code",
|
|
20
|
+
"skill",
|
|
21
|
+
"cli",
|
|
22
|
+
"env",
|
|
23
|
+
"dotenv",
|
|
24
|
+
"config",
|
|
25
|
+
"secrets"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/redredchen01/skill-foundry",
|
|
34
|
+
"directory": "packages/env-manager"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@redredchen01/cli-core": "*"
|
|
41
|
+
},
|
|
42
|
+
"skillFoundry": {
|
|
43
|
+
"concept": ".env file management — show, diff, validate, sync, encrypt, decrypt, init",
|
|
44
|
+
"phase": "generate",
|
|
45
|
+
"capabilities": [
|
|
46
|
+
"show",
|
|
47
|
+
"diff",
|
|
48
|
+
"validate",
|
|
49
|
+
"sync",
|
|
50
|
+
"encrypt",
|
|
51
|
+
"decrypt",
|
|
52
|
+
"init"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|