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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ make release-snapshot # Local release build via goreleaser
- Generate mocks with mockery v3 (testify template) for non-trivial interfaces. Hand-rolled fakes for single-method interfaces or function-type parameters. See DEVGUIDE Mocks section
- Re-use existing patterns. Prefer refactoring over copying
- `dry/log` for internal logging (`log.Warnf` for soft failures)
- Humanize table cells with `dry/tui/humanize` (`Time`, `Duration`). Never a local formatter. New value shapes extend the dry package. See DEVGUIDE Output section

For any changes in `.github/workflows/`:

Expand Down
14 changes: 14 additions & 0 deletions docs/DEVGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ Operational commands (no structured result) call `dry/tui` directly and do not i
- No direct use of `fmt.Println`, `fmt.Printf`, `os.Stdout`, or `os.Stderr` outside `internal/tui` and `dry/tui` call sites in commands. **(lint, depguard)** The single allowed exception is the top-level error path in `cmd/safedep/main.go`, which writes the fatal error to stderr before exit.
- Reusable visual components (tables, banners, diffs, badges, spinners, progress) come from `dry/tui` sub-packages. Do not reimplement.

### Humanized values

`RenderTable` humanizes machine values through [`dry/tui/humanize`](https://github.com/safedep/dry/tree/main/tui/humanize):

- `humanize.Time(t, now)` for instants: `5m ago`, `in 3d`, an absolute UTC date beyond 30 days.
- `humanize.Duration(d)` for durations: `45s`, `1h30m`, `1d12h`.

Rules:

- Only `RenderTable` humanizes. `RenderPlain` and `RenderJSON` keep exact values (RFC 3339 instants, raw durations), so pipelines and agents never parse human text.
- Do not write a local time or duration formatter in a command package. Every relative-time cell in the CLI reads the same because they all go through the one package.
- Call sites pass `time.Now()` at render time. The humanize functions themselves are pure and take `now` as a parameter.
- Extension path: a value shape with no humanizer yet (bytes, counts, rates) gets a new pure function in `dry/tui/humanize` via a PR to [safedep/dry](https://github.com/safedep/dry), then a dry dependency bump here. Follow the existing style: compact output, at most two units of precision, table-driven tests next to the function. Only a formatter that is truly specific to one command (encodes its domain wording, not a general value shape) may live next to that command.

## Authentication

- API key (data plane): `a.DataPlane()`.
Expand Down
2 changes: 1 addition & 1 deletion docs/cmd/integration-bitbucket-link-create.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ safedep integration bitbucket link create --output json | jq -r '.link_code'
| Field | Description |
|-------|-------------|
| `link_code` | The code to paste into the workspace's SafeDep settings page in Bitbucket. |
| `expires_at` | The instant the code expires, RFC 3339 UTC. |
| `expires_at` | The instant the code expires, RFC 3339 UTC, in the plain and JSON output. The table shows the remaining lifetime instead, as `in 15m`. |

## Authentication

Expand Down
26 changes: 15 additions & 11 deletions internal/cmd/integration/bitbucket/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/services/controltower/v1"
"github.com/safedep/dry/tui/humanize"
"github.com/safedep/dry/tui/table"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -80,26 +81,29 @@ func (r *linkCreateResult) RenderJSON() ([]byte, error) {
}

func (r *linkCreateResult) RenderPlain() string {
return "link_code\texpires_at\n" + strings.Join(r.cells(), "\t")
expiresAt := ""
if r.expiresAt != nil {
expiresAt = r.expiresAt.Format(time.RFC3339)
}
return "link_code\texpires_at\n" + r.linkCode + "\t" + expiresAt
}

func (r *linkCreateResult) RenderTable() string {
// The code lives for minutes, so the table shows the remaining lifetime
// via the shared humanizer, like auth status and the list commands. The
// plain and JSON output keep the exact instant.
expires := ""
if r.expiresAt != nil {
expires = humanize.Time(*r.expiresAt, time.Now())
}
return table.New().
Title("Bitbucket workspace link code").
Headers("LINK CODE", "EXPIRES AT").
Rows(r.cells()).
Headers("LINK CODE", "EXPIRES").
Rows([]string{r.linkCode, expires}).
Footer("Paste the code into the workspace's SafeDep settings page in Bitbucket before it expires.").
Render()
}

func (r *linkCreateResult) cells() []string {
expiresAt := ""
if r.expiresAt != nil {
expiresAt = r.expiresAt.Format(time.RFC3339)
}
return []string{r.linkCode, expiresAt}
}

type linkListResult struct {
links []bitbucketlink.Link
}
Expand Down
Loading