Fix .gitignore: track cmd/ directories, add mdsync + server source
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
configDir = filepath.Join(homeDir(), ".config", "mdsync")
|
||||
tokenFile = filepath.Join(configDir, "token")
|
||||
serverFile = filepath.Join(configDir, "server")
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
switch os.Args[1] {
|
||||
case "login":
|
||||
cmdLogin()
|
||||
case "pull":
|
||||
cmdPull()
|
||||
case "push":
|
||||
cmdPush()
|
||||
case "status":
|
||||
cmdStatus()
|
||||
case "list":
|
||||
cmdList()
|
||||
case "flag":
|
||||
cmdFlag()
|
||||
case "unflag":
|
||||
cmdUnflag()
|
||||
default:
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println(`mdsync — MarkdownHub CLI sync tool
|
||||
|
||||
Usage: mdsync <command>
|
||||
|
||||
Commands:
|
||||
login <server-url> <email> <password> Authenticate and store token
|
||||
pull [path] Pull flagged files to current directory
|
||||
push [path] Push local .md files back to server
|
||||
status Show sync status
|
||||
list List all files
|
||||
flag <path> Flag a file for sync
|
||||
unflag <path> Remove sync flag`)
|
||||
}
|
||||
|
||||
func cmdLogin() {
|
||||
if len(os.Args) < 5 {
|
||||
fmt.Println("Usage: mdsync login <server-url> <email> <password>")
|
||||
os.Exit(1)
|
||||
}
|
||||
server := strings.TrimRight(os.Args[2], "/")
|
||||
email := os.Args[3]
|
||||
password := os.Args[4]
|
||||
|
||||
resp, err := apiPost(server, "/api/auth/login", map[string]string{"email": email, "password": password}, "")
|
||||
if err != nil {
|
||||
fmt.Printf("Login failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
token, _ := resp["token"].(string)
|
||||
if token == "" {
|
||||
fmt.Println("Login failed: no token received")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.MkdirAll(configDir, 0700)
|
||||
os.WriteFile(tokenFile, []byte(token), 0600)
|
||||
os.WriteFile(serverFile, []byte(server), 0600)
|
||||
fmt.Println("Logged in successfully")
|
||||
}
|
||||
|
||||
func cmdList() {
|
||||
server, token := loadConfig()
|
||||
resp, err := apiPost(server, "/api/files/list", map[string]string{}, token)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
printTree(resp, "")
|
||||
}
|
||||
|
||||
func cmdPull() {
|
||||
server, token := loadConfig()
|
||||
dest := "."
|
||||
if len(os.Args) > 2 {
|
||||
dest = os.Args[2]
|
||||
}
|
||||
|
||||
// Get file list
|
||||
files := listAllFiles(server, token)
|
||||
for _, f := range files {
|
||||
resp, err := apiPost(server, "/api/files/read", map[string]string{"path": f}, token)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR %s: %v\n", f, err)
|
||||
continue
|
||||
}
|
||||
content, _ := resp["content"].(string)
|
||||
outPath := filepath.Join(dest, f)
|
||||
os.MkdirAll(filepath.Dir(outPath), 0755)
|
||||
os.WriteFile(outPath, []byte(content), 0644)
|
||||
fmt.Printf(" PULL %s\n", f)
|
||||
}
|
||||
fmt.Printf("Pulled %d files\n", len(files))
|
||||
}
|
||||
|
||||
func cmdPush() {
|
||||
server, token := loadConfig()
|
||||
src := "."
|
||||
if len(os.Args) > 2 {
|
||||
src = os.Args[2]
|
||||
}
|
||||
|
||||
count := 0
|
||||
filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".md" && ext != ".txt" {
|
||||
return nil
|
||||
}
|
||||
rel, _ := filepath.Rel(src, path)
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = apiPost(server, "/api/files/write", map[string]interface{}{
|
||||
"path": rel, "content": string(content),
|
||||
}, token)
|
||||
if err != nil {
|
||||
fmt.Printf(" ERROR %s: %v\n", rel, err)
|
||||
} else {
|
||||
fmt.Printf(" PUSH %s\n", rel)
|
||||
count++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
fmt.Printf("Pushed %d files\n", count)
|
||||
}
|
||||
|
||||
func cmdStatus() {
|
||||
server, token := loadConfig()
|
||||
resp, err := apiPost(server, "/api/git/status", map[string]string{}, token)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
dirty, _ := resp["dirty"].(float64)
|
||||
if dirty == 0 {
|
||||
fmt.Println("All synced (no uncommitted changes)")
|
||||
} else {
|
||||
fmt.Printf("%d uncommitted change(s)\n", int(dirty))
|
||||
}
|
||||
}
|
||||
|
||||
func cmdFlag() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: mdsync flag <path>")
|
||||
os.Exit(1)
|
||||
}
|
||||
server, token := loadConfig()
|
||||
_, err := apiPost(server, "/api/git/flag", map[string]string{"path": os.Args[2]}, token)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Flagged: %s\n", os.Args[2])
|
||||
}
|
||||
|
||||
func cmdUnflag() {
|
||||
if len(os.Args) < 3 {
|
||||
fmt.Println("Usage: mdsync unflag <path>")
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Unflagged: %s\n", os.Args[2])
|
||||
}
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
func loadConfig() (string, string) {
|
||||
serverBytes, err := os.ReadFile(serverFile)
|
||||
if err != nil {
|
||||
fmt.Println("Not logged in. Run: mdsync login <server> <email> <password>")
|
||||
os.Exit(1)
|
||||
}
|
||||
tokenBytes, err := os.ReadFile(tokenFile)
|
||||
if err != nil {
|
||||
fmt.Println("Not logged in. Run: mdsync login <server> <email> <password>")
|
||||
os.Exit(1)
|
||||
}
|
||||
return strings.TrimSpace(string(serverBytes)), strings.TrimSpace(string(tokenBytes))
|
||||
}
|
||||
|
||||
func apiPost(server, path string, data interface{}, token string) (map[string]interface{}, error) {
|
||||
body, _ := json.Marshal(data)
|
||||
req, err := http.NewRequest("POST", server+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal(respBody, &result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func listAllFiles(server, token string) []string {
|
||||
resp, err := apiPost(server, "/api/files/list", map[string]string{}, token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Response is an array, need to handle differently
|
||||
body, _ := json.Marshal(resp)
|
||||
// Re-fetch as array
|
||||
reqBody, _ := json.Marshal(map[string]string{})
|
||||
req, _ := http.NewRequest("POST", server+"/api/files/list", bytes.NewReader(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
r, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer r.Body.Close()
|
||||
body, _ = io.ReadAll(r.Body)
|
||||
var items []map[string]interface{}
|
||||
json.Unmarshal(body, &items)
|
||||
return flattenFiles(items, "")
|
||||
}
|
||||
|
||||
func flattenFiles(items []map[string]interface{}, prefix string) []string {
|
||||
var result []string
|
||||
for _, item := range items {
|
||||
path, _ := item["path"].(string)
|
||||
isDir, _ := item["isDir"].(bool)
|
||||
if isDir {
|
||||
children, _ := item["children"].([]interface{})
|
||||
var childMaps []map[string]interface{}
|
||||
for _, c := range children {
|
||||
if m, ok := c.(map[string]interface{}); ok {
|
||||
childMaps = append(childMaps, m)
|
||||
}
|
||||
}
|
||||
result = append(result, flattenFiles(childMaps, "")...)
|
||||
} else {
|
||||
result = append(result, path)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func printTree(resp map[string]interface{}, indent string) {
|
||||
// The list endpoint returns an array, print raw
|
||||
fmt.Printf("%v\n", resp)
|
||||
}
|
||||
|
||||
func homeDir() string {
|
||||
h, _ := os.UserHomeDir()
|
||||
return h
|
||||
}
|
||||
Reference in New Issue
Block a user