Files
beardrive/cmd/bdrive/url.go
T
Snow LeeandClaude Fable 5 5ff82c2c49 feat(cli): bdrive url — internal, permission-walled links agents share when they create files
New command printing a file's hub viewer URL: sign-in + project-org
membership required to open (the internal counterpart to bdrive share's
public links). Computed locally from the mount config — hub origin +
project id from the remote, path segments percent-encoded with literal
slashes; unsynced paths (ignored, or outside a --shared scope) are
refused so nobody gets handed a 404. --sync pushes first so a
just-created file's link resolves immediately; no arg = project home.

The plugin docs now instruct agents to include this link in their reply
whenever they create a shareable artifact (.md/.html/.csv/...) in the
shared folder, reserving bdrive share for people outside the hub:
SKILL.md command map + 'Share what you make' guidance, install.md root
pointer template + payoff step, README, CLAUDE.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5cxPQdSGJnjXCYY9GeWXt
2026-07-14 11:29:54 -07:00

110 lines
3.4 KiB
Go

package main
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/runbear-io/beardrive/internal/syncer"
)
// urlCmd prints a file's internal hub link — the viewer URL colleagues with
// project access open after signing in. The counterpart to `bdrive share`:
// share mints a PUBLIC link, url points at the permission-walled viewer.
func urlCmd() *cobra.Command {
var doSync bool
c := &cobra.Command{
Use: "url [path]",
Short: "Print a file's internal hub link (sign-in + membership required to view)",
Long: `Print the hub viewer URL for a file or folder in a bdrive project — the
link to hand teammates: it requires signing in to the hub and membership in
the project's organization, and always shows the latest synced content.
With no path (or "."), prints the project's home page URL.
This is the internal counterpart to "bdrive share": share mints a public
URL anyone can open; url points at the permission-walled viewer.
The link resolves once the file has synced — the daemon usually pushes
within seconds of saving; --sync pushes right now instead of waiting.
Computed locally from the folder's config; no network unless --sync.`,
Example: ` bdrive url wiki/report.md
bdrive url wiki/report.md --sync # push first, so the link works immediately
bdrive url wiki/ # the folder's listing
bdrive url # the project's home page`,
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
target := "."
if len(args) == 1 {
target = args[0]
}
abs, err := filepath.Abs(target)
if err != nil {
return err
}
// findProject walks up from a directory; start at the target
// itself only when it is one.
start := filepath.Dir(abs)
if fi, err := os.Stat(abs); err == nil && fi.IsDir() {
start = abs
}
root, proj, err := findProject(start)
if err != nil {
return err
}
rel, err := filepath.Rel(root, abs)
if err != nil || strings.HasPrefix(rel, "..") {
return fmt.Errorf("%s is outside the project at %s", abs, root)
}
rel = filepath.ToSlash(rel)
if rel != "." {
// A link to something the project doesn't sync would 404 for
// everyone — refuse it here instead.
filter, err := syncer.LoadFilter(root, proj.Include)
if err != nil {
return err
}
if filter.Skip(rel) {
return fmt.Errorf("%s is not synced (ignored, or outside the project's shared scope)", rel)
}
}
server, projectID, err := splitHubRemote(proj.Remote)
if err != nil {
return err
}
if doSync {
sess, _, err := openSession(cmd.Context(), root, true)
if err != nil {
return err
}
defer closeSession(sess)
if _, err := sess.Cycle(cmd.Context()); err != nil {
return err
}
}
link := server + "/" + projectID
if rel != "." {
link += "/" + encodePathSegments(rel)
}
fmt.Fprintln(cmd.OutOrStdout(), link)
return nil
},
}
c.Flags().BoolVar(&doSync, "sync", false, "run a sync first so a just-created file is pushed and the link resolves immediately")
return c
}
// encodePathSegments percent-encodes each path segment while keeping the
// "/" separators literal, matching the viewer's routing (no %2F).
func encodePathSegments(p string) string {
segs := strings.Split(p, "/")
for i, s := range segs {
segs[i] = url.PathEscape(s)
}
return strings.Join(segs, "/")
}