Compare commits

16 Commits

Author SHA1 Message Date
Spencer Brower
7cbc04cbf6 chore(main): release 0.2.0 2025-11-06 17:35:27 -05:00
cbd74f387e feat: Added new check command. 2025-11-06 17:35:11 -05:00
c9c34ce771 refactor(check)!: Renamed the check command to deps. 2025-11-06 17:10:53 -05:00
17ce49cd2d fix(check): fd now correctly gets marked as found. 2025-11-06 17:06:26 -05:00
af0a9f9c4c docs: Added todos. 2025-11-05 18:31:39 -05:00
4273fa5895 feat!: Multiple scan includes are now supported.
BREAKING CHANGE: The config value `scan.Include` is now a list rather than
a string.

Release-As: 0.2.0
2025-11-05 18:29:19 -05:00
bb3c0cdeee chore: Updated nix version. 2025-11-05 18:09:58 -05:00
Spencer Brower
df5fdeee67 chore(main): release 0.1.1 2025-11-05 18:08:40 -05:00
42796ec77b feat(sync): Results are now displayed in a table.
Release-As: 0.1.1
2025-11-05 17:49:22 -05:00
8a349dd760 build: Added Makefile for cross-platform builds. 2025-11-05 17:28:59 -05:00
7a49858a58 docs: Updated TODOs. 2025-11-05 16:35:07 -05:00
9ab72a25fa fix(sync): Fixed an issue where deleted folders would be restored. 2025-11-05 15:43:32 -05:00
35519550ed build: Added release-please. 2025-11-05 15:34:06 -05:00
Spencer Brower
7a60c673be build: Updated go version for github action. 2025-11-05 15:34:03 -05:00
Spencer Brower
f8dc85a8b7 build: Created github actions build. 2025-11-04 14:06:13 -05:00
5310803c8b docs: Updated README.md 2025-11-03 18:29:23 -05:00
19 changed files with 472 additions and 115 deletions

28
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24.6'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...

25
.github/workflows/release-please.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
on:
push:
branches:
- main
permissions:
contents: write
issues: write
pull-requests: write
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
# this assumes that you have created a personal access token
# (PAT) and configured it as a GitHub action secret named
# `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
# this is a built-in strategy in release-please, see "Action Inputs"
# for more options
release-type: go

1
.gitignore vendored
View File

@@ -5,5 +5,6 @@
man
# build artifacts
builds
envr
result

36
CHANGELOG.md Normal file
View File

