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
29 changes: 29 additions & 0 deletions .github/actions/setup-go/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Setup Go
description: 'Sets up Go environment with private modules'
inputs:
stainless-api-key:
required: false
description: the value of the STAINLESS_API_KEY secret
runs:
using: composite
steps:
- uses: stainless-api/retrieve-github-access-token@v1
if: github.repository == 'stainless-sdks/y2-cli'
id: get_token
with:
repo: stainless-sdks/y2-go
stainless-api-key: ${{ inputs.stainless-api-key }}

- name: Configure Git for access to the Go SDK's staging repo
if: github.repository == 'stainless-sdks/y2-cli'
shell: bash
run: git config --global url."https://x-access-token:${{ steps.get_token.outputs.github_access_token }}@github.com/stainless-sdks/y2-go".insteadOf "https://github.com/stainless-sdks/y2-go"

- name: Setup go
uses: actions/setup-go@v5
with:
go-version-file: ./go.mod

- name: Bootstrap
shell: bash
run: ./scripts/bootstrap
55 changes: 38 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
branches:
- '**'
- '!integrated/**'
- '!stl-preview-head/**'
- '!stl-preview-base/**'
- '!generated'
- '!codegen/**'
- 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
- 'stl-preview-base/**'

env:
GOPRIVATE: github.com/y2-intel/y2-go,github.com/stainless-sdks/y2-go

jobs:
lint:
timeout-minutes: 10
Expand All @@ -22,10 +27,14 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: Setup go
uses: actions/setup-go@v5
- uses: ./.github/actions/setup-go
with:
go-version-file: ./go.mod
stainless-api-key: ${{ secrets.STAINLESS_API_KEY }}

- name: Link staging branch
if: github.repository == 'stainless-sdks/y2-cli'
run: |
./scripts/link 'github.com/stainless-sdks/y2-go@${{ github.ref_name }}' || true

- name: Bootstrap
run: ./scripts/bootstrap
Expand All @@ -44,10 +53,14 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: Setup go
uses: actions/setup-go@v5
- uses: ./.github/actions/setup-go
with:
go-version-file: ./go.mod
stainless-api-key: ${{ secrets.STAINLESS_API_KEY }}

- name: Link staging branch
if: github.repository == 'stainless-sdks/y2-cli'
run: |
./scripts/link 'github.com/stainless-sdks/y2-go@${{ github.ref_name }}' || true

- name: Bootstrap
run: ./scripts/bootstrap
Expand All @@ -61,14 +74,18 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Get GitHub OIDC Token
if: github.repository == 'stainless-sdks/y2-cli'
if: |-
github.repository == 'stainless-sdks/y2-cli' &&
!startsWith(github.ref, 'refs/heads/stl/')
id: github-oidc
uses: actions/github-script@v8
with:
script: core.setOutput('github_token', await core.getIDToken());

- name: Upload tarball
if: github.repository == 'stainless-sdks/y2-cli'
if: |-
github.repository == 'stainless-sdks/y2-cli' &&
!startsWith(github.ref, 'refs/heads/stl/')
env:
URL: https://pkg.stainless.com/s
AUTH: ${{ steps.github-oidc.outputs.github_token }}
Expand All @@ -83,10 +100,14 @@ jobs:
steps:
- uses: actions/checkout@v6

- name: Setup go
uses: actions/setup-go@v5
- uses: ./.github/actions/setup-go
with:
go-version-file: ./go.mod
stainless-api-key: ${{ secrets.STAINLESS_API_KEY }}

- name: Link staging branch
if: github.repository == 'stainless-sdks/y2-cli'
run: |
./scripts/link 'github.com/stainless-sdks/y2-go@${{ github.ref_name }}' || true

- name: Bootstrap
run: ./scripts/bootstrap
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.prism.log
.stdy.log
dist/
/y2
*.exe
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.6.0"
".": "0.7.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 30
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/jedi-labs%2Fy2-2f9dde595a9741f91d2066b6912882a5814e1ea78b54bd2dabaa185f09b153e8.yml
openapi_spec_hash: 5af9f2fd5689a97d833b5cfb13b9558d
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/jedi-labs%2Fy2-4f7cb206ea938ef34326b6d48ceec8bf5edbb78826c0c3403551aa2693d56c2b.yml
openapi_spec_hash: fc1b2b74950aab022d89af6e70b0a4e2
config_hash: b16cfc4ab05ceee84810166970a3d683
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Changelog

## 0.7.0 (2026-03-24)

