Skip to content

Commit a05250d

Browse files
committed
docs: cleanup documentation & add migrations for leaflet fields
* scaffold for leaflet integration
1 parent 3e2f65b commit a05250d

File tree

16 files changed

+1606
-36
lines changed

16 files changed

+1606
-36
lines changed

README.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,6 @@
55
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
66
[![Go Version](https://img.shields.io/github/go-mod/go-version/stormlightlabs/noteleaf)](go.mod)
77

8-
```sh
9-
,, ,...
10-
`7MN. `7MF' mm `7MM .d' ""
11-
MMN. M MM MM dM`
12-
M YMb M ,pW"Wq.mmMMmm .gP"Ya MM .gP"Ya ,6"Yb. mMMmm
13-
M `MN. M 6W' `Wb MM ,M' Yb MM ,M' Yb 8) MM MM
14-
M `MM.M 8M M8 MM 8M"""""" MM 8M"""""" ,pm9MM MM
15-
M YMM YA. ,A9 MM YM. , MM YM. , 8M MM MM
16-
.JML. YM `Ybmd9' `Mbmo`Mbmmd'.JMML.`Mbmmd' `Moo9^Yo..JMML.
17-
```
18-
198
Noteleaf is a unified personal productivity CLI that combines task management, note-taking, and media tracking in one place.
209
It provides TaskWarrior-inspired task management with additional support for notes, articles, books, movies, and TV shows - all built with Golang & Charm.sh libs. Inspired by TaskWarrior & todo.txt CLI applications.
2110

@@ -72,8 +61,6 @@ noteleaf docgen --format docusaurus --out ./website/docs/manual
7261

7362
### Completed
7463

75-
Core functionality is complete and stable:
76-
7764
- Task management with projects and tags
7865
- Note-taking system
7966
- Article parsing from URLs

cmd/main.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ import (
1717
)
1818

1919
var (
20-
newTaskHandler = handlers.NewTaskHandler
21-
newMovieHandler = handlers.NewMovieHandler
22-
newTVHandler = handlers.NewTVHandler
23-
newNoteHandler = handlers.NewNoteHandler
24-
newBookHandler = handlers.NewBookHandler
25-
newArticleHandler = handlers.NewArticleHandler
26-
exc = fang.Execute
20+
newTaskHandler = handlers.NewTaskHandler
21+
newMovieHandler = handlers.NewMovieHandler
22+
newTVHandler = handlers.NewTVHandler
23+
newNoteHandler = handlers.NewNoteHandler
24+
newBookHandler = handlers.NewBookHandler
25+
newArticleHandler = handlers.NewArticleHandler
26+
newPublicationHandler = handlers.NewPublicationHandler
27+
exc = fang.Execute
2728
)
2829

2930
// App represents the main CLI application
@@ -206,10 +207,17 @@ func run() int {
206207
return 1
207208
}
208209

210+
publicationHandler, err := newPublicationHandler()
211+
if err != nil {
212+
log.Error("failed to create publication handler", "err", err)
213+
return 1
214+
}
215+
209216
root := rootCmd()
210217

211218
coreGroups := []CommandGroup{
212219
NewTaskCommand(taskHandler), NewNoteCommand(noteHandler), NewArticleCommand(articleHandler),
220+
NewPublicationCommand(publicationHandler),
213221
}
214222

215223
for _, group := range coreGroups {

cmd/publication_commands.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// TODO: implement prompt for password
2+
//
3+
// See: https://github.com/charmbracelet/bubbletea/blob/main/examples/textinputs/main.go
4+
package main
5+
6+
import (
7+
"fmt"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/stormlightlabs/noteleaf/internal/handlers"
11+
)
12+
13+
// PublicationCommand implements [CommandGroup] for leaflet publication commands
14+
type PublicationCommand struct {
15+
handler *handlers.PublicationHandler
16+
}
17+
18+
// NewPublicationCommand creates a new [PublicationCommand] with the given handler
19+
func NewPublicationCommand(handler *handlers.PublicationHandler) *PublicationCommand {
20+
return &PublicationCommand{handler: handler}
21+
}
22+
23+
func (c *PublicationCommand) Create() *cobra.Command {
24+
root := &cobra.Command{
25+
Use: "pub",
26+
Short: "Manage leaflet publication sync",
27+
Long: `Sync notes with leaflet.pub (AT Protocol publishing platform).
28+
29+
Authenticate with your BlueSky account to pull drafts and published documents
30+
from leaflet.pub into your local notes. Track publication status and manage
31+
your writing workflow across platforms.
32+
33+
Authentication uses AT Protocol (the same system as BlueSky). You'll need:
34+
- BlueSky handle (e.g., username.bsky.social)
35+
- App password (generated at bsky.app/settings/app-passwords)
36+
37+
Getting Started:
38+
1. Authenticate: noteleaf pub auth <handle>
39+
2. Pull documents: noteleaf pub pull
40+
3. List publications: noteleaf pub list`,
41+
}
42+
43+
authCmd := &cobra.Command{
44+
Use: "auth [handle]",
45+
Short: "Authenticate with BlueSky/leaflet",
46+
Long: `Authenticate with AT Protocol (BlueSky) for leaflet access.
47+
48+
Your handle is typically: username.bsky.social
49+
50+
For the password, use an app password (not your main password):
51+
1. Go to bsky.app/settings/app-passwords
52+
2. Create a new app password named "noteleaf"
53+
3. Use that password here
54+
55+
The password will be prompted securely if not provided via flag.`,
56+
Args: cobra.MaximumNArgs(1),
57+
RunE: func(cmd *cobra.Command, args []string) error {
58+
var handle string
59+
if len(args) > 0 {
60+
handle = args[0]
61+
}
62+
63+
password, _ := cmd.Flags().GetString("password")
64+
65+
if handle == "" {
66+
return fmt.Errorf("handle is required")
67+
}
68+
69+
if password == "" {
70+
return fmt.Errorf("password is required (use --password flag)")
71+
}
72+
73+
defer c.handler.Close()
74+
return c.handler.Auth(cmd.Context(), handle, password)
75+
},
76+
}
77+
authCmd.Flags().StringP("password", "p", "", "App password (will be prompted if not provided)")
78+
root.AddCommand(authCmd)
79+
80+
pullCmd := &cobra.Command{
81+
Use: "pull",
82+
Short: "Pull documents from leaflet",
83+
Long: `Fetch all drafts and published documents from leaflet.pub.
84+
85+
This will:
86+
- Connect to your BlueSky/leaflet account
87+
- Fetch all documents in your repository
88+
- Create new notes for documents not yet synced
89+
- Update existing notes that have changed
90+
91+
Notes are matched by their leaflet record key (rkey) stored in the database.`,
92+
RunE: func(cmd *cobra.Command, args []string) error {
93+
defer c.handler.Close()
94+
return c.handler.Pull(cmd.Context())
95+
},
96+
}
97+
root.AddCommand(pullCmd)
98+
99+
// List command
100+
listCmd := &cobra.Command{
101+
Use: "list [--published|--draft|--all]",
102+
Short: "List notes synced with leaflet",
103+
Aliases: []string{"ls"},
104+
Long: `Display notes that have been pulled from or pushed to leaflet.
105+
106+
Shows publication metadata including:
107+
- Publication status (draft vs published)
108+
- Published date
109+
- Leaflet record key (rkey)
110+
- Content identifier (cid) for change tracking
111+
112+
Use filters to show specific subsets:
113+
--published Show only published documents
114+
--draft Show only drafts
115+
--all Show all leaflet documents (default)`,
116+
RunE: func(cmd *cobra.Command, args []string) error {
117+
published, _ := cmd.Flags().GetBool("published")
118+
draft, _ := cmd.Flags().GetBool("draft")
119+
all, _ := cmd.Flags().GetBool("all")
120+
121+
filter := "all"
122+
if published {
123+
filter = "published"
124+
} else if draft {
125+
filter = "draft"
126+
} else if all {
127+
filter = "all"
128+
}
129+
130+
defer c.handler.Close()
131+
return c.handler.List(cmd.Context(), filter)
132+
},
133+
}
134+
listCmd.Flags().Bool("published", false, "Show only published documents")
135+
listCmd.Flags().Bool("draft", false, "Show only drafts")
136+
listCmd.Flags().Bool("all", false, "Show all leaflet documents")
137+
root.AddCommand(listCmd)
138+
139+
statusCmd := &cobra.Command{
140+
Use: "status",
141+
Short: "Show leaflet authentication status",
142+
Long: "Display current authentication status and session information.",
143+
RunE: func(cmd *cobra.Command, args []string) error {
144+
defer c.handler.Close()
145+
status := c.handler.GetAuthStatus()
146+
fmt.Println("Leaflet Status:")
147+
fmt.Printf(" %s\n", status)
148+
return nil
149+
},
150+
}
151+
root.AddCommand(statusCmd)
152+
153+
return root
154+
}
File renamed without changes.

internal/handlers/publication.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// TODO: Store credentials securely in [PublicationHandler.Auth]
2+
// Options:
3+
// 1. Use system keyring (go-keyring)
4+
// 2. Store encrypted in config file
5+
// 3. Store in environment variables
6+
//
7+
// TODO: Implement document processing
8+
// For each document:
9+
// 1. Check if note with this leaflet_rkey exists
10+
// 2. If exists: Update note content, title, metadata
11+
// 3. If new: Create new note with leaflet metadata
12+
// 4. Convert document blocks to markdown
13+
// 5. Save to database
14+
//
15+
// TODO: Implement list functionality
16+
// 1. Query notes where leaflet_rkey IS NOT NULL
17+
// 2. Apply filter (published vs draft) - "all", "published", "draft", or empty (default: all)
18+
// 3. Use prior art from package ui and other handlers to render
19+
//
20+
// TODO: Implmenent pull command
21+
// 1. Authenticates with AT Protocol
22+
// 2. Fetches all pub.leaflet.document records
23+
// 3. Creates new notes for documents not seen before
24+
// 4. Updates existing notes (matched by leaflet_rkey)
25+
// 5. Shows summary of pulled documents
26+
package handlers
27+
28+
import (
29+
"context"
30+
"fmt"
31+
32+
"github.com/stormlightlabs/noteleaf/internal/repo"
33+
"github.com/stormlightlabs/noteleaf/internal/services"
34+
"github.com/stormlightlabs/noteleaf/internal/store"
35+
)
36+
37+
// PublicationHandler handles leaflet publication commands
38+
type PublicationHandler struct {
39+
db *store.Database
40+
config *store.Config
41+
repos *repo.Repositories
42+
atproto *services.ATProtoService
43+
}
44+
45+
// NewPublicationHandler creates a new publication handler
46+
func NewPublicationHandler() (*PublicationHandler, error) {
47+
db, err := store.NewDatabase()
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to initialize database: %w", err)
50+
}
51+
52+
config, err := store.LoadConfig()
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to load configuration: %w", err)
55+
}
56+
57+
repos := repo.NewRepositories(db.DB)
58+
atproto := services.NewATProtoService()
59+
60+
return &PublicationHandler{
61+
db: db,
62+
config: config,
63+
repos: repos,
64+
atproto: atproto,
65+
}, nil
66+
}
67+
68+
// Close cleans up resources
69+
func (h *PublicationHandler) Close() error {
70+
if h.atproto != nil {
71+
if err := h.atproto.Close(); err != nil {
72+
return err
73+
}
74+
}
75+
if h.db != nil {
76+
return h.db.Close()
77+
}
78+
return nil
79+
}
80+
81+
// Auth handles authentication with BlueSky/leaflet
82+
func (h *PublicationHandler) Auth(ctx context.Context, handle, password string) error {
83+
if handle == "" {
84+
return fmt.Errorf("handle is required")
85+
}
86+
87+
if password == "" {
88+
return fmt.Errorf("password is required")
89+
}
90+
91+
fmt.Printf("Authenticating as %s...\n", handle)
92+
93+
if err := h.atproto.Authenticate(ctx, handle, password); err != nil {
94+
return fmt.Errorf("authentication failed: %w", err)
95+
}
96+
97+
fmt.Println("✓ Authentication successful!")
98+
fmt.Println("TODO: Implement persistent credential storage")
99+
return nil
100+
}
101+
102+
// Pull fetches all documents from leaflet and creates/updates local notes
103+
func (h *PublicationHandler) Pull(ctx context.Context) error {
104+
fmt.Println("TODO: Implement document conversion and note creation")
105+
return nil
106+
}
107+
108+
// List displays notes with leaflet publication metadata, showing all notes that
109+
// have been pulled from or pushed to leaflet
110+
func (h *PublicationHandler) List(ctx context.Context, filter string) error {
111+
fmt.Println("TODO: Implement leaflet document listing")
112+
return nil
113+
}
114+
115+
// GetAuthStatus returns the current authentication status
116+
func (h *PublicationHandler) GetAuthStatus() string {
117+
if h.atproto.IsAuthenticated() {
118+
session, _ := h.atproto.GetSession()
119+
if session != nil {
120+
return fmt.Sprintf("Authenticated as %s", session.Handle)
121+
}
122+
return "Authenticated (session details unavailable)"
123+
}
124+
return "Not authenticated"
125+
}

0 commit comments

Comments
 (0)