Port to latest libmutton development version (use go-boilerplate)

This commit is contained in:
2025-05-09 16:57:57 +00:00
parent 2f34fc2c0c
commit 54a7d51efa
11 changed files with 101 additions and 151 deletions
+2 -1
View File
@@ -5,7 +5,8 @@ go 1.24.3
require ( require (
github.com/Trojan2021/BEAN v0.0.0-20241210230804-8f294833b514 github.com/Trojan2021/BEAN v0.0.0-20241210230804-8f294833b514
github.com/charmbracelet/glamour v0.7.0 github.com/charmbracelet/glamour v0.7.0
github.com/rwinkhart/libmutton v0.3.2-0.20250508234242-fb83d29a23e0 github.com/rwinkhart/go-boilerplate v0.0.0-20250509154735-0846290a7620
github.com/rwinkhart/libmutton v0.3.2-0.20250509161203-03c040b373b6
golang.org/x/term v0.32.0 golang.org/x/term v0.32.0
) )
+4 -2
View File
@@ -59,10 +59,12 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rwinkhart/convertroman v0.2.0 h1:otUm939eXT77q/Lr31mJtMwVWEw0RjEUB71gO65h9OM= github.com/rwinkhart/convertroman v0.2.0 h1:otUm939eXT77q/Lr31mJtMwVWEw0RjEUB71gO65h9OM=
github.com/rwinkhart/convertroman v0.2.0/go.mod h1:Af6HqvX0EIM4Y3HcnNXbbRey1dLasGB7WU0+3Cowgmw= github.com/rwinkhart/convertroman v0.2.0/go.mod h1:Af6HqvX0EIM4Y3HcnNXbbRey1dLasGB7WU0+3Cowgmw=
github.com/rwinkhart/go-boilerplate v0.0.0-20250509154735-0846290a7620 h1:MjxVq+EqJgB/sxgSbPTTuzbOrU61kKa4oJvjqqdEoO4=
github.com/rwinkhart/go-boilerplate v0.0.0-20250509154735-0846290a7620/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY=
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410 h1:NhHwFM3Pgm6zRUfFKvi0p5ndjfFbVWsRwmmhyFlG4PE= github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410 h1:NhHwFM3Pgm6zRUfFKvi0p5ndjfFbVWsRwmmhyFlG4PE=
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/rwinkhart/libmutton v0.3.2-0.20250508234242-fb83d29a23e0 h1:XoJyvbNJBU3HbYHlyYBh3VKzVad10wOZ/feFkBTvxWU= github.com/rwinkhart/libmutton v0.3.2-0.20250509161203-03c040b373b6 h1:sV+cBjbQHX1++LrmGpcJ3+QOF6SYJDfPNuIywzqHK10=
github.com/rwinkhart/libmutton v0.3.2-0.20250508234242-fb83d29a23e0/go.mod h1:IW7espY4xtdVTd0/6THtmRadR+utzH1dDXcgrP3VqlM= github.com/rwinkhart/libmutton v0.3.2-0.20250509161203-03c040b373b6/go.mod h1:qUMh7UidlnhcAyLEygYV8z87le46MnL71r8b6MvJL4w=
github.com/rwinkhart/peercred-mini v0.0.0-20250407033241-c09add2eceea h1:VE2ti/AE4Y3kgnyK+J5c5hpddE+dKHpsFch3K2R6puQ= github.com/rwinkhart/peercred-mini v0.0.0-20250407033241-c09add2eceea h1:VE2ti/AE4Y3kgnyK+J5c5hpddE+dKHpsFch3K2R6puQ=
github.com/rwinkhart/peercred-mini v0.0.0-20250407033241-c09add2eceea/go.mod h1:t+YkvAdnTKTrg4d469tw3K+GCUzX/Bja4h8yKjSIsGs= github.com/rwinkhart/peercred-mini v0.0.0-20250407033241-c09add2eceea/go.mod h1:t+YkvAdnTKTrg4d469tw3K+GCUzX/Bja4h8yKjSIsGs=
github.com/rwinkhart/rcw v0.0.0-20250508234041-b49d9f1eea42 h1:/vsBCwHP22cmqrtjsRB1OcPcb+sckktdeFlHKmhzD54= github.com/rwinkhart/rcw v0.0.0-20250508234041-b49d9f1eea42 h1:/vsBCwHP22cmqrtjsRB1OcPcb+sckktdeFlHKmhzD54=
+4 -3
View File
@@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/rwinkhart/MUTN/src/cli" "github.com/rwinkhart/MUTN/src/cli"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/sync" "github.com/rwinkhart/libmutton/sync"
) )
@@ -96,7 +97,7 @@ func main() {
case "note", "-n": case "note", "-n":
field = 4 field = 4
case "rename", "-r": case "rename", "-r":
core.TargetIsFile(targetLocation, true, 0) // ensure location exists before prompting for new location back.TargetIsFile(targetLocation, true, 0) // ensure location exists before prompting for new location
cli.RenameCli(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory cli.RenameCli(args[1]) // pass the incomplete path as the server and all clients (reading from the deletions directory) will have a different home directory
default: default:
cli.HelpEdit() cli.HelpEdit()
@@ -161,11 +162,11 @@ func main() {
case "startrcwd": case "startrcwd":
core.RCWDArgument() core.RCWDArgument()
case "sync": case "sync":
cli.RunJobWrapper(true) sync.RunJob(true, false)
case "init": case "init":
cli.TempInitCli() cli.TempInitCli()
case "tweak": case "tweak":
core.PrintError("\"tweak\" is not yet implemented", 0, true) back.PrintError("\"tweak\" is not yet implemented", 0, true)
case "copy": case "copy":
cli.HelpCopy() cli.HelpCopy()
case "edit": case "edit":
+2 -3
View File
@@ -3,6 +3,7 @@ package cli
import ( import (
"os" "os"
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
"golang.org/x/term" "golang.org/x/term"
) )
@@ -19,7 +20,5 @@ const (
) )
func init() { func init() {
core.GetPassphrase = func() []byte { core.GetPassphrase = front.InputHidden
return inputHidden("RCW Passphrase:")
}
} }
+6 -5
View File
@@ -1,6 +1,7 @@
package cli package cli
import ( import (
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
) )
@@ -13,19 +14,19 @@ func AddEntry(targetLocation string, hideSecrets bool, entryType uint8) {
var unencryptedEntry []string var unencryptedEntry []string
if entryType < 2 { if entryType < 2 {
username := input("Username:") username := front.Input("Username:")
// determine whether to generate the password // determine whether to generate the password
var password string var password string
if entryType == 0 { if entryType == 0 {
password = string(inputHidden("Password:")) password = string(front.InputHidden("Password:"))
} else { } else {
password = inputPasswordGen() password = inputPasswordGen()
} }
totp := string(inputHidden("TOTP secret:")) totp := string(front.InputHidden("TOTP secret:"))
url := input("URL:") url := front.Input("URL:")
if inputBinary("Add a note to this entry?") { if front.InputBinary("Add a note to this entry?") {
note, _ := editNote([]string{}) note, _ := editNote([]string{})
unencryptedEntry = append([]string{password, username, totp, url}, note...) unencryptedEntry = append([]string{password, username, totp, url}, note...)
} else { } else {
+12 -10
View File
@@ -6,6 +6,8 @@ import (
"os/exec" "os/exec"
"reflect" "reflect"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/sync" "github.com/rwinkhart/libmutton/sync"
) )
@@ -13,7 +15,7 @@ import (
// RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server. // RenameCli renames an entry at oldLocationIncomplete to a new location (user input) on both the client and the server.
func RenameCli(oldLocationIncomplete string) { func RenameCli(oldLocationIncomplete string) {
// prompt user for new location and rename // prompt user for new location and rename
newLocationIncomplete := input("New location:") newLocationIncomplete := front.Input("New location:")
sync.RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete, false) sync.RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete, false)
// exit is done from sync.RenameRemoteFromClient // exit is done from sync.RenameRemoteFromClient
@@ -27,13 +29,13 @@ func EditEntryField(targetLocation string, hideSecrets bool, field int) {
// edit the field // edit the field
switch field { switch field {
case 0: case 0:
unencryptedEntry[field] = string(inputHidden("Password:")) unencryptedEntry[field] = string(front.InputHidden("Password:"))
case 1: case 1:
unencryptedEntry[field] = input("Username:") unencryptedEntry[field] = front.Input("Username:")
case 2: case 2:
unencryptedEntry[field] = string(inputHidden("TOTP secret:")) unencryptedEntry[field] = string(front.InputHidden("TOTP secret:"))
case 3: case 3:
unencryptedEntry[field] = input("URL:") unencryptedEntry[field] = front.Input("URL:")
case 4: // edit notes fields case 4: // edit notes fields
// store note and non-note data separately // store note and non-note data separately
nonNoteData := unencryptedEntry[:4] nonNoteData := unencryptedEntry[:4]
@@ -42,7 +44,7 @@ func EditEntryField(targetLocation string, hideSecrets bool, field int) {
// edit the note // edit the note
editedNote, noteEdited := editNote(noteData) editedNote, noteEdited := editNote(noteData)
if !noteEdited { // exit early if the note was not edited if !noteEdited { // exit early if the note was not edited
core.PrintError("Entry is unchanged", 0, true) back.PrintError("Entry is unchanged", 0, true)
} }
unencryptedEntry = append(nonNoteData, editedNote...) unencryptedEntry = append(nonNoteData, editedNote...)
} }
@@ -66,7 +68,7 @@ func GenUpdate(targetLocation string, hideSecrets bool) {
// editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty). // editNote uses the user-specified text editor to edit an existing note (or create a new one if baseNote is empty).
// Returns the edited note and a boolean indicating whether the note was edited. // Returns the edited note and a boolean indicating whether the note was edited.
func editNote(baseNote []string) ([]string, bool) { func editNote(baseNote []string) ([]string, bool) {
tempFile := core.CreateTempFile() tempFile := back.CreateTempFile()
defer func(name string) { defer func(name string) {
_ = os.Remove(name) // error ignored; if the file could be created, it can probably be removed _ = os.Remove(name) // error ignored; if the file could be created, it can probably be removed
}(tempFile.Name()) }(tempFile.Name())
@@ -102,13 +104,13 @@ func editNote(baseNote []string) ([]string, bool) {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
panic(core.AnsiError + "Failed to write note with " + editor + core.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement panic(back.AnsiError + "Failed to write note with " + editor + back.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement
} }
// open the tempFile for reading // open the tempFile for reading
tempFile, err = os.Open(tempFile.Name()) tempFile, err = os.Open(tempFile.Name())
if err != nil { if err != nil {
panic(core.AnsiError + "Failed to read note written with " + editor + core.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement panic(back.AnsiError + "Failed to read note written with " + editor + back.AnsiReset) // panic is used to ensure the tempFile is removed, as per the defer statement
} }
// read the edited note from the tempFile // read the edited note from the tempFile
@@ -122,7 +124,7 @@ func editNote(baseNote []string) ([]string, bool) {
_ = tempFile.Close() // error ignored; if the file could be opened, it can probably be closed _ = tempFile.Close() // error ignored; if the file could be opened, it can probably be closed
// remove trailing empty strings from the edited note // remove trailing empty strings from the edited note
note = core.RemoveTrailingEmptyStrings(note) note = back.RemoveTrailingEmptyStrings(note)
// clamp trailing whitespace in each note line // clamp trailing whitespace in each note line
core.ClampTrailingWhitespace(note) core.ClampTrailingWhitespace(note)
+6 -6
View File
@@ -5,7 +5,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/sync" "github.com/rwinkhart/libmutton/sync"
) )
@@ -72,7 +72,7 @@ func printFileEntry(entry string, lastSlash, charCounter, indent int, colorAlter
} }
// print fileEntryName to screen // print fileEntryName to screen
fmt.Printf("%s%s%s ", colorCode, fileEntryName, core.AnsiReset) fmt.Printf("%s%s%s ", colorCode, fileEntryName, back.AnsiReset)
return charCounter, colorAlternator return charCounter, colorAlternator
} }
@@ -82,7 +82,7 @@ func EntryListGen() {
fileList, dirList := sync.WalkEntryDir() fileList, dirList := sync.WalkEntryDir()
// print header bar w/total entry count // print header bar w/total entry count
fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+core.AnsiReset) fmt.Print("\n"+ansiBlackOnWhite, len(fileList), " libmutton entries:"+back.AnsiReset)
// dirList iteration // dirList iteration
dirListLength := len(dirList) // save length for multiple references below dirListLength := len(dirList) // save length for multiple references below
@@ -128,7 +128,7 @@ func EntryListGen() {
containsFiles = true containsFiles = true
skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped
indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+core.AnsiReset+"\n", vanityDirectory) fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory)
} }
charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator) charCounter, colorAlternator = printFileEntry(file, lastSlash, charCounter, indent, colorAlternator)
@@ -140,8 +140,8 @@ func EntryListGen() {
if dirListLength > 1 { // and directories besides the root-level exist... display directory header and empty directory warning if dirListLength > 1 { // and directories besides the root-level exist... display directory header and empty directory warning
skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped skippedDirList[i] = false // the directory header is being printed, indicate that it is not being skipped
indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier indent, vanityDirectory = determineIndentation(skippedDirList, dirList, i) // calculate the final indentation multiplier
fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+core.AnsiReset+"\n", vanityDirectory) fmt.Printf("\n\n"+strings.Repeat(" ", indent*2)+ansiDirectoryHeader+"%s/"+back.AnsiReset+"\n", vanityDirectory)
fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + core.AnsiReset) fmt.Print(strings.Repeat(" ", indent*2) + ansiEmptyDirectoryWarning + "-empty directory-" + back.AnsiReset)
} else { // warn if the only thing that exists is the root-level directory } else { // warn if the only thing that exists is the root-level directory
fmt.Print("\n\nNothing's here! For help creating your first entry, run \"mutn help\".") fmt.Print("\n\nNothing's here! For help creating your first entry, run \"mutn help\".")
} }
+11 -9
View File
@@ -4,7 +4,9 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/sync"
) )
const ansiShownPassword = "\033[38;5;10m" const ansiShownPassword = "\033[38;5;10m"
@@ -19,33 +21,33 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
// if the first field (password) is not empty, print it // if the first field (password) is not empty, print it
if decryptedEntry[0] != "" { if decryptedEntry[0] != "" {
if !hideSecrets { if !hideSecrets {
fmt.Print(ansiDirectoryHeader + "Password:" + core.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + core.AnsiReset + "\n\n") fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[0] + back.AnsiReset + "\n\n")
} else { } else {
fmt.Print(ansiDirectoryHeader + "Password:" + core.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + core.AnsiReset + "\n\n") fmt.Print(ansiDirectoryHeader + "Password:" + back.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
} }
} }
case 1: case 1:
// if the second field (username) is not empty, print it // if the second field (username) is not empty, print it
if decryptedEntry[1] != "" { if decryptedEntry[1] != "" {
fmt.Print(ansiDirectoryHeader + "Username:" + core.AnsiReset + "\n" + decryptedEntry[1] + "\n\n") fmt.Print(ansiDirectoryHeader + "Username:" + back.AnsiReset + "\n" + decryptedEntry[1] + "\n\n")
} }
case 2: case 2:
// if the third field (TOTP secret) is not empty, print it // if the third field (TOTP secret) is not empty, print it
if decryptedEntry[2] != "" { if decryptedEntry[2] != "" {
if !hideSecrets { if !hideSecrets {
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + core.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + core.AnsiReset + "\n\n") fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiShownPassword + decryptedEntry[2] + back.AnsiReset + "\n\n")
} else { } else {
fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + core.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + core.AnsiReset + "\n\n") fmt.Print(ansiDirectoryHeader + "TOTP Secret:" + back.AnsiReset + "\n" + ansiEmptyDirectoryWarning + "End command in \"show\" or \"-s\" to view" + back.AnsiReset + "\n\n")
} }
} }
case 3: case 3:
// if the fourth field (url) is not empty, print it // if the fourth field (url) is not empty, print it
if decryptedEntry[3] != "" { if decryptedEntry[3] != "" {
fmt.Print(ansiDirectoryHeader + "URL:" + core.AnsiReset + "\n" + decryptedEntry[3] + "\n\n") fmt.Print(ansiDirectoryHeader + "URL:" + back.AnsiReset + "\n" + decryptedEntry[3] + "\n\n")
} }
case 4: case 4:
// print the notes header // print the notes header
fmt.Println(ansiDirectoryHeader + "Notes:" + core.AnsiReset) fmt.Println(ansiDirectoryHeader + "Notes:" + back.AnsiReset)
// combine remaining fields into a single string (to support Markdown rendering) // combine remaining fields into a single string (to support Markdown rendering)
var noteLines []string var noteLines []string
@@ -62,7 +64,7 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
} }
if syncEnabled { if syncEnabled {
RunJobWrapper(false) sync.RunJob(false, false)
} }
os.Exit(0) os.Exit(0)
@@ -70,7 +72,7 @@ func EntryReader(decryptedEntry []string, hideSecrets, syncEnabled bool) {
// EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader. // EntryReaderDecrypt is a wrapper for EntryReader that first decrypts an RCW-wrapped file before sending it to EntryReader.
func EntryReaderDecrypt(targetLocation string, hideSecrets bool) { func EntryReaderDecrypt(targetLocation string, hideSecrets bool) {
if isFile, _ := core.TargetIsFile(targetLocation, true, 2); isFile { if isFile, _ := back.TargetIsFile(targetLocation, true, 2); isFile {
EntryReader(core.DecryptFileToSlice(targetLocation), hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified EntryReader(core.DecryptFileToSlice(targetLocation), hideSecrets, false) // never sync if decrypting straight to EntryReader, as this means the entry could not have been modified
} }
// do not exit, as this is the job of EntryReader // do not exit, as this is the job of EntryReader
+16 -14
View File
@@ -7,6 +7,8 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/sync" "github.com/rwinkhart/libmutton/sync"
) )
@@ -14,30 +16,30 @@ import (
// TempInitCli initializes the MUTN environment based on user input. // TempInitCli initializes the MUTN environment based on user input.
func TempInitCli() { func TempInitCli() {
// text editor // text editor
textEditor := cmp.Or(input("Text editor (leave blank for $EDITOR, falls back to \""+fallbackEditor+"\"):"), os.Getenv("EDITOR"), fallbackEditor) textEditor := cmp.Or(front.Input("Text editor (leave blank for $EDITOR, falls back to \""+fallbackEditor+"\"):"), os.Getenv("EDITOR"), fallbackEditor)
// SSH info // SSH info
configSSH := inputBinary("Configure SSH settings (for synchronization)?") configSSH := front.InputBinary("Configure SSH settings (for synchronization)?")
if configSSH { if configSSH {
// necessary SSH info // necessary SSH info
fmt.Println(AnsiBold + "\nNote:" + core.AnsiReset + " Only key-based authentication is supported (keys may optionally be passphrase-protected).\nThe remote server must already be in your ~" + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts file.") fmt.Println(AnsiBold + "\nNote:" + back.AnsiReset + " Only key-based authentication is supported (keys may optionally be passphrase-protected).\nThe remote server must already be in your ~" + core.PathSeparator + ".ssh" + core.PathSeparator + "known_hosts file.")
sshUser := input("Remote SSH username:") sshUser := front.Input("Remote SSH username:")
sshPort := input("Remote SSH port:") sshPort := front.Input("Remote SSH port:")
sshIP := input("Remote SSH IP/domain:") sshIP := front.Input("Remote SSH IP/domain:")
// prompt for and ensure existence of SSH identity file // prompt for and ensure existence of SSH identity file
var sshKey string var sshKey string
var sshKeyIsFile bool var sshKeyIsFile bool
for !sshKeyIsFile { for !sshKeyIsFile {
fallbackSSHKey := core.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "id_ed25519" fallbackSSHKey := back.Home + core.PathSeparator + ".ssh" + core.PathSeparator + "id_ed25519"
sshKey = cmp.Or(core.ExpandPathWithHome(input("SSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey) sshKey = cmp.Or(back.ExpandPathWithHome(front.Input("SSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey)
sshKeyIsFile, _ = core.TargetIsFile(sshKey, false, 0) sshKeyIsFile, _ = back.TargetIsFile(sshKey, false, 0)
if !sshKeyIsFile { if !sshKeyIsFile {
fmt.Println(core.AnsiError+"SSH identity file not found:", sshKey+core.AnsiReset) // do not exit after error (allow user to retry) fmt.Println(back.AnsiError+"SSH identity file not found:", sshKey+back.AnsiReset) // do not exit after error (allow user to retry)
} }
} }
sshKeyProtected := inputBinary("Is the identity file password-protected?") sshKeyProtected := front.InputBinary("Is the identity file password-protected?")
// initialize libmutton directories // initialize libmutton directories
oldDeviceID := core.DirInit(false) oldDeviceID := core.DirInit(false)
@@ -60,10 +62,10 @@ func TempInitCli() {
// RCW sanity check file // RCW sanity check file
var passphraseInput1 []byte var passphraseInput1 []byte
for { for {
passphraseInput1 = inputHidden("New master (RCW) passphrase:") passphraseInput1 = front.InputHidden("New master (RCW) passphrase:")
passphraseInput2 := inputHidden("Confirm RCW passphrase:") passphraseInput2 := front.InputHidden("Confirm RCW passphrase:")
if !bytes.Equal(passphraseInput1, passphraseInput2) { if !bytes.Equal(passphraseInput1, passphraseInput2) {
fmt.Println(core.AnsiError + "Passphrases do not match" + core.AnsiReset) fmt.Println(back.AnsiError + "Passphrases do not match" + back.AnsiReset)
continue continue
} }
core.RCWSanityCheckGen(passphraseInput1) core.RCWSanityCheckGen(passphraseInput1)
+32 -31
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
) )
@@ -14,13 +15,13 @@ const (
) )
func HelpMain() { func HelpMain() {
fmt.Print(AnsiBold + "\nMUTN | Copyright (c) 2024-2025 Randall Winkhart\n" + core.AnsiReset + ` fmt.Print(AnsiBold + "\nMUTN | Copyright (c) 2024-2025 Randall Winkhart\n" + back.AnsiReset + `
This software exists under the MIT license; you may redistribute it under certain conditions. This software exists under the MIT license; you may redistribute it under certain conditions.
This program comes with absolutely no warranty; type "mutn version" for details. This program comes with absolutely no warranty; type "mutn version" for details.
` + AnsiBold + "Usage:" + core.AnsiReset + ` mutn [/<entry name> [argument] [option]] | [argument] ` + AnsiBold + "Usage:" + back.AnsiReset + ` mutn [/<entry name> [argument] [option]] | [argument]
` + AnsiBold + "Arguments:" + core.AnsiReset + ` ` + AnsiBold + "Arguments:" + back.AnsiReset + `
help Bring up this menu help Bring up this menu
version Display version and license information version Display version and license information
init Set up MUTN (generates libmutton.ini) init Set up MUTN (generates libmutton.ini)
@@ -31,7 +32,7 @@ This program comes with absolutely no warranty; type "mutn version" for details.
shear Delete an existing entry shear Delete an existing entry
sync Manually sync the entry directory sync Manually sync the entry directory
` + AnsiBold + "Options:" + core.AnsiReset + ` ` + AnsiBold + "Options:" + back.AnsiReset + `
copy: copy:
password|-pw|<blank> Copy the password in an entry to your clipboard password|-pw|<blank> Copy the password in an entry to your clipboard
username|-u Copy the username in an entry to your clipboard username|-u Copy the username in an entry to your clipboard
@@ -52,17 +53,17 @@ This program comes with absolutely no warranty; type "mutn version" for details.
note|-n Add a note entry note|-n Add a note entry
folder|-f Add a new folder for entries folder|-f Add a new folder for entries
` + AnsiBold + "Tip 1:" + core.AnsiReset + ` You can quickly read an entry with "mutn /<entry name>" ` + AnsiBold + "Tip 1:" + back.AnsiReset + ` You can quickly read an entry with "mutn /<entry name>"
` + AnsiBold + "Tip 2:" + core.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries ` + AnsiBold + "Tip 2:" + back.AnsiReset + ` Type "mutn" (no arguments/options) to view a list of saved entries
` + AnsiBold + "Tip 3:" + core.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help ` + AnsiBold + "Tip 3:" + back.AnsiReset + ` Provide "add", "edit", "copy", or "gen" as the only argument to receive more specific help
` + AnsiBold + "Tip 4:" + core.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n") ` + AnsiBold + "Tip 4:" + back.AnsiReset + " Using \"add\", \"edit\", or \"copy\" without specifying an option (field) will default to \"password\"\n\n")
os.Exit(0) os.Exit(0)
} }
func HelpAdd() { func HelpAdd() {
fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn /<entry name> add <option> fmt.Print(AnsiBold + "\nUsage:" + back.AnsiReset + ` mutn /<entry name> add <option>
` + AnsiBold + "Options:" + core.AnsiReset + ` ` + AnsiBold + "Options:" + back.AnsiReset + `
add: add:
password|-pw|<blank> Add a password entry password|-pw|<blank> Add a password entry
note|-n Add a note entry note|-n Add a note entry
@@ -71,9 +72,9 @@ func HelpAdd() {
} }
func HelpEdit() { func HelpEdit() {
fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn /<entry name> edit <option> fmt.Print(AnsiBold + "\nUsage:" + back.AnsiReset + ` mutn /<entry name> edit <option>
` + AnsiBold + "Options:" + core.AnsiReset + ` ` + AnsiBold + "Options:" + back.AnsiReset + `
edit: edit:
password|-pw|<blank> Change the password in an entry password|-pw|<blank> Change the password in an entry
username|-u Change the username in an entry username|-u Change the username in an entry
@@ -85,9 +86,9 @@ func HelpEdit() {
} }
func HelpCopy() { func HelpCopy() {
fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn /<entry name> copy <option> fmt.Print(AnsiBold + "\nUsage:" + back.AnsiReset + ` mutn /<entry name> copy <option>
` + AnsiBold + "Options:" + core.AnsiReset + ` ` + AnsiBold + "Options:" + back.AnsiReset + `
copy: copy:
password|-pw|<blank> Copy the password in an entry to your clipboard password|-pw|<blank> Copy the password in an entry to your clipboard
username|-u Copy the username in an entry to your clipboard username|-u Copy the username in an entry to your clipboard
@@ -98,18 +99,18 @@ func HelpCopy() {
} }
func HelpGen() { func HelpGen() {
fmt.Print(AnsiBold + "\nUsage:" + core.AnsiReset + ` mutn /<entry name> gen [option] fmt.Print(AnsiBold + "\nUsage:" + back.AnsiReset + ` mutn /<entry name> gen [option]
` + AnsiBold + "Options:" + core.AnsiReset + ` ` + AnsiBold + "Options:" + back.AnsiReset + `
gen: gen:
update|-u Generate a password for an existing entry update|-u Generate a password for an existing entry
` + AnsiBold + "Tip:" + core.AnsiReset + " If no options are provided, a new password entry is generated\n\n") ` + AnsiBold + "Tip:" + back.AnsiReset + " If no options are provided, a new password entry is generated\n\n")
os.Exit(0) os.Exit(0)
} }
func MITLicense() { func MITLicense() {
fmt.Print(AnsiBold + "\n MIT License" + core.AnsiReset + ` fmt.Print(AnsiBold + "\n MIT License" + back.AnsiReset + `
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated
@@ -141,20 +142,20 @@ func Version() {
MITLicense() MITLicense()
fmt.Print("\n\n MUTN is a simple, self-hosted,\n SSH-synchronized password manager based on libmutton\n\n" + fmt.Print("\n\n MUTN is a simple, self-hosted,\n SSH-synchronized password manager based on libmutton\n\n" +
" .. ..\n" + " .. ..\n" +
" /()\\''.''. " + ansiVersionMeat + "♥♥♥♥" + core.AnsiReset + " .''.''/()\\ _)\n" + " /()\\''.''. " + ansiVersionMeat + "♥♥♥♥" + back.AnsiReset + " .''.''/()\\ _)\n" +
" _. : * " + ansiVersionMeat + "♥♥♥♥♥♥ ♥♥♥♥♥♥♥♥" + core.AnsiReset + " * : <[◎]|_|=\n" + " _. : * " + ansiVersionMeat + "♥♥♥♥♥♥ ♥♥♥♥♥♥♥♥" + back.AnsiReset + " * : <[◎]|_|=\n" +
" }-}-*] `..'..' " + ansiVersionMeat + "♥♥♥♥♥♥♥♥♥♥♥♥♥" + core.AnsiReset + " `..'..' |\n" + " }-}-*] `..'..' " + ansiVersionMeat + "♥♥♥♥♥♥♥♥♥♥♥♥♥" + back.AnsiReset + " `..'..' |\n" +
" ◎-◎ // \\\\ " + ansiVersionMeat + "♥♥♥♥♥♥♥♥♥" + core.AnsiReset + " // \\\\ /|\\\n" + " ◎-◎ // \\\\ " + ansiVersionMeat + "♥♥♥♥♥♥♥♥♥" + back.AnsiReset + " // \\\\ /|\\\n" +
ansiVersionOutline + "<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" + ansiVersionOutline + "<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" +
"\\" + ansiBlackOnWhite + " " + core.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " MUTN v" + MUTNVersion + " " + core.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " MUTN v" + MUTNVersion + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " The Tripe Transmission Update " + core.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " The Tripe Transmission Update " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " " + core.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " Built with libmutton v" + core.LibmuttonVersion + " " + core.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " Built with libmutton v" + core.LibmuttonVersion + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " " + core.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " Copyright (c) 2024-2025: Randall Winkhart " + core.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " Copyright (c) 2024-2025: Randall Winkhart " + back.AnsiReset + ansiVersionOutline + "/\n" +
"\\" + ansiBlackOnWhite + " " + core.AnsiReset + ansiVersionOutline + "/\n" + "\\" + ansiBlackOnWhite + " " + back.AnsiReset + ansiVersionOutline + "/\n" +
"<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" + core.AnsiReset + "<><><><><><><><><><><><><><>-<><><><><><><><><><><><><><>\n" + back.AnsiReset +
"\n For more information, see:\n\n" + "\n For more information, see:\n\n" +
" https://github.com/rwinkhart/MUTN\n" + " https://github.com/rwinkhart/MUTN\n" +
" https://github.com/rwinkhart/libmutton\n\n") " https://github.com/rwinkhart/libmutton\n\n")
+6 -67
View File
@@ -1,74 +1,19 @@
package cli package cli
import ( import (
"bufio"
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/go-boilerplate/front"
"github.com/rwinkhart/libmutton/core" "github.com/rwinkhart/libmutton/core"
"github.com/rwinkhart/libmutton/sync"
"golang.org/x/term"
) )
// input prompts the user for input and returns the input as a string.
func input(prompt string) string {
fmt.Print("\n" + prompt + " ")
reader := bufio.NewReader(os.Stdin)
userInput, _ := reader.ReadString('\n')
return strings.TrimRight(userInput, "\n\r ") // remove trailing newlines, carriage returns, and spaces
}
// inputHidden prompts the user for input and returns the input as a byte array, hiding the input from the terminal.
func inputHidden(prompt string) []byte {
fmt.Print("\n" + prompt + " ")
byteInput, _ := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
return byteInput
}
// inputInt prompts the user for input and returns the input as an integer.
// A maxValue of 0 will cause the function to return 0, an error - a negative maxValue will disable the maxValue check.
func inputInt(prompt string, maxValue int) int {
if maxValue == 0 {
return 0
}
// loop until a valid integer is entered
for {
fmt.Print("\n" + prompt + " ")
var userInput int
_, err := fmt.Scanln(&userInput)
if err == nil && userInput > 0 && (userInput <= maxValue || maxValue < 0) {
return userInput
}
}
}
// inputBinary prompts the user with a yes/no question and returns the response as a boolean.
func inputBinary(prompt string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Print("\n" + prompt + " (y/N) ")
char, _, _ := reader.ReadRune()
if char == 'y' || char == 'Y' {
return true
}
return false
}
// inputMenuGen prompts the user with a menu and returns the user's choice as an integer.
func inputMenuGen(prompt string, options []string) int {
for i, option := range options {
fmt.Printf("%d. %s\n", i+1, option)
}
return inputInt(prompt, len(options))
}
// inputPasswordGen prompts the user for password generation parameters and returns a generated password as a string. // inputPasswordGen prompts the user for password generation parameters and returns a generated password as a string.
func inputPasswordGen() string { func inputPasswordGen() string {
passLength := inputInt("Password length:", -1) passLength := front.InputPositiveInt("Password length:", 1, -1)
fmt.Println() fmt.Println()
passCharset := uint8(inputMenuGen("Password complexity:", []string{"Simple", "Complex", "Ultra Complex (not compatible with many services)"})) passCharset := uint8(front.InputMenuGen("Password complexity:", []string{"Simple", "Complex", "Ultra Complex (not compatible with many services)"}))
var complexity float64 var complexity float64
switch passCharset { switch passCharset {
case 1: case 1:
@@ -87,15 +32,9 @@ func writeEntryCLI(targetLocation string, unencryptedEntry []string, hideSecrets
// write the entry to the target location // write the entry to the target location
core.WriteEntry(targetLocation, []byte(strings.Join(unencryptedEntry, "\n"))) core.WriteEntry(targetLocation, []byte(strings.Join(unencryptedEntry, "\n")))
// preview the entry // preview the entry
fmt.Println(AnsiBold + "\nEntry Preview:" + core.AnsiReset) fmt.Println(AnsiBold + "\nEntry Preview:" + back.AnsiReset)
EntryReader(unencryptedEntry, hideSecrets, true) EntryReader(unencryptedEntry, hideSecrets, true)
} else { } else {
core.PrintError("No data supplied for entry", core.ErrorTargetNotFound, true) back.PrintError("No data supplied for entry", back.ErrorTargetNotFound, true)
} }
} }
// RunJobWrapper is a wrapper for sync.RunJob that sets the passphrase input function to inputHidden.
func RunJobWrapper(manualSync bool) {
core.PassphraseInputFunction = inputHidden
sync.RunJob(manualSync, false)
}