Compare commits

..

8 Commits

Author SHA1 Message Date
3a80c77793 wip: 2025-11-05 18:31:55 -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
7 changed files with 213 additions and 67 deletions

1
.gitignore vendored
View File

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

13
CHANGELOG.md Normal file
View File

@@ -0,0 +1,13 @@
# Changelog
## [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

@@ -24,9 +24,11 @@ type SshKeyPair struct {
} }
type scanConfig struct { type scanConfig struct {
// TODO: Support multiple matchers
Matcher string `json:"matcher"` Matcher string `json:"matcher"`
Exclude string `json:"exclude"` // TODO: Support multiple excludes
Include string `json:"include"` Exclude string `json:"exclude"`
Include []string `json:"include"`
} }
// Create a fresh config with sensible defaults. // Create a fresh config with sensible defaults.
@@ -48,7 +50,7 @@ func NewConfig(privateKeyPaths []string) Config {
ScanConfig: scanConfig{ ScanConfig: scanConfig{
Matcher: "\\.env", Matcher: "\\.env",
Exclude: "*.envrc", Exclude: "*.envrc",
Include: "~", Include: []string{"~"},
}, },
} }
} }
@@ -110,71 +112,77 @@ func (c *Config) Save() error {
// Use fd to find all ignored .env files that match the config's parameters // Use fd to find all ignored .env files that match the config's parameters
func (c Config) scan() (paths []string, err error) { func (c Config) scan() (paths []string, err error) {
searchPath, err := c.searchPath() searchPaths, err := c.searchPaths()
if err != nil { if err != nil {
return []string{}, err return []string{}, err
} }
// Find all files (including ignored ones) for _, searchPath := range searchPaths {
fmt.Printf("Searching for all files in \"%s\"...\n", searchPath) // Find all files (including ignored ones)
allCmd := exec.Command("fd", "-a", c.ScanConfig.Matcher, "-E", c.ScanConfig.Exclude, "-HI", searchPath) fmt.Printf("Searching for all files in \"%s\"...\n", searchPath)
allOutput, err := allCmd.Output() allCmd := exec.Command("fd", "-a", c.ScanConfig.Matcher, "-E", c.ScanConfig.Exclude, "-HI", searchPath)
if err != nil { allOutput, err := allCmd.Output()
return []string{}, err if err != nil {
} return paths, err
allFiles := strings.Split(strings.TrimSpace(string(allOutput)), "\n")
if len(allFiles) == 1 && allFiles[0] == "" {
allFiles = []string{}
}
// Find unignored files
fmt.Printf("Search for unignored fies in \"%s\"...\n", searchPath)
unignoredCmd := exec.Command("fd", "-a", c.ScanConfig.Matcher, "-E", c.ScanConfig.Exclude, "-H", searchPath)
unignoredOutput, err := unignoredCmd.Output()
if err != nil {
return []string{}, err
}
unignoredFiles := strings.Split(strings.TrimSpace(string(unignoredOutput)), "\n")
if len(unignoredFiles) == 1 && unignoredFiles[0] == "" {
unignoredFiles = []string{}
}
// Create a map for faster lookup
unignoredMap := make(map[string]bool)
for _, file := range unignoredFiles {
unignoredMap[file] = true
}
// Filter to get only ignored files
var ignoredFiles []string
for _, file := range allFiles {
if !unignoredMap[file] {
ignoredFiles = append(ignoredFiles, file)
} }
allFiles := strings.Split(strings.TrimSpace(string(allOutput)), "\n")
if len(allFiles) == 1 && allFiles[0] == "" {
allFiles = []string{}
}
// Find unignored files
fmt.Printf("Search for unignored fies in \"%s\"...\n", searchPath)
unignoredCmd := exec.Command("fd", "-a", c.ScanConfig.Matcher, "-E", c.ScanConfig.Exclude, "-H", searchPath)
unignoredOutput, err := unignoredCmd.Output()
if err != nil {
return []string{}, err
}
unignoredFiles := strings.Split(strings.TrimSpace(string(unignoredOutput)), "\n")
if len(unignoredFiles) == 1 && unignoredFiles[0] == "" {
unignoredFiles = []string{}
}
// Create a map for faster lookup
unignoredMap := make(map[string]bool)
for _, file := range unignoredFiles {
unignoredMap[file] = true
}
// Filter to get only ignored files
var ignoredFiles []string
for _, file := range allFiles {
if !unignoredMap[file] {
ignoredFiles = append(ignoredFiles, file)
}
}
paths = append(paths, ignoredFiles...)
} }
return ignoredFiles, nil return paths, nil
} }
func (c Config) searchPath() (path string, err error) { func (c Config) searchPaths() (paths []string, err error) {
include := c.ScanConfig.Include homeDir, err := os.UserHomeDir()
if include == "~" {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
return homeDir, nil
}
absPath, err := filepath.Abs(include)
if err != nil { if err != nil {
return "", err return paths, err
} }
return absPath, nil includes := c.ScanConfig.Include
for _, include := range includes {
path := strings.Replace(include, "~", homeDir, 1)
absPath, err := filepath.Abs(path)
if err != nil {
return paths, err
}
paths = append(paths, absPath)
}
return paths, nil
} }
// TODO: Should this be private? // TODO: Should this be private?

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package main
import "github.com/sbrow/envr/cmd" 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() { func main() {
cmd.Execute() cmd.Execute()
} }