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:
+60
-19
@@ -4,12 +4,13 @@ A simple CLI tool for managing markdown notes in a depository (vault). Inspired
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- **Multi-Depository Support**: Manage multiple note repositories with short names
|
||||||
- **Note Management**: Create, edit, list, and delete markdown notes
|
- **Note Management**: Create, edit, list, and delete markdown notes
|
||||||
- **Tag Support**: Extract and search notes by tags (default `+tag` syntax, configurable)
|
- **Tag Support**: Extract and search notes by tags (`+tag` syntax)
|
||||||
- **Full-Text Search**: Search note content using ripgrep
|
- **Full-Text Search**: Search note content using ripgrep
|
||||||
- **Link Support**: Parse both `[[wiki-style]]` and `[markdown](links)`
|
- **Link Support**: Parse both `[[wiki-style]]` and `[markdown](links)`
|
||||||
- **Trash System**: Deleted notes moved to `.jade/trash/` instead of permanent deletion
|
- **Trash System**: Deleted notes moved to `.jade/trash/` instead of permanent deletion
|
||||||
- **Flexible Configuration**: Per-depository settings for tag prefix and other options
|
- **Context-Aware**: Automatically detects which depository you're in
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -19,24 +20,53 @@ go build -o jade
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### Managing Depositories
|
||||||
|
|
||||||
|
Before you can use jade, you need to add at least one depository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add a depository with a short name
|
||||||
|
jade depo add personal ~/my-notes
|
||||||
|
|
||||||
|
# Add depository using current directory
|
||||||
|
cd ~/work/meeting-notes
|
||||||
|
jade depo add work
|
||||||
|
|
||||||
|
# List all depositories
|
||||||
|
jade depo list
|
||||||
|
|
||||||
|
# Set default depository
|
||||||
|
jade depo set-default personal
|
||||||
|
|
||||||
|
# Remove a depository (doesn't delete files)
|
||||||
|
jade depo remove work
|
||||||
|
```
|
||||||
|
|
||||||
### Open Depository
|
### Open Depository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Open the depository directory in your $EDITOR
|
# Open the default depository in your $EDITOR
|
||||||
jade
|
jade
|
||||||
|
|
||||||
# This opens ~/jade-depository (or your configured path) in your editor
|
# Open a specific depository
|
||||||
# Works great with neovim, vim, or any editor that supports directory opening
|
jade --depo work
|
||||||
```
|
```
|
||||||
|
|
||||||
### Initialize and List Notes
|
### List Notes
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List all notes in the default depository (~/jade-depository)
|
# List all notes in the default depository
|
||||||
jade list
|
jade list
|
||||||
|
|
||||||
# Use a specific depository
|
# Use a specific depository by name
|
||||||
|
jade --depo work list
|
||||||
|
|
||||||
|
# Or by path
|
||||||
jade --depo /path/to/notes list
|
jade --depo /path/to/notes list
|
||||||
|
|
||||||
|
# Context-aware: if you're inside a registered depository, jade uses it automatically
|
||||||
|
cd ~/my-notes
|
||||||
|
jade list # Uses the depository that contains current directory
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create a Note
|
### Create a Note
|
||||||
@@ -87,24 +117,33 @@ jade tags
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Configuration is stored in `<depo_path>/.jade/config.yml` and is created automatically when you first run jade.
|
Jade uses a two-level configuration system:
|
||||||
|
|
||||||
|
### Global CLI Config (`~/.config/jade/config.yml`)
|
||||||
|
|
||||||
|
Manages your depositories and default settings:
|
||||||
|
|
||||||
Example config:
|
|
||||||
```yaml
|
```yaml
|
||||||
depo_path: /home/user/jade-depository
|
depositories:
|
||||||
tag_prefix: "+"
|
personal: /home/user/my-notes
|
||||||
|
work: /home/user/work/notes
|
||||||
|
default_depository: personal
|
||||||
```
|
```
|
||||||
|
|
||||||
You can override these with flags:
|
This file is automatically created and managed by `jade depo` commands.
|
||||||
- `--depo`: Depository path
|
|
||||||
- `--config`: Config file path (if you need to override the default location)
|
### Per-Depository Structure
|
||||||
|
|
||||||
|
Each depository contains a `.jade/` directory for metadata:
|
||||||
|
|
||||||
|
- `.jade/trash/` - Deleted notes
|
||||||
|
- Future: `.jade/index.db` - SQLite index for fast search
|
||||||
|
|
||||||
## Depository Structure
|
## Depository Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
jade-depository/
|
my-notes/
|
||||||
├── .jade/
|
├── .jade/
|
||||||
│ ├── config.yml # Configuration file
|
|
||||||
│ └── trash/ # Deleted notes go here
|
│ └── trash/ # Deleted notes go here
|
||||||
├── note-1.md
|
├── note-1.md
|
||||||
├── note-2.md
|
├── note-2.md
|
||||||
@@ -112,7 +151,7 @@ jade-depository/
|
|||||||
└── note-3.md
|
└── note-3.md
|
||||||
```
|
```
|
||||||
|
|
||||||
The `.jade/` directory is automatically created and contains configuration and metadata. You can optionally add it to `.gitignore` if you don't want to track it in version control.
|
The `.jade/` directory is automatically created and contains metadata. You can optionally add it to `.gitignore` if you don't want to track it in version control, or commit it to sync trash/metadata across devices.
|
||||||
|
|
||||||
## Note Format
|
## Note Format
|
||||||
|
|
||||||
@@ -139,9 +178,11 @@ Tags can appear anywhere in +content.
|
|||||||
|
|
||||||
### v1.0 (Current)
|
### v1.0 (Current)
|
||||||
- [x] Basic note operations (add, edit, delete, list)
|
- [x] Basic note operations (add, edit, delete, list)
|
||||||
- [x] Tag extraction and listing
|
- [x] Tag extraction and listing (`+tag` syntax)
|
||||||
- [x] Content and tag search
|
- [x] Content and tag search
|
||||||
- [x] Trash system
|
- [x] Trash system
|
||||||
|
- [x] Multi-depository support with short names
|
||||||
|
- [x] Context-aware depository detection
|
||||||
|
|
||||||
### v1.1 (Future)
|
### v1.1 (Future)
|
||||||
- [ ] SQLite indexing for faster search
|
- [ ] SQLite indexing for faster search
|
||||||
|
|||||||
@@ -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
@@ -12,7 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
forceDelete bool
|
forceDelete bool
|
||||||
|
permanentDelete bool
|
||||||
|
|
||||||
rmCmd = &cobra.Command{
|
rmCmd = &cobra.Command{
|
||||||
Use: "rm [title]",
|
Use: "rm [title]",
|
||||||
@@ -24,6 +25,7 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rmCmd.Flags().BoolVarP(&forceDelete, "force", "f", false, "Skip confirmation prompt")
|
rmCmd.Flags().BoolVarP(&forceDelete, "force", "f", false, "Skip confirmation prompt")
|
||||||
|
rmCmd.Flags().BoolVarP(&permanentDelete, "permanent", "p", false, "Delete permanently")
|
||||||
rootCmd.AddCommand(rmCmd)
|
rootCmd.AddCommand(rmCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,9 +70,15 @@ func removeNote(cmd *cobra.Command, args []string) {
|
|||||||
noteToDelete = matches[selection-1]
|
noteToDelete = matches[selection-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notePath := filepath.Join(jd.Config.DepoPath, noteToDelete.Path)
|
||||||
|
|
||||||
// Confirm deletion
|
// Confirm deletion
|
||||||
if !forceDelete {
|
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
|
var confirm string
|
||||||
fmt.Scanln(&confirm)
|
fmt.Scanln(&confirm)
|
||||||
if strings.ToLower(confirm) != "y" && strings.ToLower(confirm) != "yes" {
|
if strings.ToLower(confirm) != "y" && strings.ToLower(confirm) != "yes" {
|
||||||
@@ -79,20 +87,28 @@ func removeNote(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move to trash
|
if permanentDelete {
|
||||||
notePath := filepath.Join(jd.Config.DepoPath, noteToDelete.Path)
|
// Permanently delete the file
|
||||||
trashPath := jd.GetTrashPath()
|
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)
|
// Create unique filename in trash (add timestamp to avoid conflicts)
|
||||||
timestamp := time.Now().Format("20060102-150405")
|
timestamp := time.Now().Format("20060102-150405")
|
||||||
trashFilename := fmt.Sprintf("%s-%s", timestamp, filepath.Base(noteToDelete.Path))
|
trashFilename := fmt.Sprintf("%s-%s", timestamp, filepath.Base(noteToDelete.Path))
|
||||||
trashDestination := filepath.Join(trashPath, trashFilename)
|
trashDestination := filepath.Join(trashPath, trashFilename)
|
||||||
|
|
||||||
if err := os.Rename(notePath, trashDestination); err != nil {
|
if err := os.Rename(notePath, trashDestination); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error moving note to trash: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error moving note to trash: %v\n", err)
|
||||||
os.Exit(1)
|
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
@@ -17,21 +17,27 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfgPath string
|
depoInput string
|
||||||
depoPath string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(func() {
|
cobra.OnInitialize(initializeIfNeeded)
|
||||||
if _, err := engine.Init(cfgPath, depoPath); err != nil {
|
rootCmd.PersistentFlags().StringVar(&depoInput, "depo", "", "Depository name or path")
|
||||||
fmt.Fprintf(os.Stderr, "Error initializing configuration: %v\n", err)
|
}
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVar(&cfgPath, "config", "", "Configuration file path. Default <depo>/.jade/config.yml")
|
// initializeIfNeeded initializes the depository only for commands that need it
|
||||||
rootCmd.PersistentFlags().StringVar(&depoPath, "depo", "", "Depository path. Default $HOME/jade-depository")
|
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) {
|
func openDepository(cmd *cobra.Command, args []string) {
|
||||||
|
|||||||
+10
-23
@@ -10,8 +10,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
searchTag string
|
|
||||||
|
|
||||||
searchCmd = &cobra.Command{
|
searchCmd = &cobra.Command{
|
||||||
Use: "search [query]",
|
Use: "search [query]",
|
||||||
Short: "Search notes by content or tags",
|
Short: "Search notes by content or tags",
|
||||||
@@ -20,7 +18,6 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
searchCmd.Flags().StringVarP(&searchTag, "tag", "t", "", "Search by tag")
|
|
||||||
rootCmd.AddCommand(searchCmd)
|
rootCmd.AddCommand(searchCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,25 +30,15 @@ func searchNotes(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
var results []string
|
var results []string
|
||||||
|
|
||||||
// Search by tag if flag is provided
|
if len(args) == 0 {
|
||||||
if searchTag != "" {
|
fmt.Fprintf(os.Stderr, "Error: query required when not searching by tag\n")
|
||||||
results, err = jd.SearchByTag(searchTag)
|
os.Exit(1)
|
||||||
if err != nil {
|
}
|
||||||
fmt.Fprintf(os.Stderr, "Error searching by tag: %v\n", err)
|
query := strings.Join(args, " ")
|
||||||
os.Exit(1)
|
results, err = jd.SearchContent(query)
|
||||||
}
|
if err != nil {
|
||||||
} else {
|
fmt.Fprintf(os.Stderr, "Error searching: %v\n", err)
|
||||||
// Search by content
|
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 {
|
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))
|
fmt.Printf("Found %d match(es):\n\n", len(results))
|
||||||
for _, path := range results {
|
for _, path := range results {
|
||||||
// Load note to get title
|
// 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 {
|
if err != nil {
|
||||||
fmt.Printf(" %s\n", path)
|
fmt.Printf(" %s\n", path)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -8,91 +8,287 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
// GlobalConfig represents the CLI-level configuration
|
||||||
// DepoPath is the path to the jade depository. Default ~/jade-depository
|
type GlobalConfig struct {
|
||||||
DepoPath string `mapstructure:"depo_path"`
|
// Depositories maps depository names to their filesystem paths
|
||||||
// TagPrefix is the prefix used for tags in notes. Default "+"
|
Depositories map[string]string `mapstructure:"depositories"`
|
||||||
TagPrefix string `mapstructure:"tag_prefix"`
|
// DefaultDepository is the name of the default depository to use
|
||||||
|
DefaultDepository string `mapstructure:"default_depository"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDefaultDepoPath returns the default depository path
|
// Config represents the runtime configuration for a specific depository
|
||||||
func getDefaultDepoPath() string {
|
type Config struct {
|
||||||
|
// DepoPath is the path to the active jade depository
|
||||||
|
DepoPath string
|
||||||
|
// DepoName is the name of the active depository (if registered)
|
||||||
|
DepoName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGlobalConfigPath returns the path to the global CLI config
|
||||||
|
func getGlobalConfigPath() string {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "./jade-depository"
|
return "./.config/jade/config.yml"
|
||||||
}
|
}
|
||||||
return filepath.Join(home, "jade-depository")
|
return filepath.Join(home, ".config", "jade", "config.yml")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getConfigPath returns the config file path within the depository
|
// loadGlobalConfig loads the global CLI configuration
|
||||||
func getConfigPath(depoPath string) string {
|
func loadGlobalConfig() (*GlobalConfig, error) {
|
||||||
return filepath.Join(depoPath, ".jade", "config.yml")
|
configPath := getGlobalConfigPath()
|
||||||
}
|
configDir := filepath.Dir(configPath)
|
||||||
|
|
||||||
// initConfig initializes the configuration using Viper
|
// Ensure config directory exists
|
||||||
// Precedence order: flags > environment variables > config file > defaults
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
func initConfig(cfgPath, depoPath string) (*Config, error) {
|
return nil, fmt.Errorf("failed to create config directory: %w", err)
|
||||||
// Set up environment variable support
|
|
||||||
viper.SetEnvPrefix("JADE")
|
|
||||||
viper.AutomaticEnv()
|
|
||||||
|
|
||||||
// Determine depository path (from flag, env var, or default)
|
|
||||||
actualDepoPath := depoPath
|
|
||||||
if actualDepoPath == "" {
|
|
||||||
actualDepoPath = viper.GetString("depo_path")
|
|
||||||
if actualDepoPath == "" {
|
|
||||||
actualDepoPath = getDefaultDepoPath()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new viper instance for global config
|
||||||
|
v := viper.New()
|
||||||
|
v.SetConfigFile(configPath)
|
||||||
|
v.SetConfigType("yaml")
|
||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
viper.SetDefault("depo_path", actualDepoPath)
|
v.SetDefault("depositories", make(map[string]string))
|
||||||
viper.SetDefault("tag_prefix", "+")
|
v.SetDefault("default_depository", "")
|
||||||
|
|
||||||
// Ensure depository and .jade directory exist
|
// Try to read existing config
|
||||||
jadeDir := filepath.Join(actualDepoPath, ".jade")
|
if err := v.ReadInConfig(); err != nil {
|
||||||
if err := os.MkdirAll(jadeDir, 0755); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create .jade directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine config file location
|
|
||||||
configFilePath := cfgPath
|
|
||||||
if configFilePath == "" {
|
|
||||||
configFilePath = getConfigPath(actualDepoPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up config file
|
|
||||||
viper.SetConfigFile(configFilePath)
|
|
||||||
|
|
||||||
// Try to read existing config file
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
|
||||||
// Check if file doesn't exist (handles both viper.ConfigFileNotFoundError and os.IsNotExist)
|
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok || os.IsNotExist(err) {
|
if _, ok := err.(viper.ConfigFileNotFoundError); ok || os.IsNotExist(err) {
|
||||||
// Config file not found, create it with defaults
|
// Config file not found, create it with defaults
|
||||||
if err := viper.SafeWriteConfigAs(configFilePath); err != nil {
|
if err := v.SafeWriteConfigAs(configPath); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create config file: %w", err)
|
return nil, fmt.Errorf("failed to create global config: %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Config file was found but another error occurred
|
return nil, fmt.Errorf("error reading global config: %w", err)
|
||||||
return nil, fmt.Errorf("error reading config file: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override with flags if provided (highest precedence)
|
// Unmarshal into struct
|
||||||
if depoPath != "" {
|
cfg := &GlobalConfig{}
|
||||||
viper.Set("depo_path", depoPath)
|
if err := v.Unmarshal(cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal global config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal config into struct
|
// Initialize map if nil
|
||||||
cfg := &Config{}
|
if cfg.Depositories == nil {
|
||||||
if err := viper.Unmarshal(cfg); err != nil {
|
cfg.Depositories = make(map[string]string)
|
||||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write config back to persist any changes (e.g., from flags or env vars)
|
|
||||||
if err := viper.WriteConfig(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write config: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// saveGlobalConfig saves the global CLI configuration
|
||||||
|
func saveGlobalConfig(cfg *GlobalConfig) error {
|
||||||
|
configPath := getGlobalConfigPath()
|
||||||
|
configDir := filepath.Dir(configPath)
|
||||||
|
|
||||||
|
// Ensure config directory exists
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new viper instance
|
||||||
|
v := viper.New()
|
||||||
|
v.SetConfigFile(configPath)
|
||||||
|
v.SetConfigType("yaml")
|
||||||
|
|
||||||
|
// Set values
|
||||||
|
v.Set("depositories", cfg.Depositories)
|
||||||
|
v.Set("default_depository", cfg.DefaultDepository)
|
||||||
|
|
||||||
|
// Write config
|
||||||
|
if err := v.WriteConfig(); err != nil {
|
||||||
|
// If file doesn't exist, use SafeWriteConfigAs
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if err := v.SafeWriteConfigAs(configPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to write config file: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to write config file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveDepository resolves a depository name or path to an absolute path
|
||||||
|
// Priority: 1) depoInput (name or path), 2) context (PWD), 3) global default
|
||||||
|
func resolveDepository(globalCfg *GlobalConfig, depoInput string) (path string, name string, err error) {
|
||||||
|
// 1. If depoInput is provided, check if it's a name or path
|
||||||
|
if depoInput != "" {
|
||||||
|
// Check if it's a registered name
|
||||||
|
if depoPath, exists := globalCfg.Depositories[depoInput]; exists {
|
||||||
|
return depoPath, depoInput, nil
|
||||||
|
}
|
||||||
|
// Otherwise treat it as a literal path
|
||||||
|
absPath, err := filepath.Abs(depoInput)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("invalid path: %w", err)
|
||||||
|
}
|
||||||
|
// Check if this path is registered under a name
|
||||||
|
for name, path := range globalCfg.Depositories {
|
||||||
|
if path == absPath {
|
||||||
|
return absPath, name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return absPath, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check if current working directory is inside a registered depository
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err == nil {
|
||||||
|
for name, depoPath := range globalCfg.Depositories {
|
||||||
|
absDepoPath, err := filepath.Abs(depoPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check if cwd is inside this depository
|
||||||
|
relPath, err := filepath.Rel(absDepoPath, cwd)
|
||||||
|
if err == nil && !filepath.IsAbs(relPath) && len(relPath) > 0 && relPath[0] != '.' {
|
||||||
|
return absDepoPath, name, nil
|
||||||
|
}
|
||||||
|
// Check if cwd exactly matches the depository
|
||||||
|
if cwd == absDepoPath {
|
||||||
|
return absDepoPath, name, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Use default depository from global config
|
||||||
|
if globalCfg.DefaultDepository != "" {
|
||||||
|
if depoPath, exists := globalCfg.Depositories[globalCfg.DefaultDepository]; exists {
|
||||||
|
return depoPath, globalCfg.DefaultDepository, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No depository found
|
||||||
|
return "", "", fmt.Errorf("no depository configured. Run 'jade depo add <name> [path]' to add one")
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfig initializes the configuration for a specific depository
|
||||||
|
func initConfig(depoInput string) (*Config, error) {
|
||||||
|
// Load global config
|
||||||
|
globalCfg, err := loadGlobalConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load global config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve depository
|
||||||
|
depoPath, depoName, err := resolveDepository(globalCfg, depoInput)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure depository and .jade directory exist
|
||||||
|
jadeDir := filepath.Join(depoPath, ".jade")
|
||||||
|
if err := os.MkdirAll(jadeDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create .jade directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create trash directory
|
||||||
|
trashDir := filepath.Join(jadeDir, "trash")
|
||||||
|
if err := os.MkdirAll(trashDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create trash directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Config{
|
||||||
|
DepoPath: depoPath,
|
||||||
|
DepoName: depoName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddDepository adds a new depository to the global config
|
||||||
|
func AddDepository(name, path string) error {
|
||||||
|
globalCfg, err := loadGlobalConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load global config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if name already exists
|
||||||
|
if _, exists := globalCfg.Depositories[name]; exists {
|
||||||
|
return fmt.Errorf("depository '%s' already exists", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make path absolute
|
||||||
|
absPath, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize depository structure
|
||||||
|
jadeDir := filepath.Join(absPath, ".jade")
|
||||||
|
if err := os.MkdirAll(jadeDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create .jade directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
trashDir := filepath.Join(jadeDir, "trash")
|
||||||
|
if err := os.MkdirAll(trashDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create trash directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to config
|
||||||
|
globalCfg.Depositories[name] = absPath
|
||||||
|
|
||||||
|
// If this is the first depository, make it default
|
||||||
|
if globalCfg.DefaultDepository == "" {
|
||||||
|
globalCfg.DefaultDepository = name
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveGlobalConfig(globalCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDepositories returns all depositories and the default one
|
||||||
|
func ListDepositories() (map[string]string, string, error) {
|
||||||
|
globalCfg, err := loadGlobalConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to load global config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalCfg.Depositories, globalCfg.DefaultDepository, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveDepository removes a depository from the global config
|
||||||
|
func RemoveDepository(name string) error {
|
||||||
|
globalCfg, err := loadGlobalConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load global config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if depository exists
|
||||||
|
if _, exists := globalCfg.Depositories[name]; !exists {
|
||||||
|
return fmt.Errorf("depository '%s' not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from map
|
||||||
|
delete(globalCfg.Depositories, name)
|
||||||
|
|
||||||
|
// If this was the default, clear default
|
||||||
|
if globalCfg.DefaultDepository == name {
|
||||||
|
globalCfg.DefaultDepository = ""
|
||||||
|
// Set first remaining depository as default if any exist
|
||||||
|
for depoName := range globalCfg.Depositories {
|
||||||
|
globalCfg.DefaultDepository = depoName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return saveGlobalConfig(globalCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultDepository sets the default depository
|
||||||
|
func SetDefaultDepository(name string) error {
|
||||||
|
globalCfg, err := loadGlobalConfig()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load global config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if depository exists
|
||||||
|
if _, exists := globalCfg.Depositories[name]; !exists {
|
||||||
|
return fmt.Errorf("depository '%s' not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalCfg.DefaultDepository = name
|
||||||
|
|
||||||
|
return saveGlobalConfig(globalCfg)
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ type JadeDepo struct {
|
|||||||
Config *Config
|
Config *Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init(cfgPath, depoPath string) (*JadeDepo, error) {
|
func Init(depoInput string) (*JadeDepo, error) {
|
||||||
cfg, err := initConfig(cfgPath, depoPath)
|
cfg, err := initConfig(depoInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize config: %w", err)
|
return nil, fmt.Errorf("failed to initialize config: %w", err)
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ func Init(cfgPath, depoPath string) (*JadeDepo, error) {
|
|||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize depository structure
|
// Initialize depository structure (already done in initConfig, but ensure it's complete)
|
||||||
if err := jd.initDepoStructure(); err != nil {
|
if err := jd.initDepoStructure(); err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize depository structure: %w", err)
|
return nil, fmt.Errorf("failed to initialize depository structure: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Note struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadNote reads a note from the filesystem and parses its content
|
// LoadNote reads a note from the filesystem and parses its content
|
||||||
func LoadNote(depoPath, notePath string, tagPrefix string) (*Note, error) {
|
func LoadNote(depoPath, notePath string) (*Note, error) {
|
||||||
fullPath := filepath.Join(depoPath, notePath)
|
fullPath := filepath.Join(depoPath, notePath)
|
||||||
|
|
||||||
// Get file info
|
// Get file info
|
||||||
@@ -48,8 +48,8 @@ func LoadNote(depoPath, notePath string, tagPrefix string) (*Note, error) {
|
|||||||
note.Title = strings.TrimSuffix(filepath.Base(notePath), filepath.Ext(notePath))
|
note.Title = strings.TrimSuffix(filepath.Base(notePath), filepath.Ext(notePath))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse tags
|
// Parse tags (hardcoded to + prefix)
|
||||||
note.Tags = parseTags(string(content), tagPrefix)
|
note.Tags = parseTags(string(content))
|
||||||
|
|
||||||
// Parse links
|
// Parse links
|
||||||
note.Links = parseLinks(string(content))
|
note.Links = parseLinks(string(content))
|
||||||
@@ -69,14 +69,10 @@ func parseTitle(content string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseTags extracts tags from markdown content based on the tag prefix
|
// parseTags extracts tags from markdown content using + prefix
|
||||||
func parseTags(content string, tagPrefix string) []string {
|
func parseTags(content string) []string {
|
||||||
// Escape special regex characters in tagPrefix
|
// Match + followed by word characters
|
||||||
escapedPrefix := regexp.QuoteMeta(tagPrefix)
|
re := regexp.MustCompile(`\+\w+`)
|
||||||
|
|
||||||
// Match tagPrefix followed by word characters
|
|
||||||
pattern := escapedPrefix + `\w+`
|
|
||||||
re := regexp.MustCompile(pattern)
|
|
||||||
|
|
||||||
matches := re.FindAllString(content, -1)
|
matches := re.FindAllString(content, -1)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,18 +51,6 @@ func (jd *JadeDepo) SearchContent(query string) ([]string, error) {
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchByTag searches for notes containing a specific tag
|
|
||||||
func (jd *JadeDepo) SearchByTag(tag string) ([]string, error) {
|
|
||||||
// Ensure tag has prefix
|
|
||||||
if !strings.HasPrefix(tag, jd.Config.TagPrefix) {
|
|
||||||
tag = jd.Config.TagPrefix + tag
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use ripgrep to find the tag (escape special regex characters)
|
|
||||||
escapedTag := regexp.QuoteMeta(tag)
|
|
||||||
return jd.SearchContent(escapedTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAllNotes returns all markdown notes in the depository
|
// ListAllNotes returns all markdown notes in the depository
|
||||||
func (jd *JadeDepo) ListAllNotes() ([]*Note, error) {
|
func (jd *JadeDepo) ListAllNotes() ([]*Note, error) {
|
||||||
var notes []*Note
|
var notes []*Note
|
||||||
@@ -85,7 +72,7 @@ func (jd *JadeDepo) ListAllNotes() ([]*Note, error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
note, err := LoadNote(jd.Config.DepoPath, relPath, jd.Config.TagPrefix)
|
note, err := LoadNote(jd.Config.DepoPath, relPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Log error but continue
|
// Log error but continue
|
||||||
fmt.Fprintf(os.Stderr, "Warning: failed to load note %s: %v\n", relPath, err)
|
fmt.Fprintf(os.Stderr, "Warning: failed to load note %s: %v\n", relPath, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user