Compare commits

...

9 Commits

4 changed files with 283 additions and 22 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Spencer Reid Brower
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

127
README.md
View File

@@ -1,4 +1,127 @@
# envr - Backup your env files
Have you ever wanted to back up all your .env files in case your hard drive gets
nuked? `envr` makes it easier.
`envr` is a [Nushell](https://www.nushell.sh) script that tracks your `.env` files
in an encyrpted sqlite database. Changes can be effortlessly synced with
`envr sync`, and restored with `envr restore`.
`envr` puts all your .env files in one safe place, so you can back them up with
the tool [of your choosing](#backup-options).
## Features
- 🔐 **Encrypted Storage**: All `.env` files are encrypted using your ssh key and
[age](https://github.com/FiloSottile/age) encryption.
- 🔄 **Automatic Sync**: Update the database with one command, which can easily
be run on a cron.
- 🔍 **Smart Scanning**: Automatically discover and import `.env` files in your
home directory.
- 📝 **Multiple Config Formats**: Support for many configuration formats,
including: JSON, TOML, YAML, INI, XML, and NUON.
- [ ] TODO: 🗂️ **Rename Detection**: Automatically handle renamed repositories.
-**Interactive CLI**: User-friendly prompts for file selection and management
thanks to [nushell](https://www.nushell.sh/)
## TODOS
- [ ] `envr sync` - Restore missing .env files, and update backed up ones.
- [ ] `envr scan` - Search for missing / moved .env files.
- [ ] Allow configuration of ssh key.
- [ ] Allow multiple ssh keys.
## Prerequisites
- An SSH key pair (for encryption/decryption)
- The following binaries:
- [nushell](https://www.nushell.sh/)
- [age](https://github.com/FiloSottile/age)
- [fd](https://github.com/sharkdp/fd)
- [sqlite3](https://github.com/sqlite/sqlite)
## Installation
1. Clone the repository:
```bash
git clone https://github.com/username/envr.git
cd envr
```
2. Install [dependencies](#prerequisites).
3. Configure nushell.
## Quick Start
1. **Initialize envr**:
```bash
nu mod.nu envr init
```
This will create your configuration file and set up encrypted storage.
2. **Scan for existing .env files**:
```bash
nu mod.nu envr scan
```
Select files you want to back up from the interactive list.
3. **List tracked files**:
```bash
nu mod.nu envr list
```
4. **Sync your environment files**:
```bash
nu mod.nu envr sync
```
## Disclaimers
> [!CAUTION]
> Do not lose your SSH key pair! Your backup will be **lost forever**.
## Commands
| Command | Description |
|---------|-------------|
| `envr init [format]` | Initialize envr with configuration file |
| `envr backup <file>` | Back up a specific .env file |
| `envr restore [path]` | Restore a backed-up .env file |
| `envr list` | View all tracked environment files |
| `envr scan` | Search for and selectively back up .env files |
| `envr sync` | Synchronize all tracked files (backup changes, restore missing) |
| `envr remove [...paths]` | Remove files from backup storage |
| `envr edit config` | Edit your configuration file |
| `envr config show` | Display current configuration |
## Configuration
The configuration file is created during initialization and supports multiple formats:
```toml
# Example ~/.envr/config.toml
source = "~/.envr/config.toml"
priv_key = "~/.ssh/id_ed25519"
pub_key = "~/.ssh/id_ed25519.pub"
[scan]
matcher = "\.env"
exclude = "*.envrc"
include = "~"
```
## Backup Options
`envr` merely gathers your `.env` files in one local place. It is up to you to
back up the database (found at ~/.envr/data.age) to a *secure* and *remote*
location.
### Git
### restic
## License
This project is licensed under the [MIT License](./LICENSE).
## Support
For issues, feature requests, or questions, please
[open an issue](https://github.com/sbrow/envr/issues).

View File

@@ -70,6 +70,7 @@
{
buildInputs = with pkgs; [
age
fd
nushell
sqlite

156
mod.nu
View File

@@ -1,8 +1,11 @@
#!/usr/bin/env nu
#
# TODO: Wrap blocks that use tmp files in try so we can clean them up.
use std assert;
# Manage your .env files with ease
@example "Set up envr" { envr init }
export def envr [] {
help envr
}
@@ -84,7 +87,7 @@ def "close db" [] {
rm $dec
}
# Restore a .env file from backup.
# Restore a .env file from backup
export def "envr restore" [
path?: path # The path of the file to restore. Will be prompted if left blank.
]: nothing -> string {
@@ -143,33 +146,96 @@ const available_formats = [
]
# Create your initial config
export def "envr config init" [
export def "envr init" [
format?: string
#identity?: path
] {
]: nothing -> record {
mkdir ~/.envr
let format = if ($format | is-empty) {
$available_formats | input list 'Please select the desired format for your config file'
}
if (glob ~/.envr/config.* | length | $in > 0) {
error make {
msg: "A config file already exists"
label: {
text: ""
span: (metadata $format).span
}
}
} else {
let format = if ($format | is-empty) {
$available_formats | input list 'Please select the desired format for your config file'
}
let identity = '~/.ssh/id_ed25519';
# TODO: Let user select identity.
let identity = '~/.ssh/id_ed25519';
# The path to the config file.
let source = $'~/.envr/config.($format)'
# The path to the config file.
let source = $'~/.envr/config.($format)'
{
source: $source
priv_key: $identity
pub_key: $'($identity).pub'
} | tee {
save $source
# TODO: Let user select file types to scan for.
{
source: $source
priv_key: $identity
pub_key: $'($identity).pub'
scan: {
matcher: '\.env'
exclude: '*.envrc'
include: '~'
}
} | tee {
save $source;
open db
}
}
}
# Remove a .env file from your backups.
export def "envr remove" [
...paths: path
] {
let $paths = if ($paths | is-empty) {
(envr list | get path) | input list -m "Select path(s) to remove"
} else {
$paths
};
print -e $paths;
let confirmed = (
['No' 'Yes']
| input list 'Are you sure you want to delete these backups?'
| $in == 'Yes'
);
if ($confirmed) {
let $q = (
$paths
| each { path expand }
| to json -r
| str replace '[' '('
| str replace ']' ')'
| str replace -a '"' "'");
(
open db
| stor delete -t envr_env_files -w $'path in ($q)'
);
close db
$'(ansi green)Paths removed!'
} else {
$'(ansi yellow)Operation aborted.(ansi reset)'
}
}
# View your tracked files
export def "envr list" [] {
(files | reject contents)
(
files
| reject contents
| update path { $in | path relative-to ~/ | $'~/($in)' }
| update dir { $in | path relative-to ~/ | $'~/($in)' }
)
}
# List all the files in the database
@@ -181,13 +247,63 @@ def files [] {
)
}
# Update your env backups
# Update or restore your env backups
export def "envr sync" [] {
'TODO:'
let $files = (files);
$files | each { |it|
if ($it.path | path type | $in == 'file') {
if (open $it.path | hash sha256 | $in != $it.sha256) {
envr backup $it.path
} else {
'Nothing to do!'
}
} else {
envr restore $it.path
}
}
}
# Search for .env files and select ones to back up.
export def "envr scan" [] {
let config = (envr config show).scan;
let ignored_env_files = (
find env-files $config.matcher $config.exclude $config.include
);
let tracked_file_paths = (files).path;
let scanned = $ignored_env_files | where { |it|
not ($it in $tracked_file_paths)
}
(
$scanned
| input list -m "Select files to back up"
| each { envr backup $in }
)
}
# Search for hidden env files
def "find env-files" [
match: string = '\.env'
exclude: string = '*.envrc'
search: string = '~/'
]: nothing -> list<path> {
let search = ($search | path expand);
let unignored = (fd -a $match -E $exclude -H $search | lines)
let all = (fd -a $match -E $exclude -HI $search | lines)
let ignored = $all | where { |it|
not ($it in $unignored)
}
$ignored | sort
}
# Edit your config
export def "envr config edit" [] {
export def "envr edit config" [] {
^$env.EDITOR (config-file)
}
@@ -196,6 +312,6 @@ def "config-file" []: [nothing -> path nothing -> nothing] {
}
# show your current config
export def "envr config show" []: nothing -> record<source: path, priv_key: path, pub_key: path> {
def "envr config show" []: nothing -> record<source: path, priv_key: path, pub_key: path> {
open (config-file)
}