Skip to content
Merged
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
27 changes: 13 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1173,17 +1173,15 @@ still-pending.
carries `bandwidthId` somewhere in valid JSON — that ID is the one thing that
cannot be recovered any other way if the command exits without printing it.

**This guarantee covers every path the command itself takes — it does not
cover an interrupt.** `cmd/root.go`'s `Execute()` runs with no context, and
the CLI does not install a `SIGINT` handler anywhere (no `signal.Notify` /
`signal.NotifyContext` in the codebase), so the cancellation branch in
`awaitTerminal` that exists specifically to emit this receipt can never
actually fire from a real Ctrl-C. Press Ctrl-C during `--wait` after the 202
has landed and the process dies immediately with no `bandwidthId` on stdout.
If that happens, recover with `band tendlc brand list --customer-profile-id-contains
<id>` to find the brand that was accepted. This applies CLI-wide, not just to
`tendlc` — it is a pre-existing, repo-wide gap, tracked separately from this
PR.
**The guarantee covers a single Ctrl-C too.** `cmd/root.go`'s `Execute()`
runs the command tree under `signal.NotifyContext`, so the first
`SIGINT`/`SIGTERM` cancels `cmd.Context()` rather than killing the process:
the in-flight request aborts, the cancellation branch in `awaitTerminal`
fires, and the receipt (with `bandwidthId`) lands on stdout before the
command exits. A **second** Ctrl-C is not trapped — it hard-kills the
process the Go-default way, with nothing further on stdout. If a receipt was
lost that way, recover with `band tendlc brand list
--customer-profile-id-contains <id>` to find the brand that was accepted.