@@ -0,0 +1,36 @@
# Changelog
## [0.2.0](https://github.com/sbrow/envr/compare/v0.1.1...v0.2.0) (2025-11-06)
### ⚠ BREAKING CHANGES
* **check:** Renamed the `check` command to `deps`.
* The config value `scan.Include` is now a list rather than a string.
### Features
* Added new `check` command. ([cbd74f3](https://github.com/sbrow/envr/commit/cbd74f387e2e330b2557d07dd82ba05cc91300ac))
* Multiple scan includes are now supported. ([4273fa5](https://github.com/sbrow/envr/commit/4273fa58956d8736271a0af66202dca481126fe4))
### Bug Fixes
* **check:** `fd` now correctly gets marked as found. ([17ce49c](https://github.com/sbrow/envr/commit/17ce49cd2d33942282c6f54ce819ac25978f6b7c))
### Code Refactoring
* **check:** Renamed the `check` command to `deps`. ([c9c34ce](https://github.com/sbrow/envr/commit/c9c34ce771653da214635f1df1fef1f23265c552))
## [0.1.1](https://github.com/sbrow/envr/compare/v0.1.0...v0.1.1) (2025-11-05)
### Features
* **sync:** Results are now displayed in a table. ([42796ec](https://github.com/sbrow/envr/commit/42796ec77b1817e1b9f09068d76a7b6e30da246b))
### Bug Fixes
* **sync:** Fixed an issue where deleted folders would be restored. ([9ab72a2](https://github.com/sbrow/envr/commit/9ab72a25faf1af0eedb2f4574166c6ee47450ebb))

92
Makefile Normal file
View File

@@ -0,0 +1,92 @@
# Makefile for envr - Environment file manager
# Builds release artifacts for GitHub releases
APP_NAME := envr
VERSION := $(shell grep 'version = ' flake.nix | head -1 | sed 's/.*version = "\(.*\)";/\1/')
BUILD_DIR := builds
LDFLAGS := -X github.com/sbrow/envr/cmd.version=v$(VERSION) -s -w
# Binary names
LINUX_AMD64_BIN := $(BUILD_DIR)/$(APP_NAME)-$(VERSION)-linux-amd64
LINUX_ARM64_BIN := $(BUILD_DIR)/$(APP_NAME)-$(VERSION)-linux-arm64
DARWIN_ARM64_BIN := $(BUILD_DIR)/$(APP_NAME)-$(VERSION)-darwin-arm64
.PHONY: all clean cleanall build-linux build-darwin compress release help
# Default target
all: release clean
# Create build directory
$(BUILD_DIR):
@mkdir -p $(BUILD_DIR)
# Build Linux AMD64
$(LINUX_AMD64_BIN): $(BUILD_DIR)
@echo "Building for Linux AMD64..."
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o $(LINUX_AMD64_BIN) .
@echo "Built $(LINUX_AMD64_BIN)"
# Build Linux ARM64
$(LINUX_ARM64_BIN): $(BUILD_DIR)
@echo "Building for Linux ARM64..."
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o $(LINUX_ARM64_BIN) .
@echo "Built $(LINUX_ARM64_BIN)"
# Build Darwin ARM64 (Mac)
$(DARWIN_ARM64_BIN): $(BUILD_DIR)
@echo "Building for Darwin ARM64..."
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "$(LDFLAGS)" -o $(DARWIN_ARM64_BIN) .
@echo "Built $(DARWIN_ARM64_BIN)"
# Build all binaries
build-linux: $(LINUX_AMD64_BIN) $(LINUX_ARM64_BIN)
build-darwin: $(DARWIN_ARM64_BIN)
# Compress Linux artifacts with gzip
$(BUILD_DIR)/$(APP_NAME)-$(VERSION)-linux-amd64.tar.gz: $(LINUX_AMD64_BIN)
@echo "Compressing Linux AMD64 artifact..."
cd $(BUILD_DIR) && tar -czf $(APP_NAME)-$(VERSION)-linux-amd64.tar.gz --transform 's|.*|$(APP_NAME)|' $(shell basename $(LINUX_AMD64_BIN))
$(BUILD_DIR)/$(APP_NAME)-$(VERSION)-linux-arm64.tar.gz: $(LINUX_ARM64_BIN)
@echo "Compressing Linux ARM64 artifact..."
cd $(BUILD_DIR) && tar -czf $(APP_NAME)-$(VERSION)-linux-arm64.tar.gz --transform 's|.*|$(APP_NAME)|' $(shell basename $(LINUX_ARM64_BIN))
# Compress Darwin artifacts with zip
$(BUILD_DIR)/$(APP_NAME)-$(VERSION)-darwin-arm64.zip: $(DARWIN_ARM64_BIN)
@echo "Compressing Darwin ARM64 artifact..."
cd $(BUILD_DIR) && cp $(shell basename $(DARWIN_ARM64_BIN)) $(APP_NAME) && zip $(APP_NAME)-$(VERSION)-darwin-arm64.zip $(APP_NAME) && rm $(APP_NAME)
# Compress all artifacts
compress: $(BUILD_DIR)/$(APP_NAME)-$(VERSION)-linux-amd64.tar.gz \
$(BUILD_DIR)/$(APP_NAME)-$(VERSION)-linux-arm64.tar.gz \
$(BUILD_DIR)/$(APP_NAME)-$(VERSION)-darwin-arm64.zip
# Build and compress all release artifacts
release: build-linux build-darwin compress
@echo "Release artifacts created:"
@ls -la $(BUILD_DIR)/*.tar.gz $(BUILD_DIR)/*.zip 2>/dev/null || echo "No compressed artifacts found"
# Clean binary files only
clean:
@echo "Cleaning binary files..."
@rm -f $(LINUX_AMD64_BIN) $(LINUX_ARM64_BIN) $(DARWIN_ARM64_BIN)
# Clean everything in build directory
cleanall:
@echo "Cleaning build directory..."
@rm -rf $(BUILD_DIR)
# Show available targets
help:
@echo "Available targets:"
@echo " all - Build all release artifacts (default)"
@echo " release - Build and compress all release artifacts"
@echo " build-linux - Build Linux binaries only"
@echo " build-darwin - Build Darwin binaries only"
@echo " compress - Compress all built binaries"
@echo " clean - Remove binary files only"
@echo " cleanall - Remove entire build directory"
@echo " help - Show this help message"
@echo ""
@echo "Release artifacts will be created in $(BUILD_DIR)/"
@echo "Version: $(VERSION)"

View File

@@ -88,8 +88,8 @@ The configuration file is created during initialization:
{
"keys": [
{
"private": "/home/spencer/.ssh/id_ed25519",
"public": "/home/spencer/.ssh/id_ed25519.pub"
"private": "/home/ubuntu/.ssh/id_ed25519",
"public": "/home/ubuntu/.ssh/id_ed25519.pub"
}
],
"scan": {

View File

@@ -24,9 +24,11 @@ type SshKeyPair struct {
}
type scanConfig struct {
// TODO: Support multiple matchers
Matcher string `json:"matcher"`
// TODO: Support multiple excludes
Exclude string `json:"exclude"`
Include string `json:"include"`
Include []string `json:"include"`
}
// Create a fresh config with sensible defaults.
@@ -47,7 +49,7 @@ func NewConfig(privateKeyPaths []string) Config {
ScanConfig: scanConfig{
Matcher: "\\.env",
Exclude: "*.envrc",
Include: "~",
Include: []string{"~"},
},
}
}
@@ -109,17 +111,18 @@ func (c *Config) Save() error {
// Use fd to find all ignored .env files that match the config's parameters
func (c Config) scan() (paths []string, err error) {
searchPath, err := c.searchPath()
searchPaths, err := c.searchPaths()
if err != nil {
return []string{}, err
}
for _, searchPath := range searchPaths {
// Find all files (including ignored ones)
fmt.Printf("Searching for all files in \"%s\"...\n", searchPath)
allCmd := exec.Command("fd", "-a", c.ScanConfig.Matcher, "-E", c.ScanConfig.Exclude, "-HI", searchPath)
allOutput, err := allCmd.Output()
if err != nil {
return []string{}, err
return paths, err
}
allFiles := strings.Split(strings.TrimSpace(string(allOutput)), "\n")
@@ -154,26 +157,31 @@ func (c Config) scan() (paths []string, err error) {
}
}
return ignoredFiles, nil
paths = append(paths, ignoredFiles...)
}
func (c Config) searchPath() (path string, err error) {
include := c.ScanConfig.Include
return paths, nil
}
if include == "~" {
func (c Config) searchPaths() (paths []string, err error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
return homeDir, nil
return paths, err
}
absPath, err := filepath.Abs(include)
includes := c.ScanConfig.Include
for _, include := range includes {
path := strings.Replace(include, "~", homeDir, 1)
absPath, err := filepath.Abs(path)
if err != nil {
return "", err
return paths, err
}
return absPath, nil
paths = append(paths, absPath)
}
return paths, nil
}
// TODO: Should this be private?

View File

@@ -326,8 +326,15 @@ func (db *Db) Delete(path string) error {
}
// Finds .env files in the filesystem that aren't present in the database.
func (db *Db) Scan() ([]string, error) {
all_paths, err := db.cfg.scan()
// path overrides the already configured
func (db *Db) Scan(paths []string) ([]string, error) {
cfg := db.cfg
if paths != nil {
cfg.ScanConfig.Include = paths
}
all_paths, err := cfg.scan()
if err != nil {
return []string{}, err
}

View File

@@ -99,8 +99,8 @@ func getGitRemotes(dir string) []string {
func (file EnvFile) Restore() error {
// TODO: Handle restores more cleanly
// Ensure the directory exists
if err := os.MkdirAll(filepath.Dir(file.Path), 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
if _, err := os.Stat(file.Dir); err != nil {
return fmt.Errorf("directory missing")
}
// Check if file already exists

View File

@@ -13,7 +13,7 @@ const (
// fd
Fd AvailableFeatures = 2
// All features are present
All AvailableFeatures = Git & Fd
All AvailableFeatures = Git | Fd
)
// Checks for available features.

View File

@@ -1,48 +1,106 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"github.com/olekukonko/tablewriter"
"github.com/sbrow/envr/app"
"github.com/spf13/cobra"
)
var checkCmd = &cobra.Command{
Use: "check",
Short: "Check for missing binaries",
Long: `envr relies on external binaries for certain functionality.
The check command reports on which binaries are available and which are not.`,
Use: "check [path]",
Short: "check if files in the current directory are backed up",
// TODO: Long description for new check command
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
// Accept an optional path arg, default to current working directory
var checkPath string
if len(args) > 0 {
checkPath = args[0]
} else {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current working directory: %w", err)
}
checkPath = cwd
}
// Get absolute path
absPath, err := filepath.Abs(checkPath)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Open database
db, err := app.Open()
if err != nil {
return err
} else {
return fmt.Errorf("failed to open database: %w", err)
}
defer db.Close(app.ReadOnly)
features := db.Features()
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"Feature", "Status"})
// Check Git
if features&app.Git == 1 {
table.Append([]string{"Git", "✓ Available"})
} else {
table.Append([]string{"Git", "✗ Missing"})
// Check if the path is a file or directory
info, err := os.Stat(absPath)
if err != nil {
return fmt.Errorf("failed to stat path: %w", err)
}
// Check fd
if features&app.Fd == 1 {
table.Append([]string{"fd", "✓ Available"})
} else {
table.Append([]string{"fd", "✗ Missing"})
var filesInPath []string
if info.IsDir() {
// Find .env files in the specified directory
if err := db.CanScan(); err != nil {
return err
}
table.Render()
// Scan only the specified path for .env files
filesInPath, err = db.Scan([]string{absPath})
if err != nil {
return fmt.Errorf("failed to scan path for env files: %w", err)
}
} else {
// Path is a file, just check this specific file
filesInPath = []string{absPath}
}
// Get all backed up files from the database
envFiles, err := db.List()
if err != nil {
return fmt.Errorf("failed to list files from database: %w", err)
}
// Check which files are not backed up
var notBackedUp []string
for _, file := range filesInPath {
isBackedUp := false
for _, envFile := range envFiles {
if envFile.Path == file {
isBackedUp = true
break
}
}
if !isBackedUp {
notBackedUp = append(notBackedUp, file)
}
}
// Display results
if len(notBackedUp) == 0 {
if len(filesInPath) == 0 {
fmt.Println("No .env files found in the specified directory.")
} else {
fmt.Println("✓ All .env files in the directory are backed up.")
}
} else {
fmt.Printf("Found %d .env file(s) that are not backed up:\n", len(notBackedUp))
for _, file := range notBackedUp {
fmt.Printf(" %s\n", file)
}
fmt.Println("\nRun 'envr sync' to back up these files.")
}
return nil
}
},
}

51
cmd/deps.go Normal file
View File

@@ -0,0 +1,51 @@
package cmd
import (
"os"
"github.com/olekukonko/tablewriter"
"github.com/sbrow/envr/app"
"github.com/spf13/cobra"
)
var depsCmd = &cobra.Command{
Use: "deps",
Short: "Check for missing binaries",
Long: `envr relies on external binaries for certain functionality.
The check command reports on which binaries are available and which are not.`,
RunE: func(cmd *cobra.Command, args []string) error {
db, err := app.Open()
if err != nil {
return err
} else {
defer db.Close(app.ReadOnly)
features := db.Features()
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"Feature", "Status"})
// Check Git
if features&app.Git == 1 {
table.Append([]string{"Git", "✓ Available"})
} else {
table.Append([]string{"Git", "✗ Missing"})
}
// Check fd
if features&app.Fd == app.Fd {
table.Append([]string{"fd", "✓ Available"})
} else {
table.Append([]string{"fd", "✗ Missing"})
}
table.Render()
return nil
}
},
}
func init() {
rootCmd.AddCommand(depsCmd)
}

View File

@@ -28,7 +28,7 @@ var scanCmd = &cobra.Command{
return err
}
files, err := db.Scan()
files, err := db.Scan(nil)
if err != nil {
return err
}

View File

@@ -1,8 +1,11 @@
package cmd
import (
"fmt"
"encoding/json"
"os"
"github.com/mattn/go-isatty"
"github.com/olekukonko/tablewriter"
"github.com/sbrow/envr/app"
"github.com/spf13/cobra"
)
@@ -22,33 +25,53 @@ var syncCmd = &cobra.Command{
if err != nil {
return err
} else {
for _, file := range files {
fmt.Printf("%s\n", file.Path)
type syncResult struct {
Path string `json:"path"`
Status string `json:"status"`
}
var results []syncResult
for _, file := range files {
// Syncronize the filesystem with the database.
changed, err := file.Sync()
var status string
switch changed {
case app.Updated:
fmt.Printf("File updated - changes saved\n")
status = "Backed Up"
if err := db.Insert(file); err != nil {
return err
}
case app.Restored:
fmt.Printf("File missing - restored backup\n")
status = "Restored"
case app.Error:
if err == nil {
panic("err cannot be nil when Sync returns Error")
} else {
fmt.Printf("%s\n", err)
}
status = err.Error()
case app.Noop:
fmt.Println("Nothing to do")
status = "OK"
default:
panic("Unknown result")
}
fmt.Println("")
results = append(results, syncResult{
Path: file.Path,
Status: status,
})
}
if isatty.IsTerminal(os.Stdout.Fd()) {
table := tablewriter.NewWriter(os.Stdout)
table.Header([]string{"File", "Status"})
for _, result := range results {
table.Append([]string{result.Path, result.Status})
}
table.Render()
} else {
encoder := json.NewEncoder(os.Stdout)
return encoder.Encode(results)
}
return nil

View File

@@ -44,7 +44,8 @@ at before, restore your backup with:
### SEE ALSO
* [envr backup](envr_backup.md) - Import a .env file into envr
* [envr check](envr_check.md) - Check for missing binaries
* [envr check](envr_check.md) - check if files in the current directory are backed up
* [envr deps](envr_deps.md) - Check for missing binaries
* [envr edit-config](envr_edit-config.md) - Edit your config with your default editor
* [envr init](envr_init.md) - Set up envr
* [envr list](envr_list.md) - View your tracked files

View File

@@ -1,15 +1,9 @@
## envr check
Check for missing binaries
### Synopsis
envr relies on external binaries for certain functionality.
The check command reports on which binaries are available and which are not.
check if files in the current directory are backed up
```
envr check [flags]
envr check [path] [flags]
```
### Options

24
docs/cli/envr_deps.md Normal file
View File

@@ -0,0 +1,24 @@
## envr deps
Check for missing binaries
### Synopsis
envr relies on external binaries for certain functionality.
The check command reports on which binaries are available and which are not.
```
envr deps [flags]
```
### Options
```
-h, --help help for deps
```
### SEE ALSO
* [envr](envr.md) - Manage your .env files.

View File

@@ -21,7 +21,12 @@
imports = [
inputs.treefmt-nix.flakeModule
];
systems = [ "x86_64-linux" ];
systems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
];
perSystem =
{ pkgs, system, inputs', ... }: {
@@ -56,7 +61,7 @@
packages.default = pkgs.buildGoModule rec {
pname = "envr";
version = "0.1.0";
version = "0.1.1";
src = ./.;
# If the build complains, uncomment this line
# vendorHash = "sha256:0000000000000000000000000000000000000000000000000000";
@@ -92,6 +97,9 @@
gotools
cobra-cli
# Build tools
zip
# IDE
unstable.helix
typescript-language-server

View File

@@ -2,6 +2,7 @@ package main
import "github.com/sbrow/envr/cmd"
// TODO: `envr check` command that looks in cwd and tells you if it's backed up or not.
func main() {
cmd.Execute()
}