Add multi-depository support with global config

- Implement global CLI config at ~/.config/jade/config.yml
- Add jade depo commands (add, list, remove, set-default)
- Support depository short names and context-aware detection
- Remove tag_prefix config, hardcode + syntax for consistency
- Update depository resolution: flag -> context -> default
- Auto-initialize .jade/ directory structure when adding depos
- Update documentation with new multi-depository workflow
This commit is contained in:
2026-01-03 16:25:23 +01:00
parent 0ebfaf835d
commit 1d87d93172
9 changed files with 515 additions and 158 deletions
+128
View File
@@ -0,0 +1,128 @@
package cmd
import (
"fmt"
"os"
"path/filepath"
"git.jnss.me/joakim/jadedepo/internal/engine"
"github.com/spf13/cobra"
)
var depoCmd = &cobra.Command{
Use: "depo",
Short: "Manage depositories",
}
var depoAddCmd = &cobra.Command{
Use: "add <name> [path]",
Short: "Add a depository",
Long: "Register a new depository with a short name. If path is not provided, uses current working directory.",
Args: cobra.RangeArgs(1, 2),
Run: depoAdd,
}
var depoListCmd = &cobra.Command{
Use: "list",
Short: "List all depositories",
Run: depoList,
}
var depoRemoveCmd = &cobra.Command{
Use: "remove <name>",
Short: "Remove a depository",
Args: cobra.ExactArgs(1),
Run: depoRemove,
}
var depoSetDefaultCmd = &cobra.Command{
Use: "set-default <name>",
Short: "Set the default depository",
Args: cobra.ExactArgs(1),
Run: depoSetDefault,
}
func init() {
rootCmd.AddCommand(depoCmd)
depoCmd.AddCommand(depoAddCmd)
depoCmd.AddCommand(depoListCmd)
depoCmd.AddCommand(depoRemoveCmd)
depoCmd.AddCommand(depoSetDefaultCmd)
}
func depoAdd(cmd *cobra.Command, args []string) {
name := args[0]
var path string
if len(args) == 2 {
path = args[1]
} else {
// Use current working directory
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting current directory: %v\n", err)
os.Exit(1)
}
path = cwd
}
// Make path absolute
absPath, err := filepath.Abs(path)
if err != nil {
fmt.Fprintf(os.Stderr, "Error resolving path: %v\n", err)
os.Exit(1)
}
// Add depository
if err := engine.AddDepository(name, absPath); err != nil {
fmt.Fprintf(os.Stderr, "Error adding depository: %v\n", err)
os.Exit(1)
}
fmt.Printf("Added depository '%s' at %s\n", name, absPath)
}
func depoList(cmd *cobra.Command, args []string) {
depos, defaultDepo, err := engine.ListDepositories()
if err != nil {
fmt.Fprintf(os.Stderr, "Error listing depositories: %v\n", err)
os.Exit(1)
}
if len(depos) == 0 {
fmt.Println("No depositories configured")
fmt.Println("Add one with: jade depo add <name> [path]")
return
}
fmt.Println("Depositories:")
for name, path := range depos {
if name == defaultDepo {
fmt.Printf(" * %s -> %s (default)\n", name, path)
} else {
fmt.Printf(" %s -> %s\n", name, path)
}
}
}
func depoRemove(cmd *cobra.Command, args []string) {
name := args[0]
if err := engine.RemoveDepository(name); err != nil {
fmt.Fprintf(os.Stderr, "Error removing depository: %v\n", err)
os.Exit(1)
}
fmt.Printf("Removed depository '%s'\n", name)
}
func depoSetDefault(cmd *cobra.Command, args []string) {
name := args[0]
if err := engine.SetDefaultDepository(name); err != nil {
fmt.Fprintf(os.Stderr, "Error setting default depository: %v\n", err)
os.Exit(1)
}
fmt.Printf("Set '%s' as default depository\n", name)
}
+31 -15
View File
@@ -12,7 +12,8 @@ import (
)
var (
forceDelete bool
forceDelete bool
permanentDelete bool
rmCmd = &cobra.Command{
Use: "rm [title]",
@@ -24,6 +25,7 @@ var (
func init() {
rmCmd.Flags().BoolVarP(&forceDelete, "force", "f", false, "Skip confirmation prompt")
rmCmd.Flags().BoolVarP(&permanentDelete, "permanent", "p", false, "Delete permanently")
rootCmd.AddCommand(rmCmd)
}
@@ -68,9 +70,15 @@ func removeNote(cmd *cobra.Command, args []string) {
noteToDelete = matches[selection-1]
}
notePath := filepath.Join(jd.Config.DepoPath, noteToDelete.Path)
// Confirm deletion
if !forceDelete {
fmt.Printf("Are you sure you want to delete '%s'? (y/N): ", noteToDelete.Title)
if permanentDelete {
fmt.Printf("Are you sure you want to PERMANENTLY delete '%s'? This cannot be undone. (y/N): ", noteToDelete.Title)
} else {
fmt.Printf("Are you sure you want to move '%s' to trash? (y/N): ", noteToDelete.Title)
}
var confirm string
fmt.Scanln(&confirm)
if strings.ToLower(confirm) != "y" && strings.ToLower(confirm) != "yes" {
@@ -79,20 +87,28 @@ func removeNote(cmd *cobra.Command, args []string) {
}
}
// Move to trash
notePath := filepath.Join(jd.Config.DepoPath, noteToDelete.Path)
trashPath := jd.GetTrashPath()
if permanentDelete {
// Permanently delete the file
if err := os.Remove(notePath); err != nil {
fmt.Fprintf(os.Stderr, "Error deleting note: %v\n", err)
os.Exit(1)
}
fmt.Printf("Permanently deleted '%s'\n", noteToDelete.Title)
} else {
// Move to trash
trashPath := jd.GetTrashPath()
// Create unique filename in trash (add timestamp to avoid conflicts)
timestamp := time.Now().Format("20060102-150405")
trashFilename := fmt.Sprintf("%s-%s", timestamp, filepath.Base(noteToDelete.Path))
trashDestination := filepath.Join(trashPath, trashFilename)
// Create unique filename in trash (add timestamp to avoid conflicts)
timestamp := time.Now().Format("20060102-150405")
trashFilename := fmt.Sprintf("%s-%s", timestamp, filepath.Base(noteToDelete.Path))
trashDestination := filepath.Join(trashPath, trashFilename)
if err := os.Rename(notePath, trashDestination); err != nil {
fmt.Fprintf(os.Stderr, "Error moving note to trash: %v\n", err)
os.Exit(1)
if err := os.Rename(notePath, trashDestination); err != nil {
fmt.Fprintf(os.Stderr, "Error moving note to trash: %v\n", err)
os.Exit(1)
}
fmt.Printf("Moved '%s' to trash\n", noteToDelete.Title)
fmt.Printf("Trash location: %s\n", trashDestination)
}
fmt.Printf("Moved '%s' to trash\n", noteToDelete.Title)
fmt.Printf("Trash location: %s\n", trashDestination)
}
+16 -10
View File
@@ -17,21 +17,27 @@ var (
)
var (
cfgPath string
depoPath string
depoInput string
)
func init() {
cobra.OnInitialize(func() {
if _, err := engine.Init(cfgPath, depoPath); err != nil {
fmt.Fprintf(os.Stderr, "Error initializing configuration: %v\n", err)
os.Exit(1)
}
})
cobra.OnInitialize(initializeIfNeeded)
rootCmd.PersistentFlags().StringVar(&depoInput, "depo", "", "Depository name or path")
}
rootCmd.PersistentFlags().StringVar(&cfgPath, "config", "", "Configuration file path. Default <depo>/.jade/config.yml")
rootCmd.PersistentFlags().StringVar(&depoPath, "depo", "", "Depository path. Default $HOME/jade-depository")
// initializeIfNeeded initializes the depository only for commands that need it
func initializeIfNeeded() {
// Skip initialization for depo management commands
cmd := os.Args
if len(cmd) > 1 && cmd[1] == "depo" {
return
}
// Initialize for all other commands
if _, err := engine.Init(depoInput); err != nil {
fmt.Fprintf(os.Stderr, "Error initializing configuration: %v\n", err)
os.Exit(1)
}
}
func openDepository(cmd *cobra.Command, args []string) {
+10 -23
View File
@@ -10,8 +10,6 @@ import (
)
var (
searchTag string
searchCmd = &cobra.Command{
Use: "search [query]",
Short: "Search notes by content or tags",
@@ -20,7 +18,6 @@ var (
)
func init() {
searchCmd.Flags().StringVarP(&searchTag, "tag", "t", "", "Search by tag")
rootCmd.AddCommand(searchCmd)
}
@@ -33,25 +30,15 @@ func searchNotes(cmd *cobra.Command, args []string) {
var results []string
// Search by tag if flag is provided
if searchTag != "" {
results, err = jd.SearchByTag(searchTag)
if err != nil {
fmt.Fprintf(os.Stderr, "Error searching by tag: %v\n", err)
os.Exit(1)
}
} else {
// Search by content
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Error: query required when not searching by tag\n")
os.Exit(1)
}
query := strings.Join(args, " ")
results, err = jd.SearchContent(query)
if err != nil {
fmt.Fprintf(os.Stderr, "Error searching: %v\n", err)
os.Exit(1)
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Error: query required when not searching by tag\n")
os.Exit(1)
}
query := strings.Join(args, " ")
results, err = jd.SearchContent(query)
if err != nil {
fmt.Fprintf(os.Stderr, "Error searching: %v\n", err)
os.Exit(1)
}
if len(results) == 0 {
@@ -62,7 +49,7 @@ func searchNotes(cmd *cobra.Command, args []string) {
fmt.Printf("Found %d match(es):\n\n", len(results))
for _, path := range results {
// Load note to get title
note, err := engine.LoadNote(jd.Config.DepoPath, path, jd.Config.TagPrefix)
note, err := engine.LoadNote(jd.Config.DepoPath, path)
if err != nil {
fmt.Printf(" %s\n", path)
continue