mirror of
https://github.com/sbrow/envr.git
synced 2025-12-29 23:47:39 -05:00
Compare commits
7 Commits
9ab72a25fa
...
af0a9f9c4c
| Author | SHA1 | Date | |
|---|---|---|---|
| af0a9f9c4c | |||
| 4273fa5895 | |||
| bb3c0cdeee | |||
|
|
df5fdeee67 | ||
| 42796ec77b | |||
| 8a349dd760 | |||
| 7a49858a58 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,5 +5,6 @@
|
|||||||
man
|
man
|
||||||
|
|
||||||
# build artifacts
|
# build artifacts
|
||||||
|
builds
|
||||||
envr
|
envr
|
||||||
result
|
result
|
||||||
|
|||||||
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal 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
92
Makefile
Normal 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)"
|
||||||
120
app/config.go
120
app/config.go
@@ -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.
|
||||||
@@ -47,7 +49,7 @@ func NewConfig(privateKeyPaths []string) Config {
|
|||||||
ScanConfig: scanConfig{
|
ScanConfig: scanConfig{
|
||||||
Matcher: "\\.env",
|
Matcher: "\\.env",
|
||||||
Exclude: "*.envrc",
|
Exclude: "*.envrc",
|
||||||
Include: "~",
|
Include: []string{"~"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,71 +111,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?
|
||||||
|
|||||||
41
cmd/sync.go
41
cmd/sync.go
@@ -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
|
||||||
|
|||||||
12
flake.nix
12
flake.nix
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user