Compare commits

14 Commits

Author SHA1 Message Date
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
15 changed files with 279 additions and 80 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 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

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

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.
@@ -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?

View File

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

View File

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

View File

@@ -8,8 +8,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var checkCmd = &cobra.Command{ var depsCmd = &cobra.Command{
Use: "check", Use: "deps",
Short: "Check for missing binaries", Short: "Check for missing binaries",
Long: `envr relies on external binaries for certain functionality. Long: `envr relies on external binaries for certain functionality.
@@ -33,7 +33,7 @@ The check command reports on which binaries are available and which are not.`,
} }
// Check fd // Check fd
if features&app.Fd == 1 { if features&app.Fd == app.Fd {
table.Append([]string{"fd", "✓ Available"}) table.Append([]string{"fd", "✓ Available"})
} else { } else {
table.Append([]string{"fd", "✗ Missing"}) table.Append([]string{"fd", "✗ Missing"})
@@ -47,5 +47,5 @@ The check command reports on which binaries are available and which are not.`,
} }
func init() { func init() {
rootCmd.AddCommand(checkCmd) rootCmd.AddCommand(depsCmd)
} }

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

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

View File

@@ -1,4 +1,4 @@
## envr check ## envr deps
Check for missing binaries Check for missing binaries
@@ -9,13 +9,13 @@ envr relies on external binaries for certain functionality.
The check command reports on which binaries are available and which are not. The check command reports on which binaries are available and which are not.
``` ```
envr check [flags] envr deps [flags]
``` ```
### Options ### Options
``` ```
-h, --help help for check -h, --help help for deps
``` ```
### SEE ALSO ### SEE ALSO

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()
} }