envsafe 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 65634c2098bf1fa34dd0b3ef2c127cd4c4f3a1390a05f3ab2f6a58828692efbb
4
+ data.tar.gz: 8cf9215b84fcf108b6eba6bdc9af69c7f7b35422e068c3c4174e36b37f79cd02
5
+ SHA512:
6
+ metadata.gz: e2a80fa8cfa100525f2a11c767089a80dcfa51de22c93ded2291f9a401872fef347940378f18a1dd614d09402ff5504c582be0201ab150f536c79d4585445b33
7
+ data.tar.gz: 35f51b8ab9ab3de95ca9807b1301c250eb7f465101d5987a96755ed42a315e831ad53f9453a881e07d090538443e63b26c79f2ddcbb7be410787db181acced68
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Kaiser Sakhi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # Envsafe
2
+
3
+ A Ruby CLI tool for safely managing and versioning your `.env` files. Envsafe provides backup, restore, and history management capabilities to prevent accidental loss of environment variables.
4
+
5
+ ## Features
6
+
7
+ - 📁 **Backup Management**: Create tagged or automatic backups of your `.env` files
8
+ - 🔄 **Restore System**: Restore any previous backup by tag or index
9
+ - 📋 **History Tracking**: List all backups with timestamps and tags
10
+ - ⏪ **Undo Operations**: Quickly undo the last write operation
11
+ - 👀 **File Preview**: View contents of any backup without restoring
12
+ - 🗑️ **Selective Cleanup**: Delete specific backups or clear all history
13
+ - 🔒 **Git Integration**: Automatically add `.envsafe` to your `.gitignore`
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'envsafe'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```bash
26
+ $ bundle install
27
+ ```
28
+
29
+ Or install it yourself as:
30
+
31
+ ```bash
32
+ $ gem install envsafe
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Basic Commands
38
+
39
+ #### Backup your .env file
40
+
41
+ ```bash
42
+ # Create a backup with automatic timestamp
43
+ $ envsafe backup
44
+
45
+ # Create a tagged backup for easy identification
46
+ $ envsafe backup --tag "before-production-deploy"
47
+ $ envsafe backup -t "pre-feature-update"
48
+ ```
49
+
50
+ #### List all backups
51
+
52
+ ```bash
53
+ # Show all backups
54
+ $ envsafe list
55
+
56
+ # Limit the number of backups shown
57
+ $ envsafe list 5
58
+ ```
59
+
60
+ #### Restore a backup
61
+
62
+ ```bash
63
+ # Restore by tag
64
+ $ envsafe restore --tag "before-production-deploy"
65
+ $ envsafe restore -t "pre-feature-update"
66
+
67
+ # Restore by stack index (use 'list' command to see indices)
68
+ $ envsafe restore --sindex 0
69
+ $ envsafe restore -i 2
70
+ ```
71
+
72
+ #### Quick undo
73
+
74
+ ```bash
75
+ # Undo the last write operation to .env
76
+ $ envsafe undo
77
+ ```
78
+
79
+ ### Advanced Commands
80
+
81
+ #### View backup contents
82
+
83
+ ```bash
84
+ # Show contents by tag
85
+ $ envsafe show --tag "production-config"
86
+ $ envsafe show -t "staging-setup"
87
+
88
+ # Show contents by stack index
89
+ $ envsafe show --sindex 0
90
+ $ envsafe show -i 1
91
+ ```
92
+
93
+ #### Delete specific backups
94
+
95
+ ```bash
96
+ # Delete by tag
97
+ $ envsafe delete --tag "old-config"
98
+ $ envsafe delete -t "temporary-backup"
99
+
100
+ # Delete by stack index
101
+ $ envsafe delete --sindex 3
102
+ $ envsafe delete -i 0
103
+ ```
104
+
105
+ #### Cleanup operations
106
+
107
+ ```bash
108
+ # Delete all backups
109
+ $ envsafe clear
110
+
111
+ # Add .envsafe directory to .gitignore
112
+ $ envsafe ignore
113
+ ```
114
+
115
+ ## Command Reference
116
+
117
+ | Command | Description | Options |
118
+ | -------------- | ------------------------------------------ | --------------------------------------------------------------------------------- |
119
+ | `backup` | Create a backup of current `.env` file | `-t, --tag TAG` - Optional tag for the backup |
120
+ | `list [LIMIT]` | List all backups, optionally limit results | `LIMIT` - Number of backups to show |
121
+ | `restore` | Restore a specific backup to `.env` | `-t, --tag TAG` - Restore by tag<br>`-i, --sindex INDEX` - Restore by stack index |
122
+ | `undo` | Undo last write operation to `.env` | None |
123
+ | `show` | Show contents of a backup file | `-t, --tag TAG` - Show by tag<br>`-i, --sindex INDEX` - Show by stack index |
124
+ | `delete` | Delete a specific backup | `-t, --tag TAG` - Delete by tag<br>`-i, --sindex INDEX` - Delete by stack index |
125
+ | `clear` | Delete all backups | None |
126
+ | `ignore` | Add `.envsafe` to `.gitignore` | None |
127
+
128
+ ## Workflow Examples
129
+
130
+ ### Development Workflow
131
+
132
+ ```bash
133
+ # Before making changes
134
+ $ envsafe backup -t "stable-config"
135
+
136
+ # Make your changes to .env
137
+ $ vim .env
138
+
139
+ # If something goes wrong, quickly undo
140
+ $ envsafe undo
141
+
142
+ # Or restore the tagged backup
143
+ $ envsafe restore -t "stable-config"
144
+ ```
145
+
146
+ ### Deployment Workflow
147
+
148
+ ```bash
149
+ # Backup before deployment
150
+ $ envsafe backup -t "pre-deploy-$(date +%Y%m%d)"
151
+
152
+ # Deploy and update environment variables
153
+ # ... deployment process ...
154
+
155
+ # If rollback needed
156
+ $ envsafe restore -t "pre-deploy-20240108"
157
+ ```
158
+
159
+ ### Team Collaboration
160
+
161
+ ```bash
162
+ # Setup git ignore for the team
163
+ $ envsafe ignore
164
+
165
+ # Create backups with descriptive tags
166
+ $ envsafe backup -t "feature-auth-setup"
167
+ $ envsafe backup -t "database-migration-config"
168
+
169
+ # Share backup strategies in documentation
170
+ $ envsafe list
171
+ ```
172
+
173
+ ## File Structure
174
+
175
+ Envsafe stores backups in a `.envsafe` directory in your project root:
176
+
177
+ ```
178
+ your-project/
179
+ ├── .env
180
+ ├── .envsafe/
181
+ │ ├── backup-001.env
182
+ │ ├── backup-002.env
183
+ │ └── metadata.json
184
+ └── .gitignore
185
+ ```
186
+
187
+ ## Best Practices
188
+
189
+ 1. **Tag Important Backups**: Use descriptive tags for backups before major changes
190
+ 2. **Regular Cleanup**: Periodically review and clean old backups with `envsafe clear`
191
+ 3. **Git Ignore**: Always run `envsafe ignore` to prevent committing backup files
192
+ 4. **Pre-deployment**: Create tagged backups before deployments for easy rollback
193
+ 5. **Team Coordination**: Establish tagging conventions for team projects
194
+
195
+ ## Contributing
196
+
197
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/envsafe.
198
+
199
+ ## License
200
+
201
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
data/exe/envsafe ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "envsafe"
@@ -0,0 +1,171 @@
1
+ require_relative "constants"
2
+ require_relative "file_store"
3
+
4
+ """
5
+
6
+ stack_entry = {
7
+ :file
8
+ :tag
9
+ :timestamp
10
+ }
11
+
12
+ """
13
+
14
+ class Envsafe::BackStack
15
+ class << self
16
+ def stack
17
+ stack_instance
18
+ end
19
+
20
+ # Restores last backuped up file
21
+ def pop()
22
+ if stack_instance.nil? || stack_instance.size.zero?
23
+ puts "There are no backups to restore."
24
+
25
+ return
26
+ end
27
+
28
+ entry = stack_instance.shift
29
+
30
+ if Envsafe::FileStore.restore(entry)
31
+ write()
32
+ puts "✅ Successfully retored the last backup"
33
+
34
+ else
35
+ stack_instance.unshift(entry)
36
+
37
+ puts "❌ Something went wrong will retoring the last backup."
38
+ end
39
+ end
40
+
41
+ def push(tag)
42
+ unless File.exist?(Envsafe::ENV_FILE)
43
+ puts "❌ No .env file found in current directory."
44
+ return
45
+ end
46
+
47
+
48
+ filename, timestamp = Envsafe::FileStore.new_filename(tag)
49
+
50
+ Envsafe::FileStore.copy_env(filename)
51
+
52
+ push_entry(filename, tag, timestamp)
53
+
54
+ puts "✅ Backed up .env as #{filename}"
55
+ end
56
+
57
+ def restore_by_tag(tag)
58
+ entry = stack_instance.find { |e| e["tag"] == tag }
59
+
60
+ return nil if entry.nil?
61
+
62
+ Envsafe::FileStore.restore(entry)
63
+
64
+ true
65
+ end
66
+
67
+ def restore_by_sindex(sindex)
68
+ entry = stack_instance[sindex]
69
+
70
+ Envsafe::FileStore.restore(entry)
71
+
72
+ true
73
+ end
74
+
75
+ def restore_top
76
+ return nil if stack_instance.empty?
77
+
78
+ entry = stack_instance[0]
79
+
80
+ Envsafe::FileStore.restore(entry)
81
+
82
+ true
83
+ end
84
+
85
+ def show_top()
86
+ return if stack_instance.empty?
87
+
88
+ stack_entry = stack_instance[0]
89
+
90
+ file_content = Envsafe::FileStore.file_content(stack_entry)
91
+
92
+ Envsafe::Utils.print_with_less(file_content)
93
+ end
94
+
95
+ def show_by_tag(tag)
96
+ stack_entry = stack_instance.find { |se| se["tag"] == tag }
97
+
98
+ file_content = Envsafe::FileStore.file_content(stack_entry)
99
+
100
+ Envsafe::Utils.print_with_less(file_content)
101
+ end
102
+
103
+ def show_by_sindex(sindex)
104
+ stack_entry = stack_instance[sindex]
105
+
106
+ file_content = Envsafe::FileStore.file_content(stack_entry)
107
+
108
+ Envsafe::Utils.print_with_less(file_content)
109
+ end
110
+
111
+ def remove_by_sindex(sindex)
112
+ stack_entry = stack_instance[sindex]
113
+
114
+ if Envsafe::FileStore.delete_backup(stack_entry)
115
+ stack_instance = stack_instance.reject { |se| se == stack_entry }
116
+
117
+ write()
118
+
119
+ true
120
+ else
121
+ false
122
+ end
123
+ end
124
+
125
+ def remove_by_tag(tag)
126
+ stack_entry = stack_instance.find { |se| se["tag"] == tag }
127
+
128
+
129
+ if Envsafe::FileStore.delete_backup(stack_entry)
130
+ stack_instance = stack_instance.reject { |se| se == stack_entry }
131
+
132
+ write()
133
+
134
+ true
135
+ else
136
+ false
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def push_entry(filename, tag, timestamp)
143
+ entry = {
144
+ "file" => filename,
145
+ "tag" => tag,
146
+ "timestamp" => timestamp
147
+ }
148
+
149
+ stack_instance.unshift(entry)
150
+
151
+ write()
152
+ end
153
+
154
+ def stack_instance
155
+ @stack_instance ||= read() || []
156
+ end
157
+
158
+ def read
159
+ return unless File.exist?(Envsafe::STACK_FILE)
160
+
161
+ stk_content = File.read(Envsafe::STACK_FILE)
162
+
163
+ JSON.parse(stk_content)
164
+ end
165
+
166
+ # Writes stack_instace to stack.json
167
+ def write
168
+ File.write(Envsafe::STACK_FILE, JSON.pretty_generate(stack_instance))
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,72 @@
1
+ require "thor"
2
+ require_relative "commands/backup"
3
+ require_relative "commands/ignore"
4
+ require_relative "commands/list"
5
+ require_relative "commands/clear"
6
+ require_relative "commands/restore"
7
+ require_relative "commands/undo"
8
+ require_relative "commands/show"
9
+ require_relative "commands/delete"
10
+
11
+ module Envsafe
12
+ class CLI < Thor
13
+ desc("backup", "Backup current .env")
14
+ option(:tag, aliases: "-t", type: :string, desc: "Optional tag for the backup")
15
+ def backup
16
+ Envsafe::Commands::Backup.run(tag: options[:tag])
17
+ end
18
+
19
+ desc("ignore", "Add .envsafe to git ignore")
20
+ def ignore
21
+ Envsafe::Commands::Ignore.run()
22
+ end
23
+
24
+ desc("list [LIMIT]", "List all backups, optionally limit the number shown")
25
+ def list(limit = nil)
26
+ Envsafe::Commands::List.run(limit)
27
+ end
28
+
29
+ desc("clear", "Delete all backups")
30
+ def clear
31
+ Envsafe::Commands::Clear.run()
32
+ end
33
+
34
+ desc("restore", "Restore a specific backup to .env")
35
+ option(:tag, aliases: "-t", type: :string, desc: "Restores tagged backup")
36
+ option(:sindex, aliases: "-i", type: :string, desc: "Restores backup by stack index. Use 'list' command to know the index.")
37
+ def restore()
38
+ Envsafe::Commands::Restore.run(tag: options[:tag], sindex: options[:sindex])
39
+ end
40
+
41
+ desc("undo", "Undo last write operation to .env")
42
+ def undo()
43
+ Envsafe::Commands::Undo.run()
44
+ end
45
+
46
+ desc("show", "Show contents of file using either version or stack index")
47
+ option(:tag, aliases: "-t", type: :string, desc: "Show by tag")
48
+ option(:sindex, aliases: "-i", type: :string, desc: "Show by stack index")
49
+ def show()
50
+ Envsafe::Commands::Show.run(tag: options[:tag], sindex: options[:sindex])
51
+ end
52
+
53
+ desc("delete", "Delete a specifiic backup by tag or sindex")
54
+ option(:tag, aliases: "-t", type: :string, desc: "Delete by tag")
55
+ option(:sindex, aliases: "-i", type: :string, desc: "Delete by stack index")
56
+ def delete()
57
+ Envsafe::Commands::Delete.run(tag: options[:tag], sindex: options[:sindex])
58
+ end
59
+ end
60
+ end
61
+
62
+ #✅ # backup [--tag TAG] "backup current .env with optional tag"
63
+ #✅# list n "list all save .env version"
64
+ #✅# restore version/tag "restore a specific backup to .env"
65
+ # current "show which backup is currenly checked out if any"
66
+ # delete version/tag "delete a specific backup version"
67
+ #✅# clear "clear all backups"
68
+ # check "compare .env with .example.env"
69
+ # sync "sync .example with .env creates if doesn't exist"
70
+ # diff version/tag "show diff with the currrent and provided version or tag"
71
+ #✅# undo "retores last edit" if i say "envsafe restore" it will overwrite but keep the option for user to retore the last modified version.
72
+ #✅ show
@@ -0,0 +1,54 @@
1
+ require "fileutils"
2
+ require "json"
3
+
4
+ require_relative "../utils"
5
+ require_relative "../constants"
6
+ require_relative "../back_stack"
7
+
8
+ module Envsafe::Commands; end
9
+
10
+ class Envsafe::Commands::Backup
11
+ class << self
12
+ def run(tag: nil)
13
+ tag = nil if tag == "tag" # Set it to nil and ignore "tag" string as an invalid tag.
14
+
15
+ if tag && Envsafe::Utils.invalid_tag?(tag)
16
+ puts "❌ Invalid tag...."
17
+ puts "Use alphanumeric or pure numbers"
18
+ puts "Special symbols are invalid except: hyphen and underscore"
19
+
20
+ return
21
+ end
22
+
23
+ if tag && Envsafe::Utils.tag_present?(tag, Envsafe::BackStack.stack)
24
+ puts "❌ Tag already exists in the stack. Please use a different tag."
25
+
26
+ return
27
+ end
28
+
29
+
30
+ Envsafe::BackStack.push(tag)
31
+ end
32
+
33
+ private
34
+
35
+ def update_stack(filename, timestamp, tag)
36
+ stack = []
37
+
38
+ if File.exist?(Envsafe::STACK_FILE)
39
+ content = File.read(Envsafe::STACK_FILE)
40
+ stack = JSON.parse(content)
41
+ end
42
+
43
+ entry = {
44
+ "file" => filename,
45
+ "tag" => tag,
46
+ "timestamp" => timestamp
47
+ }
48
+
49
+ stack.unshift(entry)
50
+
51
+ File.write(Envsafe::STACK_FILE, JSON.pretty_generate(stack))
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,38 @@
1
+ module Envsafe::Commands; end
2
+
3
+ class Envsafe::Commands::Clear
4
+ class << self
5
+ def run
6
+ return unless user_sure?
7
+
8
+ # TODO: check if .envsafe exists only then continue
9
+ if system("rm", "-rf", ".envsafe")
10
+ puts "✅ .envsafe removed successfully!"
11
+ else
12
+ puts "❌ Something went wrong...."
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def user_sure?
19
+
20
+ while true
21
+ STDOUT.write "This action will delete .envsafe folder along will all versions of .env, are you sure? (yes/no): "
22
+ STDOUT.flush
23
+
24
+ response = STDIN.gets.chomp.downcase
25
+
26
+ next if response.nil?
27
+
28
+ if response == "yes"
29
+ return true
30
+ elsif response == "no"
31
+ return false
32
+ else
33
+ puts "Please type yes/no"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,40 @@
1
+ require_relative "../back_stack"
2
+ require_relative "../utils"
3
+
4
+ module Envsafe::Commands; end
5
+
6
+ class Envsafe::Commands::Delete
7
+ class << self
8
+ def run(tag:, sindex:)
9
+ if tag.nil? && sindex.nil?
10
+ puts "❌ Please provide either a tag or an index."
11
+ return
12
+ end
13
+
14
+
15
+ back_stack = Envsafe::BackStack.stack()
16
+ parsed_sindex = Envsafe::Utils.parse_int(sindex)
17
+
18
+ if back_stack.empty?
19
+ puts "❌ No backup found. Please create one with 'backup' command."
20
+ return
21
+ end
22
+
23
+ if tag && not Envsafe::Utils.tag_present?(tag, back_stack)
24
+ puts "❌ Tag not found. Please check with 'list' command."
25
+ return
26
+ end
27
+
28
+ if sindex && (parsed_sindex < 0 || parsed_sindex >= back_stack.size)
29
+ puts "❌ Invalid index. Please check with 'list' command."
30
+ return
31
+ end
32
+
33
+
34
+ if (Envsafe::BackStack.remove_by_sindex(parsed_sindex) if sindex) ||
35
+ (Envsafe::BackStack.remove_by_tag(tag) if tag )
36
+ puts "✅ Backup deleted successfully."
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ require_relative '../constants'
2
+
3
+ module Envsafe::Commands; end
4
+
5
+ class Envsafe::Commands::Ignore
6
+ class << self
7
+ def run
8
+ unless File.exist?(Envsafe::IGNORE_FILE) && File.readable?(Envsafe::IGNORE_FILE) && File.writable?(Envsafe::IGNORE_FILE)
9
+ puts "❌ Couldn't load .gitignore or either the file can't be read or written."
10
+ return
11
+ end
12
+
13
+ File.foreach(Envsafe::IGNORE_FILE) do |line|
14
+ if line.strip() == ".envsafe"
15
+ puts "✅ .envsafe is already ignored."
16
+
17
+ return
18
+ end
19
+ end
20
+
21
+ File.open(Envsafe::IGNORE_FILE, "a") do |file|
22
+ file.puts(".envsafe")
23
+ puts "✅ .envsafe has been ignored."
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ require "terminal-table"
2
+
3
+ require_relative "../constants"
4
+ require_relative "../back_stack"
5
+ require_relative "../utils"
6
+
7
+ module Envsafe::Commands; end
8
+
9
+ class Envsafe::Commands::List
10
+ class << self
11
+ def run(limit = 10)
12
+
13
+ limit = limit.nil? ? limit : limit.to_i
14
+
15
+ if !limit.nil? && (limit.zero? || limit < 0)
16
+ puts "❌ Limit must be an Integer and greater than zero."
17
+ puts()
18
+ puts "Example: envsafe list 5"
19
+ puts "Default limit is 10"
20
+ return
21
+ end
22
+
23
+ unless File.exist?(Envsafe::STACK_FILE)
24
+ puts "❌ There is nothing to show or the main .envstack/stack.json is missing."
25
+
26
+ return
27
+ end
28
+
29
+ file_content = read_lines(limit)
30
+ Envsafe::Utils.print_with_less(file_content)
31
+ end
32
+
33
+ private
34
+
35
+ def read_lines(n)
36
+ stack = Envsafe::BackStack.stack()
37
+
38
+ n = n.nil? ? stack.length : n
39
+ n = stack.size if n > stack.size
40
+
41
+ stack = stack[0, n]
42
+
43
+ rows = stack.each_with_index.map { |item, index| [index, File.join(Envsafe::BACKUP_DIR, item["file"]), item["tag"], item["timestamp"], Time.at(item["timestamp"])] }
44
+
45
+ Terminal::Table.new(headings: ["Index", "File", "Tag", "Timestamp", "Backed up at"], rows: rows)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ require_relative "../back_stack"
2
+ require_relative "../utils"
3
+
4
+ module Envsafe::Commands; end
5
+
6
+ class Envsafe::Commands::Restore
7
+ class << self
8
+ def run(tag:, sindex:)
9
+ back_stack = Envsafe::BackStack.stack()
10
+ parsed_sindex = Envsafe::Utils.parse_int(sindex)
11
+
12
+ if sindex && (parsed_sindex.nil? || parsed_sindex < 0 || parsed_sindex >= back_stack.size)
13
+ puts "❌ Invalid sindex provided or out of range."
14
+
15
+ return
16
+ end
17
+
18
+ if tag && not Envsafe::Utils.tag_present?(tag, back_stack)
19
+ puts "❌ Tag not found in the backup stack. Check with 'envsafe list'"
20
+
21
+ return
22
+ end
23
+
24
+ if tag.nil? && sindex.nil?
25
+ puts "ℹ️ Tag or sindex is not provided restoring the last backed version."
26
+ end
27
+
28
+ has_restored = if tag
29
+ Envsafe::BackStack.restore_by_tag(tag)
30
+ elsif sindex
31
+ Envsafe::BackStack.restore_by_sindex(parsed_sindex)
32
+ else
33
+ Envsafe::BackStack.restore_top
34
+ end
35
+
36
+ if has_restored
37
+ puts "✅ Restored successfully."
38
+ else
39
+ puts "❌ Failed to restore."
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ require_relative "../constants"
2
+ require_relative "../back_stack"
3
+ require_relative "../utils"
4
+
5
+ module Envsafe::Commands; end
6
+
7
+ class Envsafe::Commands::Show
8
+ class << self
9
+ def run(tag:, sindex:)
10
+ back_stack = Envsafe::BackStack.stack()
11
+ parsed_sindex = Envsafe::Utils.parse_int(sindex)
12
+
13
+ if back_stack.empty?
14
+ puts "❌ No backup found. Please create one with 'backup' command."
15
+ return
16
+ end
17
+
18
+ if tag && not Envsafe::Utils.tag_present?(tag, back_stack)
19
+ puts "❌ Tag not found. Please check with 'list' command."
20
+ return
21
+ end
22
+
23
+ if sindex && (parsed_sindex < 0 || parsed_sindex >= back_stack.size)
24
+ puts "❌ Invalid index. Please check with 'list' command."
25
+ return
26
+ end
27
+
28
+ if sindex
29
+ Envsafe::BackStack.show_by_sindex(parsed_sindex)
30
+ elsif tag
31
+ Envsafe::BackStack.show_by_tag(tag)
32
+ else
33
+ Envsafe::BackStack.show_top()
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "../constants"
2
+
3
+ module Envsafe::Commands; end
4
+
5
+ class Envsafe::Commands::Undo
6
+ class << self
7
+ def run()
8
+ puts "Undoing the last write operation...."
9
+
10
+ if File.exist?(Envsafe::MAIN_ENV)
11
+ FileUtils.cp(Envsafe::MAIN_ENV, Envsafe::ENV_FILE)
12
+
13
+ puts "✅ Undo was successful"
14
+ else
15
+ puts "❌ Undo failed, have you backed up your .env?"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ module Envsafe
2
+ ENVSAFE_DIR = ".envsafe"
3
+ BACKUP_DIR = File.join(ENVSAFE_DIR, "backups")
4
+ MAIN_ENV = File.join(ENVSAFE_DIR, "main.env")
5
+ STACK_FILE = File.join(ENVSAFE_DIR, "stack.json")
6
+ ENV_FILE = ".env"
7
+ ENV_EXAMPLE = ".example.env"
8
+ IGNORE_FILE = ".gitignore"
9
+ end
@@ -0,0 +1,81 @@
1
+ require "fileutils"
2
+
3
+ class Envsafe::FileStore
4
+ class << self
5
+ def copy_env(filename)
6
+ FileUtils.mkdir_p(Envsafe::BACKUP_DIR)
7
+
8
+ unless File.exist?(Envsafe::MAIN_ENV)
9
+ backup_current_to_main_env()
10
+ end
11
+
12
+ dest_path = File.join(Envsafe::BACKUP_DIR, filename)
13
+
14
+ FileUtils.cp(Envsafe::ENV_FILE, dest_path)
15
+ end
16
+
17
+ def delete_backup(stack_entry)
18
+ File.delete(full_filepath(stack_entry))
19
+
20
+ File.exist?(full_filepath(stack_entry))
21
+ end
22
+
23
+
24
+ def new_filename(tag)
25
+ timestamp = Time.now.utc.to_i
26
+
27
+ [(tag ? "#{timestamp}_#{tag}.env" : "#{timestamp}.env"), timestamp]
28
+ end
29
+
30
+ def restore(stack_entry)
31
+ filepath = full_filepath(stack_entry)
32
+
33
+ backup_current_to_main_env()
34
+
35
+ FileUtils.cp(filepath, Envsafe::ENV_FILE)
36
+ end
37
+
38
+ # Deletes the file after copying it to main.
39
+ def restore_main(stack_entry)
40
+ backup_current_to_main_env()
41
+
42
+ FileUtils.cp(full_filepath(stack_entry), Envsafe::ENV_FILE)
43
+
44
+ delete_backup(stack_entry)
45
+ end
46
+
47
+ def backup_current_to_main_env
48
+ FileUtils.cp(Envsafe::ENV_FILE, Envsafe::MAIN_ENV)
49
+ end
50
+
51
+ # Overwrites .env with main.env
52
+ def undo_last_restore
53
+ FileUtils.cp(Envsafe::MAIN_ENV, Envsafe::ENV_FILE)
54
+ end
55
+
56
+
57
+ def filename(stack_entry)
58
+ return if stack_entry.nil?
59
+
60
+ file, tag, timestamp = dse(stack_entry)
61
+
62
+ tag ? "#{timestamp}_#{tag}.env" : "#{timestamp}.env"
63
+ end
64
+
65
+ def full_filepath(stack_entry)
66
+ File.join(Envsafe::BACKUP_DIR, filename(stack_entry))
67
+ end
68
+
69
+ def file_content(stack_entry)
70
+ File.read(full_filepath(stack_entry))
71
+ end
72
+
73
+ private
74
+
75
+ # Destructured Stack Entry (dse)
76
+ def dse(se)
77
+ [se["file"], se["tag"], se["timestamp"]]
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ module Envsafe
2
+ module Utils
3
+ def self.valid_tag?(tag)
4
+ /\A[a-zA-Z0-9_-]+\z/.match?(tag)
5
+ end
6
+
7
+ def self.invalid_tag?(tag)
8
+ not valid_tag?(tag)
9
+ end
10
+
11
+ def self.tag_present?(tag, back_stack)
12
+ back_stack.any? { |entry| entry["tag"] == tag }
13
+ end
14
+
15
+ def self.unique_tag?(tag, back_stack)
16
+ not(tag_present?(tag, back_stack))
17
+ end
18
+
19
+ def self.parse_int(num)
20
+ return nil if num.nil?
21
+
22
+ Integer(num)
23
+ rescue Error
24
+ nil
25
+ end
26
+
27
+ def self.print_with_less(content)
28
+ IO.popen("less", "w") { |io| io.write(content)}
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Envsafe
4
+ VERSION = "0.1.0"
5
+ end
data/lib/envsafe.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "envsafe/version"
4
+ require_relative "envsafe/cli"
5
+
6
+ module Envsafe; end
7
+ module Envsafe::Commands; end
8
+
9
+ module Envsafe
10
+ class Error < StandardError; end
11
+ # Your code goes here...
12
+ end
data/sig/envsafe.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Envsafe
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: envsafe
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaiser Sakhi
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ Envsafe is a standalone CLI utility for managing your .env files without project integration.
14
+
15
+ Quickly back up your current environment, restore from any saved version, and compare your .env file
16
+ against .env.example to catch missing or extra variables. Think of it as git stash for your .env.
17
+
18
+ Core features:
19
+ - Backup and restore .env files with optional tags
20
+ - Pop the latest backup off the stack
21
+ - Checkout any saved .env version or return to main
22
+ - Validate .env vs .env.example
23
+ - CLI-native — no Gemfile or code integration required
24
+
25
+ Envsafe gives you safe, versioned control of your app’s environment variables — without the overhead.
26
+ email:
27
+ - mail@kaisersakhi.com
28
+ executables:
29
+ - envsafe
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - CODE_OF_CONDUCT.md
34
+ - LICENSE.txt
35
+ - README.md
36
+ - Rakefile
37
+ - exe/envsafe
38
+ - lib/envsafe.rb
39
+ - lib/envsafe/back_stack.rb
40
+ - lib/envsafe/cli.rb
41
+ - lib/envsafe/commands/backup.rb
42
+ - lib/envsafe/commands/clear.rb
43
+ - lib/envsafe/commands/delete.rb
44
+ - lib/envsafe/commands/ignore.rb
45
+ - lib/envsafe/commands/list.rb
46
+ - lib/envsafe/commands/restore.rb
47
+ - lib/envsafe/commands/show.rb
48
+ - lib/envsafe/commands/undo.rb
49
+ - lib/envsafe/constants.rb
50
+ - lib/envsafe/file_store.rb
51
+ - lib/envsafe/utils.rb
52
+ - lib/envsafe/version.rb
53
+ - sig/envsafe.rbs
54
+ homepage: https://github.com/kaisersakhi/envsafe
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://github.com/kaisersakhi/envsafe
59
+ source_code_uri: https://github.com/kaisersakhi/envsafe
60
+ rdoc_options: []
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.0.0
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubygems_version: 3.7.0
75
+ specification_version: 4
76
+ summary: A simple CLI tool to back up, restore, and validate your .env files.
77
+ test_files: []