Without `--wait`, or on a timeout/transport failure with `--wait`, that's the
literal synthetic receipt: `{"bandwidthId": "...", "status": "accepted",
Expand Down Expand Up @@ -1794,9 +1792,10 @@ carries `bandwidthId` somewhere in valid JSON — the one thing that cannot be
recovered any other way if the command exits without printing it. This
covers `create`, `sync`, and `update` alike, all of which share the same
`{bandwidthId, campaignId (if present), status: "accepted", resume}`
receipt shape. As with `brand create`, this guarantee does not extend past a
`SIGINT` — see the equivalent note under [10DLC Brands](#create) for why and
how to recover (`band tendlc campaign list --brand-id-contains <id>`).
receipt shape. As with `brand create`, a single `SIGINT` is covered (the
receipt is emitted before exit); only a second Ctrl-C hard-kills the process
— see the equivalent note under [10DLC Brands](#create), and recover with
`band tendlc campaign list --brand-id-contains <id>`.

### `deactivate`'s honest receipt

Expand Down
2 changes: 1 addition & 1 deletion cmd/account/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func runRegister(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Post("/registration", reqBody, &result); err != nil {
if err := client.Post(cmd.Context(), "/registration", reqBody, &result); err != nil {
return fmt.Errorf("registering account: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/app/assign.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func runAssign(cmd *cobra.Command, args []string) error {
acctID, url.PathEscape(assignSite), url.PathEscape(assignLocation))

var result interface{}
if err := client.Put(path, body, &result); err != nil {
if err := client.Put(cmd.Context(), path, body, &result); err != nil {
return fmt.Errorf("assigning application to location: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/app/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func runCreate(cmd *cobra.Command, args []string) error {

if createIfNotExists {
var listResult interface{}
if err := client.Get(fmt.Sprintf("/accounts/%s/applications", acctID), &listResult); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/accounts/%s/applications", acctID), &listResult); err != nil {
return fmt.Errorf("listing applications: %w", err)
}
if existing := output.FindByName(listResult, "AppName", createName); existing != nil {
Expand All @@ -106,7 +106,7 @@ func runCreate(cmd *cobra.Command, args []string) error {
bodyData := BuildCreateBody(opts)

var result interface{}
if err := client.Post(fmt.Sprintf("/accounts/%s/applications", acctID), api.XMLBody{RootElement: "Application", Data: bodyData}, &result); err != nil {
if err := client.Post(cmd.Context(), fmt.Sprintf("/accounts/%s/applications", acctID), api.XMLBody{RootElement: "Application", Data: bodyData}, &result); err != nil {
if strings.Contains(err.Error(), "HTTP voice feature is required") {
return fmt.Errorf("creating voice application: this account requires the HTTP Voice feature to be enabled.\n" +
"Contact Bandwidth support to enable it, or check if your account is on the Universal Platform.\n" +
Expand Down
2 changes: 1 addition & 1 deletion cmd/app/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func runDelete(cmd *cobra.Command, args []string) error {
return err
}

if err := client.Delete(fmt.Sprintf("/accounts/%s/applications/%s", acctID, url.PathEscape(args[0])), nil); err != nil {
if err := client.Delete(cmd.Context(), fmt.Sprintf("/accounts/%s/applications/%s", acctID, url.PathEscape(args[0])), nil); err != nil {
return fmt.Errorf("deleting application: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/app/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func runGet(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Get(fmt.Sprintf("/accounts/%s/applications/%s", acctID, url.PathEscape(args[0])), &result); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/accounts/%s/applications/%s", acctID, url.PathEscape(args[0])), &result); err != nil {
return fmt.Errorf("getting application: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/app/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func runList(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Get(fmt.Sprintf("/accounts/%s/applications", acctID), &result); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/accounts/%s/applications", acctID), &result); err != nil {
return fmt.Errorf("listing applications: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/app/peers.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func runPeers(cmd *cobra.Command, args []string) error {

var result interface{}
path := fmt.Sprintf("/accounts/%s/applications/%s/associatedsippeers", acctID, url.PathEscape(args[0]))
if err := client.Get(path, &result); err != nil {
if err := client.Get(cmd.Context(), path, &result); err != nil {
return fmt.Errorf("getting application peers: %w", err)
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/app/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func runUpdate(cmd *cobra.Command, args []string) error {

// First, get the existing app to determine its type
var existing interface{}
if err := client.Get(fmt.Sprintf("/accounts/%s/applications/%s", acctID, url.PathEscape(appID)), &existing); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/accounts/%s/applications/%s", acctID, url.PathEscape(appID)), &existing); err != nil {
return fmt.Errorf("getting application: %w", err)
}

Expand Down Expand Up @@ -80,7 +80,7 @@ func runUpdate(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Put(fmt.Sprintf("/accounts/%s/applications/%s", acctID, url.PathEscape(appID)), body, &result); err != nil {
if err := client.Put(cmd.Context(), fmt.Sprintf("/accounts/%s/applications/%s", acctID, url.PathEscape(appID)), body, &result); err != nil {
return fmt.Errorf("updating application: %w", err)
}

Expand Down
5 changes: 3 additions & 2 deletions cmd/call/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func runCreate(cmd *cobra.Command, args []string) error {
})

var result interface{}
if err := client.Post(fmt.Sprintf("/accounts/%s/calls", acctID), reqBody, &result); err != nil {
if err := client.Post(cmd.Context(), fmt.Sprintf("/accounts/%s/calls", acctID), reqBody, &result); err != nil {
return fmt.Errorf("creating call: %w", err)
}

Expand All @@ -103,11 +103,12 @@ func runCreate(cmd *cobra.Command, args []string) error {
}

final, err := cmdutil.Poll(cmdutil.PollConfig{
Context: cmd.Context(),
Interval: 2 * time.Second,
Timeout: createTimeout,
Check: func() (bool, interface{}, error) {
var callState interface{}
if err := client.Get(fmt.Sprintf("/accounts/%s/calls/%s", acctID, url.PathEscape(callID)), &callState); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/accounts/%s/calls/%s", acctID, url.PathEscape(callID)), &callState); err != nil {
// The Voice API is eventually consistent — a 404 right after
// creation means the call record hasn't propagated yet. Retry.
var apiErr *api.APIError
Expand Down
2 changes: 1 addition & 1 deletion cmd/call/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func runGet(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Get(fmt.Sprintf("/accounts/%s/calls/%s", acctID, url.PathEscape(args[0])), &result); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/accounts/%s/calls/%s", acctID, url.PathEscape(args[0])), &result); err != nil {
return fmt.Errorf("getting call: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/call/hangup.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func runHangup(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Post(fmt.Sprintf("/accounts/%s/calls/%s", acctID, url.PathEscape(args[0])), reqBody, &result); err != nil {
if err := client.Post(cmd.Context(), fmt.Sprintf("/accounts/%s/calls/%s", acctID, url.PathEscape(args[0])), reqBody, &result); err != nil {
return fmt.Errorf("hanging up call: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/call/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func runList(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Get(fmt.Sprintf("/accounts/%s/calls", acctID), &result); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/accounts/%s/calls", acctID), &result); err != nil {
return fmt.Errorf("listing calls: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/call/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func runUpdate(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Post(fmt.Sprintf("/accounts/%s/calls/%s", acctID, url.PathEscape(args[0])), reqBody, &result); err != nil {
if err := client.Post(cmd.Context(), fmt.Sprintf("/accounts/%s/calls/%s", acctID, url.PathEscape(args[0])), reqBody, &result); err != nil {
return fmt.Errorf("updating call: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/customerprofile/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ retry — list profiles and reconcile against what you submitted first.`,
if err != nil {
return err
}
env, err := svc.Create(cpsvc.BuildCreateRequest(createOpts))
env, err := svc.Create(cmd.Context(), cpsvc.BuildCreateRequest(createOpts))
if err != nil {
return roleGateError(err)
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/customerprofile/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ attached.`,
if err != nil {
return err
}
if err := svc.Delete(args[0]); err != nil {
if err := svc.Delete(cmd.Context(), args[0]); err != nil {
return roleGateError(err)
}
format, plain := cmdutil.OutputFlags(cmd)
Expand Down Expand Up @@ -73,7 +73,7 @@ No --confirm needed: restoring is not destructive.`,
if err != nil {
return err
}
env, err := svc.Get(args[0])
env, err := svc.Get(cmd.Context(), args[0])
if err != nil {
return roleGateError(err)
}
Expand All @@ -85,7 +85,7 @@ No --confirm needed: restoring is not destructive.`,
if err != nil {
return err
}
restored, err := svc.Update(args[0], body)
restored, err := svc.Update(cmd.Context(), args[0], body)
if err != nil {
return roleGateError(conflictHint(err))
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/customerprofile/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var getCmd = &cobra.Command{
if err != nil {
return err
}
env, err := svc.Get(args[0])
env, err := svc.Get(cmd.Context(), args[0])
if err != nil {
return roleGateError(err)
}
Expand Down
6 changes: 3 additions & 3 deletions cmd/customerprofile/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ CREATED, UPDATED, and DELETED.`,
format, plain := cmdutil.OutputFlags(cmd)

if !historyAll {
env, err := svc.History(args[0], historyLimit, historyOffset)
env, err := svc.History(cmd.Context(), args[0], historyLimit, historyOffset)
if err != nil {
return roleGateError(err)
}
Expand All @@ -67,7 +67,7 @@ CREATED, UPDATED, and DELETED.`,

var all []any
err = api.ForEachPage(func(limit, offset int) (*api.Envelope, error) {
return svc.History(args[0], limit, offset)
return svc.History(cmd.Context(), args[0], limit, offset)
}, historyLimit, func(batch []any) error {
all = append(all, batch...)
return nil
Expand Down Expand Up @@ -101,7 +101,7 @@ count: list always returns an array, get always returns an object.`,
if err != nil {
return err
}
env, err := svc.HistoryVersion(args[0], args[1])
env, err := svc.HistoryVersion(cmd.Context(), args[0], args[1])
if err != nil {
return roleGateError(err)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/customerprofile/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ var listCmd = &cobra.Command{
format, plain := cmdutil.OutputFlags(cmd)

if !listAll {
env, err := svc.List(listLimit, listOffset, filters)
env, err := svc.List(cmd.Context(), listLimit, listOffset, filters)
if err != nil {
return roleGateError(err)
}
Expand All @@ -65,7 +65,7 @@ var listCmd = &cobra.Command{

var all []any
err = api.ForEachPage(func(limit, offset int) (*api.Envelope, error) {
return svc.List(limit, offset, filters)
return svc.List(cmd.Context(), limit, offset, filters)
}, listLimit, func(batch []any) error {
all = append(all, batch...)
return nil
Expand Down
4 changes: 2 additions & 2 deletions cmd/customerprofile/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ is rejected by the API's version check — the command exits 4 and you can retry
return err
}

env, err := svc.Get(args[0])
env, err := svc.Get(cmd.Context(), args[0])
if err != nil {
return roleGateError(err)
}
Expand All @@ -75,7 +75,7 @@ is rejected by the API's version check — the command exits 4 and you can retry
return err
}

updated, err := svc.Update(args[0], body)
updated, err := svc.Update(cmd.Context(), args[0], body)
if err != nil {
return roleGateError(conflictHint(err))
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/location/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func runCreate(cmd *cobra.Command, args []string) error {
if createIfNotExists {
var listResult interface{}
listPath := fmt.Sprintf("/accounts/%s/sites/%s/sippeers", acctID, createSiteID)
if err := client.Get(listPath, &listResult); err != nil {
if err := client.Get(cmd.Context(), listPath, &listResult); err != nil {
return fmt.Errorf("listing locations: %w", err)
}
if existing := output.FindByName(listResult, "PeerName", createName); existing != nil {
Expand All @@ -61,7 +61,7 @@ func runCreate(cmd *cobra.Command, args []string) error {

var result interface{}
path := fmt.Sprintf("/accounts/%s/sites/%s/sippeers", acctID, createSiteID)
if err := client.Post(path, api.XMLBody{RootElement: "SipPeer", Data: bodyData}, &result); err != nil {
if err := client.Post(cmd.Context(), path, api.XMLBody{RootElement: "SipPeer", Data: bodyData}, &result); err != nil {
return fmt.Errorf("creating location: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/location/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func runList(cmd *cobra.Command, args []string) error {

var result interface{}
path := fmt.Sprintf("/accounts/%s/sites/%s/sippeers", acctID, listSiteID)
if err := client.Get(path, &result); err != nil {
if err := client.Get(cmd.Context(), path, &result); err != nil {
return fmt.Errorf("listing locations: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/message/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func runGet(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Get(fmt.Sprintf("/users/%s/messages?messageId=%s", acctID, url.QueryEscape(args[0])), &result); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/users/%s/messages?messageId=%s", acctID, url.QueryEscape(args[0])), &result); err != nil {
return fmt.Errorf("getting message: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/message/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func runList(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Get(path, &result); err != nil {
if err := client.Get(cmd.Context(), path, &result); err != nil {
return fmt.Errorf("listing messages: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/message/media/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func runDelete(cmd *cobra.Command, args []string) error {
return err
}

if err := client.Delete(fmt.Sprintf("/users/%s/media/%s", acctID, args[0]), nil); err != nil {
if err := client.Delete(cmd.Context(), fmt.Sprintf("/users/%s/media/%s", acctID, args[0]), nil); err != nil {
return fmt.Errorf("deleting media: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/message/media/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func runGet(cmd *cobra.Command, args []string) error {
return err
}

data, err := client.GetRaw(fmt.Sprintf("/users/%s/media/%s", acctID, args[0]))
data, err := client.GetRaw(cmd.Context(), fmt.Sprintf("/users/%s/media/%s", acctID, args[0]))
if err != nil {
return fmt.Errorf("downloading media: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/message/media/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func runList(cmd *cobra.Command, args []string) error {
}

var result interface{}
if err := client.Get(fmt.Sprintf("/users/%s/media", acctID), &result); err != nil {
if err := client.Get(cmd.Context(), fmt.Sprintf("/users/%s/media", acctID), &result); err != nil {
return fmt.Errorf("listing media: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/message/media/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func runUpload(cmd *cobra.Command, args []string) error {
return err
}

if err := client.PutRaw(fmt.Sprintf("/users/%s/media/%s", acctID, mediaID), data, ct); err != nil {
if err := client.PutRaw(cmd.Context(), fmt.Sprintf("/users/%s/media/%s", acctID, mediaID), data, ct); err != nil {
return fmt.Errorf("uploading media: %w", err)
}

Expand Down
Loading
Loading