mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
refactor(odin): Ported init command.
This commit is contained in:
@@ -51,7 +51,7 @@ func NewConfig(privateKeyPaths []string) Config {
|
|||||||
Matcher: "\\.env",
|
Matcher: "\\.env",
|
||||||
Exclude: []string{
|
Exclude: []string{
|
||||||
"*\\.envrc",
|
"*\\.envrc",
|
||||||
"\\.local/",
|
"\\.local",
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"vendor",
|
"vendor",
|
||||||
},
|
},
|
||||||
|
|||||||
2
cli.odin
2
cli.odin
@@ -36,6 +36,7 @@ COMMANDS := []CommandInfo{
|
|||||||
}
|
}
|
||||||
|
|
||||||
IMPLEMENTED_COMMANDS := []string{
|
IMPLEMENTED_COMMANDS := []string{
|
||||||
|
"init",
|
||||||
"version",
|
"version",
|
||||||
"deps",
|
"deps",
|
||||||
"list",
|
"list",
|
||||||
@@ -46,6 +47,7 @@ IMPLEMENTED_COMMANDS := []string{
|
|||||||
"edit-config",
|
"edit-config",
|
||||||
"check",
|
"check",
|
||||||
"scan",
|
"scan",
|
||||||
|
"sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_args :: proc() -> (cmd: Command, ok: bool) {
|
parse_args :: proc() -> (cmd: Command, ok: bool) {
|
||||||
|
|||||||
53
cmd_init.odin
Normal file
53
cmd_init.odin
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
|
||||||
|
cmd_init :: proc(cmd: ^Command) {
|
||||||
|
force := has_flag(cmd, "force") || has_flag(cmd, "f")
|
||||||
|
|
||||||
|
_, cfg_exists := load_config()
|
||||||
|
if cfg_exists && !force {
|
||||||
|
fmt.println("You have already initialized envr.")
|
||||||
|
fmt.println("Run again with the --force flag if you want to reinitialize.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, ok := find_ssh_private_keys()
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys) == 0 {
|
||||||
|
fmt.println("No SSH private keys found in ~/.ssh")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selected, result := multi_select("Select SSH private keys:", keys[:])
|
||||||
|
if result == .Cancel {
|
||||||
|
fmt.println("\x1b[2mCancelled.\x1b[0m")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selected_paths := make([dynamic]string, 0, min(1, len(keys) / 2))
|
||||||
|
for i in 0 ..< len(keys) {
|
||||||
|
if selected[i] {
|
||||||
|
append(&selected_paths, keys[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(selected_paths) == 0 {
|
||||||
|
fmt.println("No SSH keys selected - Config not created")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := new_config(selected_paths[:])
|
||||||
|
if !save_config(cfg, force = force) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.printf(
|
||||||
|
"Config initialized with %d SSH key(s). You are ready to use envr.\n",
|
||||||
|
len(selected_paths),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
95
cmd_sync.odin
Normal file
95
cmd_sync.odin
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:encoding/json"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
SyncEntry :: struct {
|
||||||
|
Path: string `json:"path"`,
|
||||||
|
Status: string `json:"status"`,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_sync :: proc(cmd: ^Command) {
|
||||||
|
db, db_ok := db_open()
|
||||||
|
if !db_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db_close(&db)
|
||||||
|
|
||||||
|
files, list_ok := db_list(&db)
|
||||||
|
if !list_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer delete(files)
|
||||||
|
|
||||||
|
results: [dynamic]SyncEntry
|
||||||
|
|
||||||
|
for &file in files {
|
||||||
|
old_path: string
|
||||||
|
old_path, _ = strings.clone(file.Path)
|
||||||
|
|
||||||
|
result, err_msg := db_sync(&db, &file)
|
||||||
|
|
||||||
|
status: string
|
||||||
|
s := i32(result)
|
||||||
|
is_error := (s & i32(SyncResult.Error)) != 0
|
||||||
|
is_backed := (s & i32(SyncResult.BackedUp)) != 0
|
||||||
|
is_restored := (s & i32(SyncResult.Restored)) != 0
|
||||||
|
is_dir_updated := (s & i32(SyncResult.DirUpdated)) != 0
|
||||||
|
|
||||||
|
if is_error {
|
||||||
|
if len(err_msg) > 0 {
|
||||||
|
status = err_msg
|
||||||
|
} else {
|
||||||
|
status = "error"
|
||||||
|
}
|
||||||
|
} else if is_backed {
|
||||||
|
status = "Backed Up"
|
||||||
|
if !db_insert(&db, file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if is_restored {
|
||||||
|
status = "Restored"
|
||||||
|
} else if is_dir_updated && !is_restored {
|
||||||
|
status = "Moved"
|
||||||
|
} else {
|
||||||
|
status = "OK"
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_dir_updated {
|
||||||
|
if !db_delete(&db, old_path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if db_update_required(result) {
|
||||||
|
if !db_insert(&db, file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path_str, _ := strings.clone(file.Path)
|
||||||
|
status_str, _ := strings.clone(status)
|
||||||
|
append(&results, SyncEntry{Path = path_str, Status = status_str})
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_tty() {
|
||||||
|
headers := []string{"File", "Status"}
|
||||||
|
table_rows := make([dynamic][]string, 0, len(results))
|
||||||
|
|
||||||
|
for res in results {
|
||||||
|
row_slice := make([]string, 2)
|
||||||
|
row_slice[0] = res.Path
|
||||||
|
row_slice[1] = res.Status
|
||||||
|
append(&table_rows, row_slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
render_table(headers, table_rows[:])
|
||||||
|
} else {
|
||||||
|
data, marshal_err := json.marshal(results[:])
|
||||||
|
if marshal_err != nil {
|
||||||
|
fmt.printf("Error marshaling JSON: %v\n", marshal_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.println(string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
135
config.odin
135
config.odin
@@ -61,6 +61,119 @@ data_age_path :: proc() -> string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
find_ssh_private_keys :: proc() -> (keys: [dynamic]string, ok: bool) {
|
||||||
|
home, home_err := os.user_home_dir(context.allocator)
|
||||||
|
if home_err != nil {
|
||||||
|
fmt.printf("Error getting home dir: %v\n", home_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_dir, join_err := filepath.join([]string{home, ".ssh"})
|
||||||
|
if join_err != nil {
|
||||||
|
fmt.printf("Error building ssh path: %v\n", join_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, dir_err := os.read_all_directory_by_path(ssh_dir, context.allocator)
|
||||||
|
if dir_err != nil {
|
||||||
|
fmt.printf("Could not read ~/.ssh directory: %v\n", dir_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.file_info_slice_delete(entries, context.allocator)
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
name := entry.name
|
||||||
|
if entry.type == .Directory {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.has_suffix(name, ".pub") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.contains(name, "known_hosts") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.contains(name, "config") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
full_path, _ := filepath.join([]string{ssh_dir, name})
|
||||||
|
append(&keys, full_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
new_config :: proc(private_key_paths: []string) -> Config {
|
||||||
|
keys := make([dynamic]SshKeyPair, 0, len(private_key_paths))
|
||||||
|
for priv in private_key_paths {
|
||||||
|
pub, _ := strings.concatenate([]string{priv, ".pub"})
|
||||||
|
append(&keys, SshKeyPair{Private = priv, Public = pub})
|
||||||
|
}
|
||||||
|
|
||||||
|
exclude := make([dynamic]string, 0, 4)
|
||||||
|
append(&exclude, "*\\.envrc")
|
||||||
|
append(&exclude, "\\.local")
|
||||||
|
append(&exclude, "node_modules")
|
||||||
|
append(&exclude, "vendor")
|
||||||
|
|
||||||
|
include := make([dynamic]string, 0, 1)
|
||||||
|
append(&include, "~")
|
||||||
|
|
||||||
|
scan_cfg := ScanConfig {
|
||||||
|
Matcher = "\\.env",
|
||||||
|
Exclude = exclude[:],
|
||||||
|
Include = include[:],
|
||||||
|
}
|
||||||
|
|
||||||
|
return Config{Keys = keys[:], ScanConfig = scan_cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
save_config :: proc(cfg: Config, force: bool = false) -> bool {
|
||||||
|
home, home_err := os.user_home_dir(context.allocator)
|
||||||
|
if home_err != nil {
|
||||||
|
fmt.printf("Error getting home dir: %v\n", home_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
config_dir, _ := filepath.join([]string{home, ".envr"})
|
||||||
|
|
||||||
|
if !os.exists(config_dir) {
|
||||||
|
mkdir_err := os.make_directory(config_dir)
|
||||||
|
if mkdir_err != nil {
|
||||||
|
fmt.printf("Error creating ~/.envr directory: %v\n", mkdir_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config_path, _ := filepath.join([]string{config_dir, "config.json"})
|
||||||
|
|
||||||
|
if !force && os.exists(config_path) {
|
||||||
|
info, stat_err := os.stat(config_path, context.allocator)
|
||||||
|
if stat_err == nil {
|
||||||
|
defer os.file_info_delete(info, context.allocator)
|
||||||
|
if info.size > 0 {
|
||||||
|
fmt.println("Config file already exists. Run again with --force to reinitialize.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, marshal_err := json.marshal(cfg, {pretty = true, use_spaces = true, spaces = 2})
|
||||||
|
if marshal_err != nil {
|
||||||
|
fmt.printf("Error marshaling config: %v\n", marshal_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
write_err := os.write_entire_file(config_path, data)
|
||||||
|
if write_err != nil {
|
||||||
|
fmt.printf("Error writing config: %v\n", write_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
search_paths :: proc(cfg: Config) -> (paths: [dynamic]string) {
|
search_paths :: proc(cfg: Config) -> (paths: [dynamic]string) {
|
||||||
home, _ := os.user_home_dir(context.allocator)
|
home, _ := os.user_home_dir(context.allocator)
|
||||||
|
|
||||||
@@ -79,3 +192,25 @@ search_paths :: proc(cfg: Config) -> (paths: [dynamic]string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
find_git_roots :: proc(cfg: Config) -> (roots: [dynamic]string, ok: bool) {
|
||||||
|
paths := search_paths(cfg)
|
||||||
|
|
||||||
|
for sp in paths {
|
||||||
|
args := []string{"fd", "-H", "-t", "d", "^\\.git$", sp}
|
||||||
|
lines, fd_ok := run_fd(args)
|
||||||
|
if !fd_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
cleaned, _ := filepath.clean(line)
|
||||||
|
parent := filepath.dir(cleaned)
|
||||||
|
cloned, _ := strings.clone(parent)
|
||||||
|
append(&roots, cloned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
151
db.odin
151
db.odin
@@ -12,6 +12,19 @@ import "core:time"
|
|||||||
|
|
||||||
import "sqlite"
|
import "sqlite"
|
||||||
|
|
||||||
|
SyncResult :: enum i32 {
|
||||||
|
Noop = 0,
|
||||||
|
DirUpdated = 1,
|
||||||
|
Restored = 1 << 1,
|
||||||
|
BackedUp = 1 << 2,
|
||||||
|
Error = 1 << 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncDirection :: enum {
|
||||||
|
TrustDatabase,
|
||||||
|
TrustFilesystem,
|
||||||
|
}
|
||||||
|
|
||||||
Db :: struct {
|
Db :: struct {
|
||||||
db: ^rawptr,
|
db: ^rawptr,
|
||||||
cfg: Config,
|
cfg: Config,
|
||||||
@@ -470,3 +483,141 @@ string_to_cstring :: proc(s: string) -> cstring {
|
|||||||
cs, _ := strings.clone_to_cstring(s)
|
cs, _ := strings.clone_to_cstring(s)
|
||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db_update_required :: proc(status: SyncResult) -> bool {
|
||||||
|
s := i32(status)
|
||||||
|
return (s & (i32(SyncResult.BackedUp) | i32(SyncResult.DirUpdated))) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
|
||||||
|
remote_set: map[string]bool
|
||||||
|
for r in f.Remotes {
|
||||||
|
remote_set[r] = true
|
||||||
|
}
|
||||||
|
for r in remotes {
|
||||||
|
if r in remote_set {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
update_dir :: proc(f: ^EnvFile, new_dir: string) {
|
||||||
|
f.Dir = new_dir
|
||||||
|
base := filepath.base(f.Path)
|
||||||
|
new_path, _ := strings.concatenate({new_dir, "/", base})
|
||||||
|
f.Path = new_path
|
||||||
|
f.Remotes = get_git_remotes(new_dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) {
|
||||||
|
feats := check_features()
|
||||||
|
if !has_feature(feats, .Fd) || !has_feature(feats, .Git) {
|
||||||
|
fmt.println("Error: fd and git are required for moved dir detection")
|
||||||
|
return {}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
roots, roots_ok := find_git_roots(d.cfg)
|
||||||
|
if !roots_ok {
|
||||||
|
return {}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
moved: [dynamic]string
|
||||||
|
for root in roots {
|
||||||
|
remotes := get_git_remotes(root)
|
||||||
|
if shares_remote(f, remotes[:]) {
|
||||||
|
cloned, _ := strings.clone(root)
|
||||||
|
append(&moved, cloned)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return moved, true
|
||||||
|
}
|
||||||
|
|
||||||
|
env_file_backup :: proc(f: ^EnvFile) -> bool {
|
||||||
|
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
|
||||||
|
if read_err != nil {
|
||||||
|
fmt.printf("Error reading file %s: %v\n", f.Path, read_err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
f.contents = string(data)
|
||||||
|
digest := hash.hash_bytes(hash.Algorithm.SHA256, data)
|
||||||
|
hex_bytes, _ := hex.encode(digest)
|
||||||
|
f.Sha256 = string(hex_bytes)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncResult, string) {
|
||||||
|
result: SyncResult = .Noop
|
||||||
|
err_msg: string
|
||||||
|
|
||||||
|
_, stat_err := os.stat(f.Dir, context.allocator)
|
||||||
|
if stat_err != nil {
|
||||||
|
moved_dirs: [dynamic]string
|
||||||
|
|
||||||
|
if d != nil {
|
||||||
|
dirs, dirs_ok := find_moved_dirs(d, f)
|
||||||
|
if !dirs_ok {
|
||||||
|
return .Error, "failed to find moved dirs"
|
||||||
|
}
|
||||||
|
moved_dirs = dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(moved_dirs) == 0 {
|
||||||
|
return .Error, "directory missing"
|
||||||
|
} else if len(moved_dirs) == 1 {
|
||||||
|
update_dir(f, moved_dirs[0])
|
||||||
|
result = .DirUpdated
|
||||||
|
} else {
|
||||||
|
return .Error, "multiple directories found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, file_stat_err := os.stat(f.Path, context.allocator)
|
||||||
|
if file_stat_err != nil {
|
||||||
|
write_err := os.write_entire_file(f.Path, f.contents)
|
||||||
|
if write_err != nil {
|
||||||
|
msg, _ := strings.concatenate({"failed to write file: ", fmt.aprintf("%v", write_err)})
|
||||||
|
return .Error, msg
|
||||||
|
}
|
||||||
|
|
||||||
|
s := i32(result) | i32(SyncResult.Restored)
|
||||||
|
return SyncResult(s), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
|
||||||
|
if read_err != nil {
|
||||||
|
msg, _ := strings.concatenate({"failed to read file for SHA comparison: ", fmt.aprintf("%v", read_err)})
|
||||||
|
return .Error, msg
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := hash.hash_bytes(hash.Algorithm.SHA256, data)
|
||||||
|
hex_bytes, _ := hex.encode(digest)
|
||||||
|
current_sha := string(hex_bytes)
|
||||||
|
|
||||||
|
if current_sha == f.Sha256 {
|
||||||
|
return result, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dir {
|
||||||
|
case .TrustDatabase:
|
||||||
|
write_err := os.write_entire_file(f.Path, f.contents)
|
||||||
|
if write_err != nil {
|
||||||
|
msg, _ := strings.concatenate({"failed to write file: ", fmt.aprintf("%v", write_err)})
|
||||||
|
return .Error, msg
|
||||||
|
}
|
||||||
|
s := i32(result) | i32(SyncResult.Restored)
|
||||||
|
return SyncResult(s), ""
|
||||||
|
case .TrustFilesystem:
|
||||||
|
if !env_file_backup(f) {
|
||||||
|
return .Error, "failed to backup file"
|
||||||
|
}
|
||||||
|
return .BackedUp, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncResult, string) {
|
||||||
|
return env_file_sync(f, .TrustFilesystem, d)
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ main :: proc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch cmd.name {
|
switch cmd.name {
|
||||||
|
case "init":
|
||||||
|
cmd_init(&cmd)
|
||||||
case "version":
|
case "version":
|
||||||
cmd_version(&cmd)
|
cmd_version(&cmd)
|
||||||
case "deps":
|
case "deps":
|
||||||
@@ -35,6 +37,8 @@ main :: proc() {
|
|||||||
cmd_check(&cmd)
|
cmd_check(&cmd)
|
||||||
case "scan":
|
case "scan":
|
||||||
cmd_scan(&cmd)
|
cmd_scan(&cmd)
|
||||||
|
case "sync":
|
||||||
|
cmd_sync(&cmd)
|
||||||
case:
|
case:
|
||||||
fmt.printf("Unknown command: %s\n", cmd.name)
|
fmt.printf("Unknown command: %s\n", cmd.name)
|
||||||
print_usage()
|
print_usage()
|
||||||
|
|||||||
10
stubs.odin
10
stubs.odin
@@ -1,11 +1 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
|
||||||
|
|
||||||
cmd_init :: proc(cmd: ^Command) {
|
|
||||||
fmt.println("TODO: init")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd_sync :: proc(cmd: ^Command) {
|
|
||||||
fmt.println("TODO: sync")
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user