Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Link staging branch
if: github.repository == 'stainless-sdks/x-twitter-scraper-cli'
run: |
./scripts/link 'github.com/stainless-sdks/x-twitter-scraper-go@${{ github.ref_name }}' || true
./scripts/link 'github.com/stainless-sdks/x-twitter-scraper-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/x-twitter-scraper-go'

- name: Bootstrap
run: ./scripts/bootstrap
Expand All @@ -60,7 +60,7 @@ jobs:
- name: Link staging branch
if: github.repository == 'stainless-sdks/x-twitter-scraper-cli'
run: |
./scripts/link 'github.com/stainless-sdks/x-twitter-scraper-go@${{ github.ref_name }}' || true
./scripts/link 'github.com/stainless-sdks/x-twitter-scraper-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/x-twitter-scraper-go'

- name: Bootstrap
run: ./scripts/bootstrap
Expand Down Expand Up @@ -107,7 +107,7 @@ jobs:
- name: Link staging branch
if: github.repository == 'stainless-sdks/x-twitter-scraper-cli'
run: |
./scripts/link 'github.com/stainless-sdks/x-twitter-scraper-go@${{ github.ref_name }}' || true
./scripts/link 'github.com/stainless-sdks/x-twitter-scraper-go@${{ github.ref_name }}' || go mod edit -dropreplace='github.com/stainless-sdks/x-twitter-scraper-go'

- name: Bootstrap
run: ./scripts/bootstrap
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.2.0"
".": "0.3.0"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 115
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/xquik%2Fx-twitter-scraper-3b2c6c771ad1da0bbfeb0af115972929ed2c7fcd5e47a79556d66cd21431b224.yml
openapi_spec_hash: de2890233b68387bf5f9b6d19e7d87dc
configured_endpoints: 102
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/xquik%2Fx-twitter-scraper-cc76e6e9b7122fdacc45fbd7cf4603aaaca4906ae2de0984b51a5fc7cf8dadd6.yml
openapi_spec_hash: 0b1bc061a669d7c77e5bf1476d083a2d
config_hash: 8894c96caeb6df84c9394518810221bd
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## 0.3.0 (2026-04-07)