Full Changelog: [v0.6.0...v0.7.0](https://github.com/y2-intel/y2-cli/compare/v0.6.0...v0.7.0)

### Features

* add default description for enum CLI flags without an explicit description ([cae6e8e](https://github.com/y2-intel/y2-cli/commit/cae6e8e0cf7f370f7ad50bc7ac35fb4ba70f90b9))


### Bug Fixes

* avoid reading from stdin unless request body is form encoded or json ([0e4c7ff](https://github.com/y2-intel/y2-cli/commit/0e4c7ff960fc9c6ed75c35771e208c65ac693616))
* better support passing client args in any position ([f39af5c](https://github.com/y2-intel/y2-cli/commit/f39af5c23769579b608d7acc1bd3d13a1b01a563))
* cli no longer hangs when stdin is attached to a pipe with empty input ([6a14372](https://github.com/y2-intel/y2-cli/commit/6a143721fbb81b7cce43d0fb603c4bf2a89f829f))
* fix for test cases with newlines in YAML and better error reporting ([e48f86f](https://github.com/y2-intel/y2-cli/commit/e48f86f78b72e6f328503427ce3979c509fc0dab))
* improve linking behavior when developing on a branch not in the Go SDK ([31eaf83](https://github.com/y2-intel/y2-cli/commit/31eaf83bb47abcf4def03988f2c58e24a29db275))
* improved workflow for developing on branches ([5ed568c](https://github.com/y2-intel/y2-cli/commit/5ed568c761725544ac69f02003ad1a783052788e))
* no longer require an API key when building on production repos ([7f85ba7](https://github.com/y2-intel/y2-cli/commit/7f85ba7fecc326e1b5d9588272e79b5aa70cbbf3))
* only set client options when the corresponding CLI flag or env var is explicitly set ([159a1d5](https://github.com/y2-intel/y2-cli/commit/159a1d51c58eaa04fb91f550388f269b3ab19c95))


### Chores

* **ci:** skip uploading artifacts on stainless-internal branches ([2e72da4](https://github.com/y2-intel/y2-cli/commit/2e72da41491d4480f2ab09d8cdc9657907bb87d8))
* **internal:** tweak CI branches ([c581a0e](https://github.com/y2-intel/y2-cli/commit/c581a0e47b0be4c2a30d2a4a6b98ecb6c06874b5))
* **internal:** update gitignore ([372e829](https://github.com/y2-intel/y2-cli/commit/372e829a974682f751ec67976577e3897b6343ba))

## 0.6.0 (2026-03-07)

Full Changelog: [v0.5.0...v0.6.0](https://github.com/y2-intel/y2-cli/compare/v0.5.0...v0.6.0)
Expand Down
8 changes: 8 additions & 0 deletions internal/requestflag/requestflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,14 @@ func (f *Flag[T]) Count() int {
return f.count
}

// Implementation for the cli.LocalFlag interface
var _ cli.LocalFlag = (*Flag[any])(nil) // Type assertion to ensure interface compliance

func (f Flag[T]) IsLocal() bool {
// By default, all request flags are local, i.e. can be provided at any part of the CLI command.
return true
}

// cliValue is a generic implementation of cli.Value for common types
type cliValue[
T []any | []map[string]any | []DateTimeValue | []DateValue | []TimeValue | []string | []float64 |
Expand Down
34 changes: 31 additions & 3 deletions pkg/cmd/cmdutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption {
option.WithHeader("X-Stainless-Package-Version", Version),
option.WithHeader("X-Stainless-Runtime", "cli"),
option.WithHeader("X-Stainless-CLI-Command", cmd.FullName()),
option.WithAPIKey(cmd.String("api-key")),
}
if cmd.IsSet("api-key") {
opts = append(opts, option.WithAPIKey(cmd.String("api-key")))
}

// Override base URL if the --base-url flag is provided
Expand Down Expand Up @@ -68,9 +70,35 @@ var debugMiddlewareOption = option.WithMiddleware(
},
)

// isInputPiped tries to check for input being piped into the CLI which tells us that we should try to read
// from stdin. This can be a bit tricky in some cases like when an stdin is connected to a pipe but nothing is
// being piped in (this may happen in some environments like Cursor's integration terminal or CI), which is
// why this function is a little more elaborate than it'd be otherwise.
func isInputPiped() bool {
stat, _ := os.Stdin.Stat()
return (stat.Mode() & os.ModeCharDevice) == 0
stat, err := os.Stdin.Stat()
if err != nil {
return false
}

mode := stat.Mode()

// Regular file (redirect like < file.txt) — only if non-empty.
//
// Notably, on Unix the case like `< /dev/null` is handled below because `/dev/null` is not a regular
// file. On Windows, NUL appears as a regular file with size 0, so it's also handled correctly.
if mode.IsRegular() && stat.Size() > 0 {
return true
}

// For pipes/sockets (e.g. `echo foo | stainlesscli`), use an OS-specific check to determine whether
// data is actually available. Some environments like Cursor's integrated terminal connect stdin as a
// pipe even when nothing is being piped.
if mode&(os.ModeNamedPipe|os.ModeSocket) != 0 {
// Defined in either cmdutil_unix.go or cmdutil_windows.go.
return isPipedDataAvailableOSSpecific()
}

return false
}

func isTerminal(w io.Writer) bool {
Expand Down
11 changes: 11 additions & 0 deletions pkg/cmd/cmdutil_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ import (
"golang.org/x/sys/unix"
)

func isPipedDataAvailableOSSpecific() bool {
// Try to determine if there's non-empty data being piped into the command by polling for data for a short
// amount of time. This is necessary because some environments (e.g. Cursor's integrated terminal) connect
// stdin as a pipe even when nothing is being piped, which would cause the command to block indefinitely
// waiting for input that will never come. The 10 ms timeout is arbitrary -- designed to be long enough to
// allow data to be detected, but short enough that it shouldn't cause a noticeable delay in command runs.
fds := []unix.PollFd{{Fd: int32(os.Stdin.Fd()), Events: unix.POLLIN}}
n, _ := unix.Poll(fds, 10 /* ms */)
return n > 0
}

func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error {
// Try to use socket pair for better buffer control
pagerInput, pid, err := openSocketPairPager(label)
Expand Down
26 changes: 25 additions & 1 deletion pkg/cmd/cmdutil_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,31 @@

package cmd

import "os"
import (
"os"
"syscall"
"unsafe"
)

var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procPeekNamedPipe = kernel32.NewProc("PeekNamedPipe")
)

func isPipedDataAvailableOSSpecific() bool {
// On Windows, unix.Poll is not available. Use PeekNamedPipe to check if data is available
// on the pipe without consuming it.
var available uint32
r, _, _ := procPeekNamedPipe.Call(
os.Stdin.Fd(),
0,
0,
0,
uintptr(unsafe.Pointer(&available)),
0,
)
return r != 0 && available > 0
}

func streamOutputOSSpecific(label string, generateOutput func(w *os.File) error) error {
// We have a trick with sockets that we use when possible on Unix-like systems. Those APIs aren't
Expand Down
25 changes: 13 additions & 12 deletions pkg/cmd/flagoptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,27 +219,28 @@ func flagOptions(

requestContents := requestflag.ExtractRequestContents(cmd)

if bodyType != ApplicationOctetStream && isInputPiped() && !ignoreStdin {
if (bodyType == MultipartFormEncoded || bodyType == ApplicationJSON) && !ignoreStdin && isInputPiped() {
pipeData, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, err
}

if len(pipeData) > 0 {
var bodyData any
if err := yaml.Unmarshal(pipeData, &bodyData); err == nil {
if bodyMap, ok := bodyData.(map[string]any); ok {
if flagMap, ok := requestContents.Body.(map[string]any); ok {
maps.Copy(bodyMap, flagMap)
requestContents.Body = bodyMap
} else {
bodyData = requestContents.Body
}
} else if flagMap, ok := requestContents.Body.(map[string]any); ok && len(flagMap) > 0 {
return nil, fmt.Errorf("Cannot merge flags with a body that is not a map: %v", bodyData)
if err := yaml.Unmarshal(pipeData, &bodyData); err != nil {
return nil, fmt.Errorf("Failed to parse piped data as YAML/JSON:\n%w", err)
}
if bodyMap, ok := bodyData.(map[string]any); ok {
if flagMap, ok := requestContents.Body.(map[string]any); ok {
maps.Copy(bodyMap, flagMap)
requestContents.Body = bodyMap
} else {
requestContents.Body = bodyData
bodyData = requestContents.Body
}
} else if flagMap, ok := requestContents.Body.(map[string]any); ok && len(flagMap) > 0 {
return nil, fmt.Errorf("Cannot merge flags with a body that is not a map: %v", bodyData)
} else {
requestContents.Body = bodyData
}
}
}
Expand Down
9 changes: 6 additions & 3 deletions pkg/cmd/news_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ func TestNewsList(t *testing.T) {
t.Skip("Mock server tests are disabled")
t.Run("regular flags", func(t *testing.T) {
mocktest.TestRunMockTestWithFlags(
t, "news", "list",
t,
"--api-key", "string",
"news", "list",
"--limit", "1",
"--topics", "crypto,ai_agents,bitcoin",
)
Expand All @@ -24,8 +25,9 @@ func TestNewsGetRecaps(t *testing.T) {
t.Skip("Mock server tests are disabled")
t.Run("regular flags", func(t *testing.T) {
mocktest.TestRunMockTestWithFlags(
t, "news", "get-recaps",
t,
"--api-key", "string",
"news", "get-recaps",
"--timeframe", "12h",
"--topics", "topics",
)
Expand All @@ -36,8 +38,9 @@ func TestNewsListFeeds(t *testing.T) {
t.Skip("Mock server tests are disabled")
t.Run("regular flags", func(t *testing.T) {
mocktest.TestRunMockTestWithFlags(
t, "news", "list-feeds",
t,
"--api-key", "string",
"news", "list-feeds",
)
})
}
Loading
Loading