Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ gander signup --email you@example.com # opens browser form, polls for API toke
gander share README.md # opens https://gander.md/s/xK7m2pQa
gander watch README.md # upload + live-update the remote viewer on save
gander share README.md --watch # same as `watch`, spelled out
gander share README.md --comments team # only the author's team can comment
gander share README.md --no-comments # same as --comments team (hide threads from the public)
gander share README.md --private # private doc; team access + team-only threads
gander list # table of active shares
gander remove README.md # 404s the short link
gander remove --all # remove every share in your account
Expand Down Expand Up @@ -264,8 +267,10 @@ Subcommands:

```
gander signup --email <addr> Open the signup form in your browser, save the API token
gander share [--watch] <file> Upload to gander.md and open the share link
gander watch <file> Live-share to gander.md and push every save (alias for share --watch)
gander share [--watch] [--comments=anyone|team] [--private] [--no-comments] <file>
Upload to gander.md and open the share link
gander watch [--comments=anyone|team] [--private] [--no-comments] <file>
Live-share to gander.md and push every save (alias for share --watch)
gander status Show runner + active watches + URLs
gander stop [<file>|<id>] [--all] Stop a watch (by file, id, or --all)
gander logs [<id>] Tail the runner log (optionally filtered by watch id)
Expand Down
27 changes: 24 additions & 3 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,24 @@ type shareResp struct {
Path string `json:"path,omitempty"`
Watch bool `json:"watch"`
URL string `json:"url"`
CommentAccess string `json:"comment_access"`
CommentVisibility string `json:"comment_visibility"`
DocVisibility string `json:"doc_visibility"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
SizeBytes int `json:"size_bytes"`
UnresolvedCount int `json:"unresolved_count"`
AgentUnresolvedCount int `json:"agent_unresolved_count"`
}

// shareOpts are POST /api/shares policy fields. Empty strings are omitted so
// an upsert does not clobber stored values (server omitted = no-touch).
type shareOpts struct {
CommentAccess string
CommentVisibility string
DocVisibility string
}