Full Changelog: [v0.2.0...v0.3.0](https://github.com/Xquik-dev/x-twitter-scraper-cli/compare/v0.2.0...v0.3.0)

### Features

* allow `-` as value representing stdin to binary-only file parameters in CLIs ([712746c](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/712746c9735661862689301f04ff1a90ebac1d8a))
* **api:** api update ([dcbfdc4](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/dcbfdc4f2bef587eb67db7d75bde62aaac8cb586))
* **api:** api update ([2939f30](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/2939f308c9443e7e0fe53e1ac47f43413b6d78a5))
* **api:** api update ([600dade](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/600dadedbb67605a0619ff305b6859ac2d2b22f2))
* better error message if scheme forgotten in CLI `*_BASE_URL`/`--base-url` ([07a4005](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/07a40054367cdbd2ebba05fc271f0f8924adc3b3))
* binary-only parameters become CLI flags that take filenames only ([ce4ff99](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/ce4ff99c2fd457bd041238b36255282dec76aa1a))


### Bug Fixes

* fall back to main branch if linking fails in CI ([b1a0aa8](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/b1a0aa8e01818c582ba1608fd254f5f7872a9394))
* fix quoting typo ([f82cba5](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/f82cba546262865b22af17ece6aa426fe3c9b0db))
* handle empty data set using `--format explore` ([fb087bb](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/fb087bb19f7783f5c6b0d34a6c800da8d6ff6337))
* use `RawJSON` when iterating items with `--format explore` in the CLI ([9b49da1](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/9b49da128f9110d987a9174a6fa84419ae337e9b))


### Chores

* mark all CLI-related tests in Go with `t.Parallel()` ([273bb76](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/273bb76c4ca2923aaf4ca99ad527b1ebb93f6be9))
* modify CLI tests to inject stdout so mutating `os.Stdout` isn't necessary ([2da8d2f](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/2da8d2f9f31119189bbf53ffae8b92817959bb2d))
* switch some CLI Go tests from `os.Chdir` to `t.Chdir` ([445ffb4](https://github.com/Xquik-dev/x-twitter-scraper-cli/commit/445ffb42a38c6b8cb90a18dbf377e1949e1b0fde))

## 0.2.0 (2026-04-01)

Full Changelog: [v0.1.0...v0.2.0](https://github.com/Xquik-dev/x-twitter-scraper-cli/compare/v0.1.0...v0.2.0)
Expand Down
7 changes: 7 additions & 0 deletions cmd/x-twitter-scraper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ func main() {
prepareForAutocomplete(app)
}

if baseURL, ok := os.LookupEnv("X_TWITTER_SCRAPER_BASE_URL"); ok {
if err := cmd.ValidateBaseURL(baseURL, "X_TWITTER_SCRAPER_BASE_URL"); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err.Error())
os.Exit(1)
}
}

if err := app.Run(context.Background(), os.Args); err != nil {
exitCode := 1

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/Xquik-dev/x-twitter-scraper-cli
go 1.25

require (
github.com/Xquik-dev/x-twitter-scraper-go v0.1.0
github.com/Xquik-dev/x-twitter-scraper-go v0.2.0
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/lipgloss v1.1.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/Xquik-dev/x-twitter-scraper-go v0.1.0 h1:7pI1R5oOjgMDKl86ItIM8meUOxtwcmgGVjHctRNXXXo=
github.com/Xquik-dev/x-twitter-scraper-go v0.1.0/go.mod h1:OHW3aIR8E3+ANa/mjFTZs1sG7ePzrBEmW0a8JUN+NvI=
github.com/Xquik-dev/x-twitter-scraper-go v0.2.0 h1:WEn0e9rZEQ+m82tPTuksxhluADV1Rjpj0zJ2LTrQRvs=
github.com/Xquik-dev/x-twitter-scraper-go v0.2.0/go.mod h1:OHW3aIR8E3+ANa/mjFTZs1sG7ePzrBEmW0a8JUN+NvI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
Expand Down
4 changes: 4 additions & 0 deletions internal/apiform/form_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,12 @@ var tests = map[string]struct {
}

func TestEncode(t *testing.T) {
t.Parallel()

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

buf := bytes.NewBuffer(nil)
writer := multipart.NewWriter(buf)
writer.SetBoundary("xxx")
Expand Down
4 changes: 4 additions & 0 deletions internal/apiquery/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
)

func TestEncode(t *testing.T) {
t.Parallel()

tests := map[string]struct {
val any
settings QuerySettings
Expand Down Expand Up @@ -114,6 +116,8 @@ func TestEncode(t *testing.T) {

for name, test := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

query := map[string]any{"query": test.val}
values, err := MarshalWithSettings(query, test.settings)
if err != nil {
Expand Down
40 changes: 40 additions & 0 deletions internal/autocomplete/autocomplete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
)

func TestGetCompletions_EmptyArgs(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "generate", Usage: "Generate SDK"},
Expand All @@ -26,6 +28,8 @@ func TestGetCompletions_EmptyArgs(t *testing.T) {
}

func TestGetCompletions_SubcommandPrefix(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "generate", Usage: "Generate SDK"},
Expand All @@ -43,6 +47,8 @@ func TestGetCompletions_SubcommandPrefix(t *testing.T) {
}

func TestGetCompletions_HiddenCommand(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "visible", Usage: "Visible command"},
Expand All @@ -57,6 +63,8 @@ func TestGetCompletions_HiddenCommand(t *testing.T) {
}

func TestGetCompletions_NestedSubcommand(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand All @@ -79,6 +87,8 @@ func TestGetCompletions_NestedSubcommand(t *testing.T) {
}

func TestGetCompletions_FlagCompletion(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand All @@ -102,6 +112,8 @@ func TestGetCompletions_FlagCompletion(t *testing.T) {
}

func TestGetCompletions_ShortFlagCompletion(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand All @@ -123,6 +135,8 @@ func TestGetCompletions_ShortFlagCompletion(t *testing.T) {
}

func TestGetCompletions_FileFlagBehavior(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand All @@ -142,6 +156,8 @@ func TestGetCompletions_FileFlagBehavior(t *testing.T) {
}

func TestGetCompletions_NonBoolFlagValue(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand All @@ -161,6 +177,8 @@ func TestGetCompletions_NonBoolFlagValue(t *testing.T) {
}

func TestGetCompletions_BoolFlagDoesNotBlockCompletion(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand All @@ -185,6 +203,8 @@ func TestGetCompletions_BoolFlagDoesNotBlockCompletion(t *testing.T) {
}

func TestGetCompletions_ColonCommands_NoColonTyped(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "config:get", Usage: "Get config value"},
Expand All @@ -202,6 +222,8 @@ func TestGetCompletions_ColonCommands_NoColonTyped(t *testing.T) {
}

func TestGetCompletions_ColonCommands_ColonTyped_Bash(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "config:get", Usage: "Get config value"},
Expand All @@ -221,6 +243,8 @@ func TestGetCompletions_ColonCommands_ColonTyped_Bash(t *testing.T) {
}

func TestGetCompletions_ColonCommands_ColonTyped_Zsh(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "config:get", Usage: "Get config value"},
Expand All @@ -240,6 +264,8 @@ func TestGetCompletions_ColonCommands_ColonTyped_Zsh(t *testing.T) {
}

func TestGetCompletions_BashStyleColonCompletion(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "config:get", Usage: "Get config value"},
Expand All @@ -257,6 +283,8 @@ func TestGetCompletions_BashStyleColonCompletion(t *testing.T) {
}

func TestGetCompletions_BashStyleColonCompletion_NoMatch(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "config:get", Usage: "Get config value"},
Expand All @@ -271,6 +299,8 @@ func TestGetCompletions_BashStyleColonCompletion_NoMatch(t *testing.T) {
}

func TestGetCompletions_ZshStyleColonCompletion(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "config:get", Usage: "Get config value"},
Expand All @@ -287,6 +317,8 @@ func TestGetCompletions_ZshStyleColonCompletion(t *testing.T) {
}

func TestGetCompletions_MixedColonAndRegularCommands(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "generate", Usage: "Generate SDK"},
Expand All @@ -305,6 +337,8 @@ func TestGetCompletions_MixedColonAndRegularCommands(t *testing.T) {
}

func TestGetCompletions_FlagWithBoolFlagSkipsValue(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand All @@ -329,6 +363,8 @@ func TestGetCompletions_FlagWithBoolFlagSkipsValue(t *testing.T) {
}

func TestGetCompletions_MultipleFlagsBeforeSubcommand(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand All @@ -353,6 +389,8 @@ func TestGetCompletions_MultipleFlagsBeforeSubcommand(t *testing.T) {
}

func TestGetCompletions_CommandAliases(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{Name: "generate", Aliases: []string{"gen", "g"}, Usage: "Generate SDK"},
Expand All @@ -372,6 +410,8 @@ func TestGetCompletions_CommandAliases(t *testing.T) {
}

func TestGetCompletions_AllFlagsWhenNoPrefix(t *testing.T) {
t.Parallel()

root := &cli.Command{
Commands: []*cli.Command{
{
Expand Down
38 changes: 35 additions & 3 deletions internal/jsonview/explorer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jsonview

import (
"bytes"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -309,6 +310,10 @@ func ExploreJSON(title string, json gjson.Result) error {
return err
}

type hasRawJSON interface {
RawJSON() string
}

// ExploreJSONStream explores JSON data loaded incrementally via an iterator
func ExploreJSONStream[T any](title string, it Iterator[T]) error {
anyIt := genericToAnyIterator(it)
Expand All @@ -327,12 +332,12 @@ func ExploreJSONStream[T any](title string, it Iterator[T]) error {
return err
}

// Convert items to JSON array
jsonBytes, err := json.Marshal(items)
arrayJSONBytes, err := marshalItemsToJSONArray(items)
if err != nil {
return err
}
arrayJSON := gjson.ParseBytes(jsonBytes)

arrayJSON := gjson.ParseBytes(arrayJSONBytes)
view, err := newTableView("", arrayJSON, false)
if err != nil {
return err
Expand All @@ -352,6 +357,29 @@ func ExploreJSONStream[T any](title string, it Iterator[T]) error {
return err
}

func marshalItemsToJSONArray(items []any) ([]byte, error) {
var buf bytes.Buffer
buf.WriteByte('[')

for i, item := range items {
if i > 0 {
buf.WriteByte(',')
}
if hasRaw, ok := item.(hasRawJSON); ok {
buf.WriteString(hasRaw.RawJSON())
} else {
jsonData, err := json.Marshal(item)
if err != nil {
return nil, err
}
buf.Write(jsonData)
}
}

buf.WriteByte(']')
return buf.Bytes(), nil
}

func (v *JSONViewer) current() JSONView { return v.stack[len(v.stack)-1] }
func (v *JSONViewer) Init() tea.Cmd { return nil }

Expand Down Expand Up @@ -406,6 +434,10 @@ func (v *JSONViewer) navigateForward() (tea.Model, tea.Cmd) {
return v, nil
}

if len(tableView.rowData) < 1 {
return v, nil
}

cursor := tableView.table.Cursor()
selected := tableView.rowData[cursor]
if !v.canNavigateInto(selected) {
Expand Down
Loading
Loading