Add invite strings (waste: URI) for peer onboarding
- internal/invite: Encode/Decode waste:<base64json{anchor,network}>
- proto: CmdGenerateInvite + EvtInviteGenerated + InviteString field
- ipc: track active network name; handle generate_invite (returns invite
string when joined to a network, errors if not joined or no anchor)
- daemon: --join <invite> flag — decodes anchor URL + network name,
sets anchor and auto-joins on startup
- tui: --join <invite> flag — extracts network name, skips -network
requirement; ctrl+i generates invite and shows it full-screen;
Esc dismisses the invite overlay
- FUTURE.md: document multi-network derived-identity design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/waste-go/internal/anchor"
|
||||
"github.com/waste-go/internal/crypto"
|
||||
"github.com/waste-go/internal/invite"
|
||||
"github.com/waste-go/internal/ipc"
|
||||
"github.com/waste-go/internal/mesh"
|
||||
"github.com/waste-go/internal/proto"
|
||||
@@ -22,8 +23,21 @@ func main() {
|
||||
alias := flag.String("alias", "anon", "display name shown to peers (advisory only)")
|
||||
ipcPort := flag.Int("ipc-port", 17337, "port for local IPC (UI connects here)")
|
||||
anchorURL := flag.String("anchor", "", "anchor WebSocket URL, e.g. ws://your-vps:17339/ws")
|
||||
joinInvite := flag.String("join", "", "waste: invite string — sets anchor URL and auto-joins the network on startup")
|
||||
flag.Parse()
|
||||
|
||||
// --join overrides/sets the anchor URL and triggers an auto-join.
|
||||
var autoJoinNetwork string
|
||||
if *joinInvite != "" {
|
||||
inv, err := invite.Decode(*joinInvite)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid invite: %v", err)
|
||||
}
|
||||
*anchorURL = inv.Anchor
|
||||
autoJoinNetwork = inv.Network
|
||||
log.Printf("daemon: invite decoded — anchor=%s network=%s", inv.Anchor, inv.Network)
|
||||
}
|
||||
|
||||
dir := expandHome(*dataDir)
|
||||
id, err := crypto.LoadOrCreate(dir, *alias)
|
||||
if err != nil {
|
||||
@@ -51,7 +65,14 @@ func main() {
|
||||
log.Printf("daemon: left network %q", networkName)
|
||||
}
|
||||
|
||||
if err := ipc.Run(m, *ipcPort, joinFn); err != nil {
|
||||
// Auto-join from --join flag before starting IPC (non-blocking).
|
||||
if autoJoinNetwork != "" {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
_ = cancel // lifecycle managed by the joinFn / ipc.Run leave
|
||||
go joinFn(ctx, autoJoinNetwork)
|
||||
}
|
||||
|
||||
if err := ipc.Run(m, *ipcPort, *anchorURL, joinFn); err != nil {
|
||||
log.Fatalf("ipc: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/waste-go/internal/invite"
|
||||
"github.com/waste-go/internal/proto"
|
||||
)
|
||||
|
||||
@@ -114,8 +115,9 @@ type model struct {
|
||||
viewport viewport.Model
|
||||
vpReady bool
|
||||
|
||||
status string
|
||||
errMsg string
|
||||
status string
|
||||
errMsg string
|
||||
invitePopup string // non-empty = show invite overlay
|
||||
}
|
||||
|
||||
func newModel(ipcPort int, network string) model {
|
||||
@@ -200,7 +202,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case msg.Type == tea.KeyCtrlC:
|
||||
return m, tea.Quit
|
||||
case msg.Type == tea.KeyEsc:
|
||||
if m.invitePopup != "" {
|
||||
m.invitePopup = ""
|
||||
return m, nil
|
||||
}
|
||||
return m, tea.Quit
|
||||
case msg.String() == "ctrl+i":
|
||||
if m.enc != nil {
|
||||
cmds = append(cmds, sendIPC(m.enc, proto.IpcMessage{Type: proto.CmdGenerateInvite}))
|
||||
}
|
||||
case msg.Type == tea.KeyEnter:
|
||||
m, cmds = m.doSend(cmds)
|
||||
case msg.Type == tea.KeyTab:
|
||||
@@ -272,6 +282,9 @@ func (m model) applyEvent(evt proto.IpcMessage) model {
|
||||
m.peerOrder = filterIDs(m.peerOrder, pid)
|
||||
}
|
||||
|
||||
case proto.EvtInviteGenerated:
|
||||
m.invitePopup = evt.InviteString
|
||||
|
||||
case proto.EvtMessageReceived:
|
||||
if evt.Message != nil {
|
||||
msg := evt.Message
|
||||
@@ -417,11 +430,31 @@ func (m model) View() string {
|
||||
if m.errMsg != "" {
|
||||
statusLine = styleErr.Render(" ✗ " + m.errMsg)
|
||||
} else {
|
||||
hint := " tab: rooms · pgup/dn: scroll · ctrl+c: quit"
|
||||
hint := " tab: rooms · ctrl+i: invite · ctrl+c: quit"
|
||||
statusLine = styleStatus.Width(m.width).Render(" " + m.status + hint)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, mainRow, inputBox, statusLine)
|
||||
view := lipgloss.JoinVertical(lipgloss.Left, mainRow, inputBox, statusLine)
|
||||
|
||||
// ── invite popup (full-screen overlay) ───────────────────────────────────
|
||||
if m.invitePopup != "" {
|
||||
label := styleActive.Render("Invite — share this with anyone you want to add:")
|
||||
code := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("238")).
|
||||
Padding(0, 1).
|
||||
Width(m.width - 6).
|
||||
Render(m.invitePopup)
|
||||
hint := styleStatus.Render("Esc to close · ctrl+c to quit")
|
||||
popup := lipgloss.NewStyle().
|
||||
Width(m.width).
|
||||
Height(m.height).
|
||||
Align(lipgloss.Center, lipgloss.Center).
|
||||
Render(lipgloss.JoinVertical(lipgloss.Left, label, "", code, "", hint))
|
||||
return popup
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func (m model) renderRooms(boxH int) string {
|
||||
@@ -565,12 +598,23 @@ func min(a, b int) int {
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func main() {
|
||||
ipcPort := flag.Int("ipc", 17337, "daemon IPC port")
|
||||
network := flag.String("network", "", "network name to join on startup (required)")
|
||||
ipcPort := flag.Int("ipc", 17337, "daemon IPC port")
|
||||
network := flag.String("network", "", "network name to join on startup")
|
||||
joinInvite := flag.String("join", "", "waste: invite string — auto-sets the network name")
|
||||
flag.Parse()
|
||||
|
||||
// --join overrides --network.
|
||||
if *joinInvite != "" {
|
||||
inv, err := invite.Decode(*joinInvite)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error: invalid invite:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
*network = inv.Network
|
||||
}
|
||||
|
||||
if *network == "" {
|
||||
fmt.Fprintln(os.Stderr, "error: -network is required")
|
||||
fmt.Fprintln(os.Stderr, "error: -network or -join is required")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user