type commentView struct {
UUID string `json:"uuid"`
AuthorKind string `json:"author_kind"`
Expand Down Expand Up @@ -152,14 +163,24 @@ func (c *apiClient) OpenManageIntent() (*manageIntentResp, error) {
return &out, nil
}

func (c *apiClient) CreateShare(filename, path, content string, watch bool) (*shareResp, bool, error) {
func (c *apiClient) CreateShare(filename, path, content string, watch bool, opts shareOpts) (*shareResp, bool, error) {
var out shareResp
status, err := c.doStatus("POST", "/api/shares", map[string]any{
body := map[string]any{
"filename": filename,
"path": path,
"content": content,
"watch": watch,
}, &out)
}
if opts.CommentAccess != "" {
body["comment_access"] = opts.CommentAccess
}
if opts.CommentVisibility != "" {
body["comment_visibility"] = opts.CommentVisibility
}
if opts.DocVisibility != "" {
body["doc_visibility"] = opts.DocVisibility
}
status, err := c.doStatus("POST", "/api/shares", body, &out)
if err != nil {
return nil, false, err
}
Expand Down
4 changes: 2 additions & 2 deletions completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestRunCompletionBashNonEmpty(t *testing.T) {
if out == "" {
t.Fatal("bash completion is empty")
}
for _, want := range []string{"complete -F", "signup", "share", "remove", "completion"} {
for _, want := range []string{"complete -F", "signup", "share", "remove", "completion", "--comments", "--no-comments", "--private"} {
if !strings.Contains(out, want) {
t.Errorf("bash completion missing %q\n%s", want, out)
}
Expand Down Expand Up @@ -69,7 +69,7 @@ func TestRunCompletionZshNonEmpty(t *testing.T) {
if out == "" {
t.Fatal("zsh completion is empty")
}
for _, want := range []string{"#compdef gander", "_gander", "signup", "share", "remove", "completion"} {
for _, want := range []string{"#compdef gander", "_gander", "signup", "share", "remove", "completion", "--comments", "--no-comments", "--private"} {
if !strings.Contains(out, want) {
t.Errorf("zsh completion missing %q\n%s", want, out)
}
Expand Down
9 changes: 9 additions & 0 deletions completions/_gander
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,20 @@ _gander() {
share)
_arguments \
'--watch[Live-update the shared page as the file changes]' \
'--foreground[Keep share --watch in-process instead of handing off to the runner]' \
'--comments[Who may comment]:access:(anyone team)' \
'--comment-visibility[Who may see comment threads]:visibility:(public team)' \
'--private[Make the document private (team access + team-only threads)]' \
'--no-comments[Hide comments from the public (alias for --comments team)]' \
'*:markdown file:_files -g "*.md"'
;;
watch)
_arguments \
'--foreground[Run the foreground blocking watcher instead of handing off to the runner]' \
'--comments[Who may comment]:access:(anyone team)' \
'--comment-visibility[Who may see comment threads]:visibility:(public team)' \
'--private[Make the document private (team access + team-only threads)]' \
'--no-comments[Hide comments from the public (alias for --comments team)]' \
'*:markdown file:_files -g "*.md"'
;;
remove)
Expand Down
22 changes: 16 additions & 6 deletions completions/gander.bash
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,22 @@ _gander_completions() {
COMPREPLY=( $(compgen -W "--email --force" -- "${cur}") )
return 0
;;
share)
COMPREPLY=( $(compgen -W "--watch" -- "${cur}") )
return 0
;;
watch)
COMPREPLY=( $(compgen -W "--foreground" -- "${cur}") )
share|watch)
case "${prev}" in
--comments)
COMPREPLY=( $(compgen -W "anyone team" -- "${cur}") )
return 0
;;
--comment-visibility)
COMPREPLY=( $(compgen -W "public team" -- "${cur}") )
return 0
;;
esac
local share_flags="--watch --foreground --comments --comment-visibility --private --no-comments"
if [[ "${COMP_WORDS[1]}" == "watch" ]]; then
share_flags="--foreground --comments --comment-visibility --private --no-comments"
fi
COMPREPLY=( $(compgen -W "${share_flags}" -- "${cur}") )
COMPREPLY+=( $(compgen -f -- "${cur}") )
return 0
;;
Expand Down
11 changes: 9 additions & 2 deletions list.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func runList(_ []string) error {
}

tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "SHORT ID\tFILE\tPATH\tWATCH\tUPDATED\tURL")
fmt.Fprintln(tw, "SHORT ID\tFILE\tPATH\tWATCH\tCOMMENTS\tTHREADS\tPRIVATE\tUPDATED\tURL")
for i := range all {
watch := "no"
if all[i].Watch {
Expand All @@ -37,12 +37,19 @@ func runList(_ []string) error {
if path == "" {
path = "-"
}
priv := ""
if all[i].DocVisibility == "private" {
priv = "yes"
}
updated, _ := time.Parse(time.RFC3339, all[i].UpdatedAt)
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n",
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
all[i].ShortID,
all[i].Filename,
path,
watch,
all[i].CommentAccess,
all[i].CommentVisibility,
priv,
updated.Format("2006-01-02 15:04 MST"),
all[i].URL,
)
Expand Down
6 changes: 4 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,10 @@ func printUsage(w io.Writer) {
fmt.Fprintln(w, " gander <file.md> [options] Render and open locally")
fmt.Fprintln(w, " gander signup --email <addr> Open signup form in your browser, save the API token")
if authed {
fmt.Fprintln(w, " gander share [--watch] <file> Upload to gander.md and open the share link")
fmt.Fprintln(w, " gander watch <file> Live-share to gander.md and push every save (alias for `share --watch`)")
fmt.Fprintln(w, " gander share [--watch] [--comments=anyone|team] [--comment-visibility=public|team] [--private] [--no-comments] <file>")
fmt.Fprintln(w, " Upload to gander.md and open the share link")
fmt.Fprintln(w, " gander watch [--comments=anyone|team] [--comment-visibility=public|team] [--private] [--no-comments] <file>")
fmt.Fprintln(w, " Live-share to gander.md and push every save (alias for `share --watch`)")
fmt.Fprintln(w, " gander remove [--all|--pick <short_id>|--yes|--non-interactive] <file|short_id|url>")
fmt.Fprintln(w, " Delete a share from gander.md")
fmt.Fprintln(w, " gander list List shares currently on gander.md")
Expand Down
49 changes: 48 additions & 1 deletion man/man1/gander.1
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,17 @@ gander \- render Markdown locally, optionally share it on gander.md
.sp
.B gander share
.RB [ \-watch ]
.RB [ \-\-comments =\fIanyone\fR|\fIteam\fR]
.RB [ \-\-comment\-visibility =\fIpublic\fR|\fIteam\fR]
.RB [ \-\-private ]
.RB [ \-\-no\-comments ]
.IR file.md
.sp
.B gander watch
.RB [ \-\-comments =\fIanyone\fR|\fIteam\fR]
.RB [ \-\-comment\-visibility =\fIpublic\fR|\fIteam\fR]
.RB [ \-\-private ]
.RB [ \-\-no\-comments ]
.IR file.md
.sp
.B gander status
Expand Down Expand Up @@ -142,11 +150,40 @@ The address to register. Required.
Upload
.IR file.md
to gander.md, print the share link, and open it in the browser.
Omit each policy flag to leave the stored value alone on an upsert
(new shares default to anyone may comment, public threads, public doc).
.sp
.RS
.TP
.B \-watch
Continuously push local changes to the remote share until interrupted.
.TP
.BR \-\-comments =\fIanyone\fR|\fIteam
Who may write comments.
.I team
limits writes to the author and members with a comment grant.
Omitting thread visibility with
.B \-\-comments team
lets the server default threads to team\-only.
.TP
.BR \-\-comment\-visibility =\fIpublic\fR|\fIteam
Who may see comment threads.
.TP
.B \-\-private
Make the document private. The share URL is no longer a read
credential; viewers must be signed in as the owner or hold a read/comment
grant. Omitting the other policy flags lets the server default comment
access and thread visibility to team.
.TP
.B \-\-no\-comments
Alias for
.BR "\-\-comments team" :
hide threads from the public; the author's team can still comment.
Cannot be combined with
.BR "\-\-comments anyone" .
.B \-\-private
cannot be combined with
.BR "\-\-comments anyone" .
.RE
.SS "Watch"
.TP
Expand All @@ -157,6 +194,15 @@ Shorthand for
.IR file.md
to gander.md and live\-update the remote share on every save. The
share is owned by the runner daemon and survives the CLI's lifetime.
Accepts the same
.BR \-\-comments ","
.BR \-\-comment\-visibility ","
.BR \-\-private ","
and
.B \-\-no\-comments
flags as
.BR share .
Content pushes send only the markdown body and never clobber policy.
.SS "Status"
.TP
.B status
Expand Down Expand Up @@ -231,7 +277,8 @@ when the argument is ambiguous.
.TP
.B list
Print a table of every share in the user's gander.md account (short id,
file, watch flag, last update, URL).
file, watch flag, comment access, thread visibility, private, last
update, URL).
.SS "Comments"
.TP
.B comments
Expand Down
1 change: 1 addition & 0 deletions manpage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func TestManPageExistsAndRenders(t *testing.T) {
"NAME", "SYNOPSIS", "DESCRIPTION", "OPTIONS", "COMMANDS", "FILES",
"EXIT STATUS", "EXAMPLES",
"gander signup", "gander share", "gander watch", "gander remove",
"--comments", "--no-comments", "--private",
"gander list", "gander auth", "gander completion",
"gander uninstall",
"--upgrade",
Expand Down
Loading