From 2aa453f3c0cc4191329ade9a8c8b2b328eca97e1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:06:25 +0000 Subject: [PATCH 1/5] feat(api): manual updates --- .github/workflows/ci.yml | 34 ++ .github/workflows/publish-release.yml | 31 ++ .github/workflows/release-doctor.yml | 19 + .gitignore | 3 + .goreleaser.yml | 80 ++++ .release-please-manifest.json | 3 + .stats.yml | 4 + LICENSE | 201 +++++++++ README.md | 45 +- SECURITY.md | 23 + bin/check-release-environment | 17 + cmd/hypeman/main.go | 35 ++ go.mod | 39 ++ go.sum | 80 ++++ pkg/cmd/cmd.go | 184 ++++++++ pkg/cmd/health.go | 42 ++ pkg/cmd/image.go | 153 +++++++ pkg/cmd/instance.go | 406 ++++++++++++++++++ pkg/cmd/instancevolume.go | 126 ++++++ pkg/cmd/util.go | 254 +++++++++++ pkg/cmd/version.go | 5 + pkg/cmd/volume.go | 169 ++++++++ pkg/jsonflag/json_flag.go | 248 +++++++++++ pkg/jsonflag/mutation.go | 104 +++++ pkg/jsonflag/mutation_test.go | 37 ++ pkg/jsonview/explorer.go | 588 ++++++++++++++++++++++++++ pkg/jsonview/staticdisplay.go | 139 ++++++ release-please-config.json | 67 +++ scripts/bootstrap | 24 ++ scripts/format | 8 + scripts/link | 16 + scripts/lint | 8 + scripts/mock | 41 ++ scripts/unlink | 8 + 34 files changed, 3240 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish-release.yml create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 .release-please-manifest.json create mode 100644 .stats.yml create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 bin/check-release-environment create mode 100644 cmd/hypeman/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/cmd/cmd.go create mode 100644 pkg/cmd/health.go create mode 100644 pkg/cmd/image.go create mode 100644 pkg/cmd/instance.go create mode 100644 pkg/cmd/instancevolume.go create mode 100644 pkg/cmd/util.go create mode 100644 pkg/cmd/version.go create mode 100644 pkg/cmd/volume.go create mode 100644 pkg/jsonflag/json_flag.go create mode 100644 pkg/jsonflag/mutation.go create mode 100644 pkg/jsonflag/mutation_test.go create mode 100644 pkg/jsonview/explorer.go create mode 100644 pkg/jsonview/staticdisplay.go create mode 100644 release-please-config.json create mode 100755 scripts/bootstrap create mode 100755 scripts/format create mode 100755 scripts/link create mode 100755 scripts/lint create mode 100755 scripts/mock create mode 100755 scripts/unlink diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..697b7ff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI +on: + push: + branches-ignore: + - 'generated' + - 'codegen/**' + - 'integrated/**' + - 'stl-preview-head/**' + - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' + +jobs: + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ github.repository == 'stainless-sdks/hypeman-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork + + steps: + - uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run lints + run: ./scripts/lint diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml new file mode 100644 index 0000000..2634ef5 --- /dev/null +++ b/.github/workflows/publish-release.yml @@ -0,0 +1,31 @@ +--- +name: Publish Release +permissions: + contents: write + +concurrency: + group: publish + +on: + push: + tags: + - "v*" +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6.1.0 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..e1356d2 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,19 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'onkernel/hypeman-cli' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@v4 + + - name: Check release environment + run: | + bash ./bin/check-release-environment diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8d1ec6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.prism.log +dist/ +/hypeman diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..46a2f46 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,80 @@ +project_name: hypeman +version: 2 + +before: + hooks: + - mkdir -p completions + - sh -c "go run ./cmd/hypeman/main.go @completion bash > completions/hypeman.bash" + - sh -c "go run ./cmd/hypeman/main.go @completion zsh > completions/hypeman.zsh" + - sh -c "go run ./cmd/hypeman/main.go @completion fish > completions/hypeman.fish" + - sh -c "go run ./cmd/hypeman/main.go @manpages -o man" + +builds: + - id: macos + goos: [darwin] + goarch: [amd64, arm64] + binary: '{{ .ProjectName }}' + main: ./cmd/hypeman/main.go + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + + - id: linux + goos: [linux] + goarch: ['386', arm, amd64, arm64] + env: + - CGO_ENABLED=0 + binary: '{{ .ProjectName }}' + main: ./cmd/hypeman/main.go + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + + - id: windows + goos: [windows] + goarch: ['386', amd64, arm64] + binary: '{{ .ProjectName }}' + main: ./cmd/hypeman/main.go + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + +archives: + - id: linux-archive + ids: [linux] + name_template: '{{ .ProjectName }}_{{ .Version }}_linux_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + formats: [tar.gz] + files: + - completions/* + - man/*/* + - id: macos-archive + ids: [macos] + name_template: '{{ .ProjectName }}_{{ .Version }}_macos_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + formats: [zip] + files: + - completions/* + - man/*/* + - id: windows-archive + ids: [windows] + name_template: '{{ .ProjectName }}_{{ .Version }}_windows_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + formats: [zip] + files: + - completions/* + - man/*/* + +snapshot: + version_template: '{{ .Tag }}-next' + +nfpms: + - license: Apache-2.0 + maintainer: + bindir: /usr + formats: + - apk + - deb + - rpm + - termux.deb + - archlinux + contents: + - src: man/man1/*.1.gz + dst: /usr/share/man/man1/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..1332969 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..91c6268 --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 18 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-7c27e323412e72166bce2de104f1bf82b57197e05b686e94cd81d07e288bd558.yml +openapi_spec_hash: 4656d2b318d04a9fec0210897d76b505 +config_hash: 8d56572492f9a13ba410b9beddf2c57d diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e9bf84 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Hypeman + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 1d9aca1..d8490da 100644 --- a/README.md +++ b/README.md @@ -1 +1,44 @@ -# hypeman-cli \ No newline at end of file +# Hypeman CLI + +The official CLI for the Hypeman REST API. + +It is generated with [Stainless](https://www.stainless.com/). + +## Installation + +### Installing with Go + + + +```sh +go install 'github.com/onkernel/hypeman-cli/cmd/hypeman@latest' +``` + +### Running Locally + + + +```sh +go run cmd/hypeman/main.go +``` + + + +## Usage + +The CLI follows a resource-based command structure: + +```sh +hypeman [resource] [command] [flags] +``` + +```sh +hypeman health check +``` + +For details about specific commands, use the `--help` flag. + +## Global Flags + +- `--debug` - Enable debug logging (includes HTTP request/response details) +- `--version`, `-v` - Show the CLI version diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..94a5b00 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Reporting Security Issues + +This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. + +To report a security issue, please contact the Stainless team at security@stainless.com. + +## Responsible Disclosure + +We appreciate the efforts of security researchers and individuals who help us maintain the security of +SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible +disclosure practices by allowing us a reasonable amount of time to investigate and address the issue +before making any information public. + +## Reporting Non-SDK Related Security Issues + +If you encounter security issues that are not directly related to SDKs but pertain to the services +or products provided by Hypeman, please follow the respective company's security reporting guidelines. + +--- + +Thank you for helping us keep the SDKs and systems they interact with secure. diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..1e951e9 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +errors=() + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/cmd/hypeman/main.go b/cmd/hypeman/main.go new file mode 100644 index 0000000..cc68416 --- /dev/null +++ b/cmd/hypeman/main.go @@ -0,0 +1,35 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package main + +import ( + "context" + "errors" + "fmt" + "https://github.com/stainless-sdks/hypeman-go" + "net/http" + "os" + + "github.com/onkernel/hypeman-cli/pkg/cmd" + "github.com/tidwall/gjson" +) + +func main() { + app := cmd.Command + if err := app.Run(context.Background(), os.Args); err != nil { + var apierr *hypeman.Error + if errors.As(err, &apierr) { + fmt.Fprintf(os.Stderr, "%s %q: %d %s\n", apierr.Request.Method, apierr.Request.URL, apierr.Response.StatusCode, http.StatusText(apierr.Response.StatusCode)) + format := app.String("format-error") + json := gjson.Parse(apierr.RawJSON()) + show_err := cmd.ShowJSON("Error", json, format, app.String("transform-error")) + if show_err != nil { + // Just print the original error: + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } + } else { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cf17680 --- /dev/null +++ b/go.mod @@ -0,0 +1,39 @@ +module github.com/onkernel/hypeman-cli + +go 1.25 + +require ( + github.com/charmbracelet/bubbles v0.21.0 + github.com/charmbracelet/bubbletea v1.3.6 + github.com/charmbracelet/lipgloss v1.1.0 + github.com/charmbracelet/x/term v0.2.1 + github.com/muesli/reflow v0.3.0 + github.com/tidwall/gjson v1.18.0 + github.com/tidwall/sjson v1.2.5 + github.com/urfave/cli-docs/v3 v3.0.0-alpha6 + github.com/urfave/cli/v3 v3.3.2 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.3.8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..118b317 --- /dev/null +++ b/go.sum @@ -0,0 +1,80 @@ +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= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= +github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o= +github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go new file mode 100644 index 0000000..12584bc --- /dev/null +++ b/pkg/cmd/cmd.go @@ -0,0 +1,184 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "compress/gzip" + "context" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + docs "github.com/urfave/cli-docs/v3" + "github.com/urfave/cli/v3" +) + +var ( + Command *cli.Command + OutputFormats = []string{"auto", "explore", "json", "pretty", "raw", "yaml"} +) + +func init() { + Command = &cli.Command{ + Name: "hypeman", + Usage: "CLI for the hypeman API", + Version: Version, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug logging", + }, + &cli.StringFlag{ + Name: "base-url", + DefaultText: "url", + Usage: "Override the base URL for API requests", + }, + &cli.StringFlag{ + Name: "format", + Usage: "The format for displaying response data (one of: " + strings.Join(OutputFormats, ", ") + ")", + Value: "auto", + Validator: func(format string) error { + if !slices.Contains(OutputFormats, strings.ToLower(format)) { + return fmt.Errorf("format must be one of: %s", strings.Join(OutputFormats, ", ")) + } + return nil + }, + }, + &cli.StringFlag{ + Name: "format-error", + Usage: "The format for displaying error data (one of: " + strings.Join(OutputFormats, ", ") + ")", + Value: "auto", + Validator: func(format string) error { + if !slices.Contains(OutputFormats, strings.ToLower(format)) { + return fmt.Errorf("format must be one of: %s", strings.Join(OutputFormats, ", ")) + } + return nil + }, + }, + &cli.StringFlag{ + Name: "transform", + Usage: "The GJSON transformation for data output.", + }, + &cli.StringFlag{ + Name: "transform-error", + Usage: "The GJSON transformation for errors.", + }, + }, + Commands: []*cli.Command{ + { + Name: "health", + Category: "API RESOURCE", + Commands: []*cli.Command{ + &healthCheck, + }, + }, + { + Name: "images", + Category: "API RESOURCE", + Commands: []*cli.Command{ + &imagesCreate, + &imagesRetrieve, + &imagesList, + }, + }, + { + Name: "instances", + Category: "API RESOURCE", + Commands: []*cli.Command{ + &instancesCreate, + &instancesRetrieve, + &instancesList, + &instancesPutInStandby, + &instancesRestoreFromStandby, + }, + }, + { + Name: "instances:volumes", + Category: "API RESOURCE", + Commands: []*cli.Command{ + &instancesVolumesAttach, + &instancesVolumesDetach, + }, + }, + { + Name: "volumes", + Category: "API RESOURCE", + Commands: []*cli.Command{ + &volumesCreate, + &volumesRetrieve, + &volumesList, + }, + }, + { + Name: "@manpages", + Usage: "Generate documentation for 'man'", + UsageText: "hypeman @manpages [-o hypeman.1] [--gzip]", + Hidden: true, + Action: generateManpages, + HideHelpCommand: true, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "write manpages to the given folder", + Value: "man", + }, + &cli.BoolFlag{ + Name: "gzip", + Aliases: []string{"z"}, + Usage: "output gzipped manpage files to .gz", + Value: true, + }, + &cli.BoolFlag{ + Name: "text", + Aliases: []string{"z"}, + Usage: "output uncompressed text files", + Value: false, + }, + }, + }, + }, + EnableShellCompletion: true, + ShellCompletionCommandName: "@completion", + HideHelpCommand: true, + } +} + +func generateManpages(ctx context.Context, c *cli.Command) error { + manpage, err := docs.ToManWithSection(Command, 1) + if err != nil { + return err + } + dir := c.String("output") + err = os.MkdirAll(filepath.Join(dir, "man1"), 0755) + if err != nil { + // handle error + } + if c.Bool("text") { + file, err := os.Create(filepath.Join(dir, "man1", "hypeman.1")) + if err != nil { + return err + } + defer file.Close() + if _, err := file.WriteString(manpage); err != nil { + return err + } + } + if c.Bool("gzip") { + file, err := os.Create(filepath.Join(dir, "man1", "hypeman.1.gz")) + if err != nil { + return err + } + defer file.Close() + gzWriter := gzip.NewWriter(file) + defer gzWriter.Close() + _, err = gzWriter.Write([]byte(manpage)) + if err != nil { + return err + } + } + fmt.Printf("Wrote manpages to %s\n", dir) + return nil +} diff --git a/pkg/cmd/health.go b/pkg/cmd/health.go new file mode 100644 index 0000000..c73e01e --- /dev/null +++ b/pkg/cmd/health.go @@ -0,0 +1,42 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "https://github.com/stainless-sdks/hypeman-go/option" + + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var healthCheck = cli.Command{ + Name: "check", + Usage: "Health check", + Flags: []cli.Flag{}, + Action: handleHealthCheck, + HideHelpCommand: true, +} + +func handleHealthCheck(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Health.Check( + ctx, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("health check", json, format, transform) +} diff --git a/pkg/cmd/image.go b/pkg/cmd/image.go new file mode 100644 index 0000000..073e69d --- /dev/null +++ b/pkg/cmd/image.go @@ -0,0 +1,153 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "https://github.com/stainless-sdks/hypeman-go" + "https://github.com/stainless-sdks/hypeman-go/option" + + "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var imagesCreate = cli.Command{ + Name: "create", + Usage: "Pull and convert OCI image", + Flags: []cli.Flag{ + &jsonflag.JSONStringFlag{ + Name: "name", + Usage: "OCI image reference (e.g., docker.io/library/nginx:latest)", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "name", + }, + }, + }, + Action: handleImagesCreate, + HideHelpCommand: true, +} + +var imagesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get image details", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + }, + }, + Action: handleImagesRetrieve, + HideHelpCommand: true, +} + +var imagesList = cli.Command{ + Name: "list", + Usage: "List images", + Flags: []cli.Flag{}, + Action: handleImagesList, + HideHelpCommand: true, +} + +var imagesDelete = cli.Command{ + Name: "delete", + Usage: "Delete image", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + }, + }, + Action: handleImagesDelete, + HideHelpCommand: true, +} + +func handleImagesCreate(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + params := hypeman.ImageNewParams{} + var res []byte + _, err := cc.client.Images.New( + ctx, + params, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("images create", json, format, transform) +} + +func handleImagesRetrieve(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("name") && len(unusedArgs) > 0 { + cmd.Set("name", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Images.Get( + ctx, + cmd.Value("name").(string), + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("images retrieve", json, format, transform) +} + +func handleImagesList(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Images.List( + ctx, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("images list", json, format, transform) +} + +func handleImagesDelete(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("name") && len(unusedArgs) > 0 { + cmd.Set("name", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + return cc.client.Images.Delete( + ctx, + cmd.Value("name").(string), + option.WithMiddleware(cc.AsMiddleware()), + ) +} diff --git a/pkg/cmd/instance.go b/pkg/cmd/instance.go new file mode 100644 index 0000000..8d826fa --- /dev/null +++ b/pkg/cmd/instance.go @@ -0,0 +1,406 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "https://github.com/stainless-sdks/hypeman-go" + "https://github.com/stainless-sdks/hypeman-go/option" + + "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var instancesCreate = cli.Command{ + Name: "create", + Usage: "Create and start instance", + Flags: []cli.Flag{ + &jsonflag.JSONStringFlag{ + Name: "id", + Usage: "Unique identifier for the instance (provided by caller)", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "id", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "image", + Usage: "Image identifier", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "image", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "name", + Usage: "Human-readable name", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "name", + }, + }, + &jsonflag.JSONIntFlag{ + Name: "memory-max-mb", + Usage: "Maximum memory with hotplug in MB", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "memory_max_mb", + }, + Value: 4096, + }, + &jsonflag.JSONIntFlag{ + Name: "memory-mb", + Usage: "Base memory in MB", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "memory_mb", + }, + Value: 1024, + }, + &jsonflag.JSONIntFlag{ + Name: "port-mappings.guest_port", + Usage: "Port mappings from host to guest", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "port_mappings.#.guest_port", + }, + }, + &jsonflag.JSONIntFlag{ + Name: "port-mappings.host_port", + Usage: "Port mappings from host to guest", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "port_mappings.#.host_port", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "port-mappings.protocol", + Usage: "Port mappings from host to guest", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "port_mappings.#.protocol", + }, + Value: "tcp", + }, + &jsonflag.JSONAnyFlag{ + Name: "+port-mapping", + Usage: "Port mappings from host to guest", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "port_mappings.-1", + SetValue: map[string]interface{}{}, + }, + }, + &jsonflag.JSONIntFlag{ + Name: "timeout-seconds", + Usage: "Timeout for scale-to-zero semantics", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "timeout_seconds", + }, + Value: 3600, + }, + &jsonflag.JSONIntFlag{ + Name: "vcpus", + Usage: "Number of virtual CPUs", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "vcpus", + }, + Value: 2, + }, + &jsonflag.JSONStringFlag{ + Name: "volumes.mount_path", + Usage: "Volumes to attach", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "volumes.#.mount_path", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "volumes.volume_id", + Usage: "Volumes to attach", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "volumes.#.volume_id", + }, + }, + &jsonflag.JSONBoolFlag{ + Name: "volumes.readonly", + Usage: "Volumes to attach", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "volumes.#.readonly", + SetValue: true, + }, + Value: false, + }, + &jsonflag.JSONAnyFlag{ + Name: "+volume", + Usage: "Volumes to attach", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "volumes.-1", + SetValue: map[string]interface{}{}, + }, + }, + }, + Action: handleInstancesCreate, + HideHelpCommand: true, +} + +var instancesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get instance details", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + }, + Action: handleInstancesRetrieve, + HideHelpCommand: true, +} + +var instancesList = cli.Command{ + Name: "list", + Usage: "List instances", + Flags: []cli.Flag{}, + Action: handleInstancesList, + HideHelpCommand: true, +} + +var instancesDelete = cli.Command{ + Name: "delete", + Usage: "Stop and delete instance", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + }, + Action: handleInstancesDelete, + HideHelpCommand: true, +} + +var instancesPutInStandby = cli.Command{ + Name: "put-in-standby", + Usage: "Put instance in standby (pause, snapshot, delete VMM)", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + }, + Action: handleInstancesPutInStandby, + HideHelpCommand: true, +} + +var instancesRestoreFromStandby = cli.Command{ + Name: "restore-from-standby", + Usage: "Restore instance from standby", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + }, + Action: handleInstancesRestoreFromStandby, + HideHelpCommand: true, +} + +var instancesStreamLogs = cli.Command{ + Name: "stream-logs", + Usage: "Stream instance logs (SSE)", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + &jsonflag.JSONBoolFlag{ + Name: "follow", + Usage: "Follow logs (stream with SSE)", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "follow", + SetValue: true, + }, + Value: false, + }, + &jsonflag.JSONIntFlag{ + Name: "tail", + Usage: "Number of lines to return from end", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Query, + Path: "tail", + }, + Value: 100, + }, + }, + Action: handleInstancesStreamLogs, + HideHelpCommand: true, +} + +func handleInstancesCreate(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + params := hypeman.InstanceNewParams{} + var res []byte + _, err := cc.client.Instances.New( + ctx, + params, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("instances create", json, format, transform) +} + +func handleInstancesRetrieve(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Instances.Get( + ctx, + cmd.Value("id").(string), + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("instances retrieve", json, format, transform) +} + +func handleInstancesList(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Instances.List( + ctx, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("instances list", json, format, transform) +} + +func handleInstancesDelete(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + return cc.client.Instances.Delete( + ctx, + cmd.Value("id").(string), + option.WithMiddleware(cc.AsMiddleware()), + ) +} + +func handleInstancesPutInStandby(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Instances.PutInStandby( + ctx, + cmd.Value("id").(string), + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("instances put-in-standby", json, format, transform) +} + +func handleInstancesRestoreFromStandby(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Instances.RestoreFromStandby( + ctx, + cmd.Value("id").(string), + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("instances restore-from-standby", json, format, transform) +} + +func handleInstancesStreamLogs(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + params := hypeman.InstanceStreamLogsParams{} + stream := cc.client.Instances.StreamLogsStreaming( + ctx, + cmd.Value("id").(string), + params, + option.WithMiddleware(cc.AsMiddleware()), + ) + for stream.Next() { + fmt.Printf("%s\n", stream.Current().RawJSON()) + } + return stream.Err() +} diff --git a/pkg/cmd/instancevolume.go b/pkg/cmd/instancevolume.go new file mode 100644 index 0000000..d7e62e5 --- /dev/null +++ b/pkg/cmd/instancevolume.go @@ -0,0 +1,126 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "https://github.com/stainless-sdks/hypeman-go" + "https://github.com/stainless-sdks/hypeman-go/option" + + "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var instancesVolumesAttach = cli.Command{ + Name: "attach", + Usage: "Attach volume to instance", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + &cli.StringFlag{ + Name: "volume-id", + }, + &jsonflag.JSONStringFlag{ + Name: "mount-path", + Usage: "Path where volume should be mounted", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "mount_path", + }, + }, + &jsonflag.JSONBoolFlag{ + Name: "readonly", + Usage: "Mount as read-only", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "readonly", + SetValue: true, + }, + Value: false, + }, + }, + Action: handleInstancesVolumesAttach, + HideHelpCommand: true, +} + +var instancesVolumesDetach = cli.Command{ + Name: "detach", + Usage: "Detach volume from instance", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + &cli.StringFlag{ + Name: "volume-id", + }, + }, + Action: handleInstancesVolumesDetach, + HideHelpCommand: true, +} + +func handleInstancesVolumesAttach(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("volume-id") && len(unusedArgs) > 0 { + cmd.Set("volume-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + params := hypeman.InstanceVolumeAttachParams{} + if cmd.IsSet("id") { + params.ID = cmd.Value("id").(string) + } + var res []byte + _, err := cc.client.Instances.Volumes.Attach( + ctx, + cmd.Value("volume-id").(string), + params, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("instances:volumes attach", json, format, transform) +} + +func handleInstancesVolumesDetach(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("volume-id") && len(unusedArgs) > 0 { + cmd.Set("volume-id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + params := hypeman.InstanceVolumeDetachParams{} + if cmd.IsSet("id") { + params.ID = cmd.Value("id").(string) + } + var res []byte + _, err := cc.client.Instances.Volumes.Detach( + ctx, + cmd.Value("volume-id").(string), + params, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("instances:volumes detach", json, format, transform) +} diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go new file mode 100644 index 0000000..557960b --- /dev/null +++ b/pkg/cmd/util.go @@ -0,0 +1,254 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "bytes" + "fmt" + "golang.org/x/term" + "https://github.com/stainless-sdks/hypeman-go" + "https://github.com/stainless-sdks/hypeman-go/option" + "io" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" + + "github.com/itchyny/json2yaml" + "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/onkernel/hypeman-cli/pkg/jsonview" + "github.com/tidwall/gjson" + "github.com/tidwall/pretty" + "github.com/urfave/cli/v3" +) + +func getDefaultRequestOptions(cmd *cli.Command) []option.RequestOption { + opts := []option.RequestOption{ + option.WithHeader("User-Agent", fmt.Sprintf("Hypeman/CLI %s", Version)), + option.WithHeader("X-Stainless-Lang", "cli"), + option.WithHeader("X-Stainless-Package-Version", Version), + option.WithHeader("X-Stainless-Runtime", "cli"), + option.WithHeader("X-Stainless-CLI-Command", cmd.FullName()), + } + + // Override base URL if the --base-url flag is provided + if baseURL := cmd.String("base-url"); baseURL != "" { + opts = append(opts, option.WithBaseURL(baseURL)) + } + + return opts +} + +type apiCommandContext struct { + client hypeman.Client + cmd *cli.Command +} + +func (c apiCommandContext) AsMiddleware() option.Middleware { + body := getStdInput() + if body == nil { + body = []byte("{}") + } + var query = []byte("{}") + var header = []byte("{}") + + // Apply JSON flag mutations + body, query, header, err := jsonflag.ApplyMutations(body, query, header) + if err != nil { + log.Fatal(err) + } + + debug := c.cmd.Bool("debug") + + return func(r *http.Request, mn option.MiddlewareNext) (*http.Response, error) { + q := r.URL.Query() + for key, values := range serializeQuery(query) { + for _, value := range values { + q.Set(key, value) + } + } + r.URL.RawQuery = q.Encode() + + for key, values := range serializeHeader(header) { + for _, value := range values { + r.Header.Add(key, value) + } + } + + if r.Body != nil || len(body) > 2 { + r.Body = io.NopCloser(bytes.NewBuffer(body)) + r.ContentLength = int64(len(body)) + r.Header.Set("Content-Type", "application/json") + } + + // Add debug logging if the --debug flag is set + if debug { + logger := log.Default() + + if reqBytes, err := httputil.DumpRequest(r, true); err == nil { + logger.Printf("Request Content:\n%s\n", reqBytes) + } + + resp, err := mn(r) + if err != nil { + return resp, err + } + + if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + logger.Printf("Response Content:\n%s\n", respBytes) + } + + return resp, err + } + + return mn(r) + } +} + +func getAPICommandContext(cmd *cli.Command) *apiCommandContext { + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + return &apiCommandContext{client, cmd} +} + +func serializeQuery(params []byte) url.Values { + serialized := url.Values{} + + var serialize func(value gjson.Result, path string) + serialize = func(res gjson.Result, path string) { + if res.IsObject() { + for key, value := range res.Map() { + newPath := path + if len(newPath) == 0 { + newPath += key + } else { + newPath = "[" + key + "]" + } + + serialize(value, newPath) + } + } else if res.IsArray() { + for _, value := range res.Array() { + serialize(value, path) + } + } else { + serialized.Add(path, res.String()) + } + } + serialize(gjson.GetBytes(params, "@this"), "") + + for key, values := range serialized { + serialized.Set(key, strings.Join(values, ",")) + } + + return serialized +} + +func serializeHeader(params []byte) http.Header { + serialized := http.Header{} + + var serialize func(value gjson.Result, path string) + serialize = func(res gjson.Result, path string) { + if res.IsObject() { + for key, value := range res.Map() { + newPath := path + if len(newPath) > 0 { + newPath += "." + } + newPath += key + + serialize(value, newPath) + } + } else if res.IsArray() { + for _, value := range res.Array() { + serialize(value, path) + } + } else { + serialized.Add(path, res.String()) + } + } + serialize(gjson.GetBytes(params, "@this"), "") + + return serialized +} + +func getStdInput() []byte { + if !isInputPiped() { + return nil + } + data, err := io.ReadAll(os.Stdin) + if err != nil { + log.Fatal(err) + return nil + } + return data +} + +func isInputPiped() bool { + stat, _ := os.Stdin.Stat() + return (stat.Mode() & os.ModeCharDevice) == 0 +} + +func isTerminal(w io.Writer) bool { + switch v := w.(type) { + case *os.File: + return term.IsTerminal(int(v.Fd())) + default: + return false + } +} + +func shouldUseColors(w io.Writer) bool { + force, ok := os.LookupEnv("FORCE_COLOR") + + if ok { + if force == "1" { + return true + } + if force == "0" { + return false + } + } + + return isTerminal(w) +} + +func ShowJSON(title string, res gjson.Result, format string, transform string) error { + if format != "raw" && transform != "" { + transformed := res.Get(transform) + if transformed.Exists() { + res = transformed + } + } + switch strings.ToLower(format) { + case "auto": + return ShowJSON(title, res, "json", "") + case "explore": + return jsonview.ExploreJSON(title, res) + case "pretty": + jsonview.DisplayJSON(title, res) + return nil + case "json": + prettyJSON := pretty.Pretty([]byte(res.Raw)) + if shouldUseColors(os.Stdout) { + fmt.Print(string(pretty.Color(prettyJSON, pretty.TerminalStyle))) + } else { + fmt.Print(string(prettyJSON)) + } + return nil + case "raw": + fmt.Println(res.Raw) + return nil + case "yaml": + input := strings.NewReader(res.Raw) + var yaml strings.Builder + if err := json2yaml.Convert(&yaml, input); err != nil { + return err + } + fmt.Print(yaml.String()) + return nil + default: + return fmt.Errorf("Invalid format: %s, valid formats are: %s", format, strings.Join(OutputFormats, ", ")) + } +} diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go new file mode 100644 index 0000000..1f71453 --- /dev/null +++ b/pkg/cmd/version.go @@ -0,0 +1,5 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +const Version = "0.0.1" // x-release-please-version diff --git a/pkg/cmd/volume.go b/pkg/cmd/volume.go new file mode 100644 index 0000000..5ee97fb --- /dev/null +++ b/pkg/cmd/volume.go @@ -0,0 +1,169 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "context" + "fmt" + "https://github.com/stainless-sdks/hypeman-go" + "https://github.com/stainless-sdks/hypeman-go/option" + + "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var volumesCreate = cli.Command{ + Name: "create", + Usage: "Create volume", + Flags: []cli.Flag{ + &jsonflag.JSONStringFlag{ + Name: "name", + Usage: "Volume name", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "name", + }, + }, + &jsonflag.JSONIntFlag{ + Name: "size-gb", + Usage: "Size in gigabytes", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "size_gb", + }, + }, + &jsonflag.JSONStringFlag{ + Name: "id", + Usage: "Optional custom identifier (auto-generated if not provided)", + Config: jsonflag.JSONConfig{ + Kind: jsonflag.Body, + Path: "id", + }, + }, + }, + Action: handleVolumesCreate, + HideHelpCommand: true, +} + +var volumesRetrieve = cli.Command{ + Name: "retrieve", + Usage: "Get volume details", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + }, + Action: handleVolumesRetrieve, + HideHelpCommand: true, +} + +var volumesList = cli.Command{ + Name: "list", + Usage: "List volumes", + Flags: []cli.Flag{}, + Action: handleVolumesList, + HideHelpCommand: true, +} + +var volumesDelete = cli.Command{ + Name: "delete", + Usage: "Delete volume", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + }, + }, + Action: handleVolumesDelete, + HideHelpCommand: true, +} + +func handleVolumesCreate(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + params := hypeman.VolumeNewParams{} + var res []byte + _, err := cc.client.Volumes.New( + ctx, + params, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("volumes create", json, format, transform) +} + +func handleVolumesRetrieve(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Volumes.Get( + ctx, + cmd.Value("id").(string), + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("volumes retrieve", json, format, transform) +} + +func handleVolumesList(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + var res []byte + _, err := cc.client.Volumes.List( + ctx, + option.WithMiddleware(cc.AsMiddleware()), + option.WithResponseBodyInto(&res), + ) + if err != nil { + return err + } + + json := gjson.Parse(string(res)) + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + return ShowJSON("volumes list", json, format, transform) +} + +func handleVolumesDelete(ctx context.Context, cmd *cli.Command) error { + cc := getAPICommandContext(cmd) + unusedArgs := cmd.Args().Slice() + if !cmd.IsSet("id") && len(unusedArgs) > 0 { + cmd.Set("id", unusedArgs[0]) + unusedArgs = unusedArgs[1:] + } + if len(unusedArgs) > 0 { + return fmt.Errorf("Unexpected extra arguments: %v", unusedArgs) + } + return cc.client.Volumes.Delete( + ctx, + cmd.Value("id").(string), + option.WithMiddleware(cc.AsMiddleware()), + ) +} diff --git a/pkg/jsonflag/json_flag.go b/pkg/jsonflag/json_flag.go new file mode 100644 index 0000000..605f883 --- /dev/null +++ b/pkg/jsonflag/json_flag.go @@ -0,0 +1,248 @@ +package jsonflag + +import ( + "fmt" + "strconv" + "time" + + "github.com/urfave/cli/v3" +) + +type JSONConfig struct { + Kind MutationKind + Path string + // For boolean flags that set a specific value when present + SetValue any +} + +type JSONValueCreator[T any] struct{} + +func (c JSONValueCreator[T]) Create(val T, dest *T, config JSONConfig) cli.Value { + *dest = val + return &jsonValue[T]{ + destination: dest, + config: config, + } +} + +func (c JSONValueCreator[T]) ToString(val T) string { + switch v := any(val).(type) { + case string: + if v == "" { + return v + } + return fmt.Sprintf("%q", v) + case bool: + return strconv.FormatBool(v) + case int: + return strconv.Itoa(v) + case float64: + return strconv.FormatFloat(v, 'g', -1, 64) + case time.Time: + return v.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", v) + } +} + +type jsonValue[T any] struct { + destination *T + config JSONConfig +} + +func (v *jsonValue[T]) Set(val string) error { + var parsed T + var err error + + // If SetValue is configured, use that value instead of parsing the input + if v.config.SetValue != nil { + // For boolean flags with SetValue, register the configured value + if _, isBool := any(parsed).(bool); isBool { + globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) + *v.destination = any(true).(T) // Set the flag itself to true + return nil + } + // For any flags with SetValue, register the configured value + globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) + *v.destination = any(v.config.SetValue).(T) + return nil + } + + switch any(parsed).(type) { + case string: + parsed = any(val).(T) + case bool: + boolVal, parseErr := strconv.ParseBool(val) + if parseErr != nil { + return fmt.Errorf("invalid boolean value %q: %w", val, parseErr) + } + parsed = any(boolVal).(T) + case int: + intVal, parseErr := strconv.Atoi(val) + if parseErr != nil { + return fmt.Errorf("invalid integer value %q: %w", val, parseErr) + } + parsed = any(intVal).(T) + case float64: + floatVal, parseErr := strconv.ParseFloat(val, 64) + if parseErr != nil { + return fmt.Errorf("invalid float value %q: %w", val, parseErr) + } + parsed = any(floatVal).(T) + case time.Time: + // Try common datetime formats + formats := []string{ + time.RFC3339, + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + "2006-01-02", + "15:04:05", + "15:04", + } + var timeVal time.Time + var parseErr error + for _, format := range formats { + timeVal, parseErr = time.Parse(format, val) + if parseErr == nil { + break + } + } + if parseErr != nil { + return fmt.Errorf("invalid datetime value %q: %w", val, parseErr) + } + parsed = any(timeVal).(T) + case any: + // For `any`, store the string value directly + parsed = any(val).(T) + default: + return fmt.Errorf("unsupported type for JSON flag") + } + + *v.destination = parsed + globalRegistry.Mutate(v.config.Kind, v.config.Path, parsed) + return err +} + +func (v *jsonValue[T]) Get() any { + if v.destination != nil { + return *v.destination + } + var zero T + return zero +} + +func (v *jsonValue[T]) String() string { + if v.destination != nil { + switch val := any(*v.destination).(type) { + case string: + return val + case bool: + return strconv.FormatBool(val) + case int: + return strconv.Itoa(val) + case float64: + return strconv.FormatFloat(val, 'g', -1, 64) + case time.Time: + return val.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", val) + } + } + var zero T + switch any(zero).(type) { + case string: + return "" + case bool: + return "false" + case int: + return "0" + case float64: + return "0" + case time.Time: + return "" + default: + return fmt.Sprintf("%v", zero) + } +} + +func (v *jsonValue[T]) IsBoolFlag() bool { + return v.config.SetValue != nil +} + +// JSONDateValueCreator is a specialized creator for date-only values +type JSONDateValueCreator struct{} + +func (c JSONDateValueCreator) Create(val time.Time, dest *time.Time, config JSONConfig) cli.Value { + *dest = val + return &jsonDateValue{ + destination: dest, + config: config, + } +} + +func (c JSONDateValueCreator) ToString(val time.Time) string { + return val.Format("2006-01-02") +} + +type jsonDateValue struct { + destination *time.Time + config JSONConfig +} + +func (v *jsonDateValue) Set(val string) error { + // Try date-only formats first, then fall back to datetime formats + formats := []string{ + "2006-01-02", + "01/02/2006", + "Jan 2, 2006", + "January 2, 2006", + "2-Jan-2006", + time.RFC3339, + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + } + + var timeVal time.Time + var parseErr error + for _, format := range formats { + timeVal, parseErr = time.Parse(format, val) + if parseErr == nil { + break + } + } + if parseErr != nil { + return fmt.Errorf("invalid date value %q: %w", val, parseErr) + } + + *v.destination = timeVal + globalRegistry.Mutate(v.config.Kind, v.config.Path, timeVal.Format("2006-01-02")) + return nil +} + +func (v *jsonDateValue) Get() any { + if v.destination != nil { + return *v.destination + } + return time.Time{} +} + +func (v *jsonDateValue) String() string { + if v.destination != nil { + return v.destination.Format("2006-01-02") + } + return "" +} + +func (v *jsonDateValue) IsBoolFlag() bool { + return false +} + +type JSONStringFlag = cli.FlagBase[string, JSONConfig, JSONValueCreator[string]] +type JSONBoolFlag = cli.FlagBase[bool, JSONConfig, JSONValueCreator[bool]] +type JSONIntFlag = cli.FlagBase[int, JSONConfig, JSONValueCreator[int]] +type JSONFloatFlag = cli.FlagBase[float64, JSONConfig, JSONValueCreator[float64]] +type JSONDatetimeFlag = cli.FlagBase[time.Time, JSONConfig, JSONValueCreator[time.Time]] +type JSONDateFlag = cli.FlagBase[time.Time, JSONConfig, JSONDateValueCreator] +type JSONAnyFlag = cli.FlagBase[any, JSONConfig, JSONValueCreator[any]] diff --git a/pkg/jsonflag/mutation.go b/pkg/jsonflag/mutation.go new file mode 100644 index 0000000..46c115b --- /dev/null +++ b/pkg/jsonflag/mutation.go @@ -0,0 +1,104 @@ +package jsonflag + +import ( + "fmt" + "strconv" + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +type MutationKind string + +const ( + Body MutationKind = "body" + Query MutationKind = "query" + Header MutationKind = "header" +) + +type Mutation struct { + Kind MutationKind + Path string + Value any +} + +type registry struct { + mutations []Mutation +} + +var globalRegistry = ®istry{} + +func (r *registry) Mutate(kind MutationKind, path string, value any) { + r.mutations = append(r.mutations, Mutation{ + Kind: kind, + Path: path, + Value: value, + }) +} + +func (r *registry) Apply(body, query, header []byte) ([]byte, []byte, []byte, error) { + var err error + + for _, mutation := range r.mutations { + switch mutation.Kind { + case Body: + body, err = jsonSet(body, mutation.Path, mutation.Value) + case Query: + query, err = jsonSet(query, mutation.Path, mutation.Value) + case Header: + header, err = jsonSet(header, mutation.Path, mutation.Value) + } + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to apply mutation %s.%s: %w", mutation.Kind, mutation.Path, err) + } + } + + return body, query, header, nil +} + +func (r *registry) Clear() { + r.mutations = nil +} + +func (r *registry) List() []Mutation { + result := make([]Mutation, len(r.mutations)) + copy(result, r.mutations) + return result +} + +// Mutate adds a mutation that will be applied to the specified kind of data +func Mutate(kind MutationKind, path string, value any) { + globalRegistry.Mutate(kind, path, value) +} + +// ApplyMutations applies all registered mutations to the provided JSON data +func ApplyMutations(body, query, header []byte) ([]byte, []byte, []byte, error) { + return globalRegistry.Apply(body, query, header) +} + +// ClearMutations removes all registered mutations from the global registry +func ClearMutations() { + globalRegistry.Clear() +} + +// ListMutations returns a copy of all currently registered mutations +func ListMutations() []Mutation { + return globalRegistry.List() +} + +func jsonSet(json []byte, path string, value any) ([]byte, error) { + keys := strings.Split(path, ".") + path = "" + for _, key := range keys { + if key == "#" { + key = strconv.Itoa(len(gjson.GetBytes(json, path).Array()) - 1) + } + + if len(path) > 0 { + path += "." + } + path += key + } + return sjson.SetBytes(json, path, value) +} diff --git a/pkg/jsonflag/mutation_test.go b/pkg/jsonflag/mutation_test.go new file mode 100644 index 0000000..e87e518 --- /dev/null +++ b/pkg/jsonflag/mutation_test.go @@ -0,0 +1,37 @@ +package jsonflag + +import ( + "testing" +) + +func TestApply(t *testing.T) { + ClearMutations() + + Mutate(Body, "name", "test") + Mutate(Query, "page", 1) + Mutate(Header, "authorization", "Bearer token") + + body, query, header, err := ApplyMutations( + []byte(`{}`), + []byte(`{}`), + []byte(`{}`), + ) + + if err != nil { + t.Fatalf("Failed to apply mutations: %v", err) + } + + expectedBody := `{"name":"test"}` + expectedQuery := `{"page":1}` + expectedHeader := `{"authorization":"Bearer token"}` + + if string(body) != expectedBody { + t.Errorf("Body mismatch. Expected: %s, Got: %s", expectedBody, string(body)) + } + if string(query) != expectedQuery { + t.Errorf("Query mismatch. Expected: %s, Got: %s", expectedQuery, string(query)) + } + if string(header) != expectedHeader { + t.Errorf("Header mismatch. Expected: %s, Got: %s", expectedHeader, string(header)) + } +} diff --git a/pkg/jsonview/explorer.go b/pkg/jsonview/explorer.go new file mode 100644 index 0000000..96a7a6f --- /dev/null +++ b/pkg/jsonview/explorer.go @@ -0,0 +1,588 @@ +package jsonview + +import ( + "fmt" + "math" + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/reflow/truncate" + "github.com/muesli/reflow/wordwrap" + "github.com/tidwall/gjson" +) + +const ( + // UI layout constants + borderPadding = 2 + heightOffset = 5 + tableMinHeight = 2 + titlePaddingLeft = 2 + titlePaddingTop = 0 + footerPaddingLeft = 1 + + // Column width constants + defaultColumnWidth = 10 + keyColumnWidth = 3 + valueColumnWidth = 5 + + // String formatting constants + maxStringLength = 100 + maxPreviewLength = 24 + + arrayColor = lipgloss.Color("1") + stringColor = lipgloss.Color("5") + objectColor = lipgloss.Color("4") +) + +type keyMap struct { + Up key.Binding + Down key.Binding + Enter key.Binding + Back key.Binding + PrintValue key.Binding + Raw key.Binding + Quit key.Binding +} + +func (k keyMap) ShortHelp() []key.Binding { + return []key.Binding{k.Quit, k.Up, k.Down, k.Back, k.Enter, k.PrintValue, k.Raw} +} + +func (k keyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{k.ShortHelp()} +} + +var keys = keyMap{ + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "up"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "down"), + ), + Back: key.NewBinding( + key.WithKeys("left", "h", "backspace"), + key.WithHelp("←/h", "go back"), + ), + Enter: key.NewBinding( + key.WithKeys("right", "l"), + key.WithHelp("→/l", "expand"), + ), + PrintValue: key.NewBinding( + key.WithKeys("p"), + key.WithHelp("p", "print and exit"), + ), + Raw: key.NewBinding( + key.WithKeys("r"), + key.WithHelp("r", "toggle raw JSON"), + ), + Quit: key.NewBinding( + key.WithKeys("q", "esc", "ctrl+c", "enter"), + key.WithHelp("q/enter", "quit"), + ), +} + +var ( + titleStyle = lipgloss.NewStyle().Bold(true).PaddingLeft(titlePaddingLeft).PaddingTop(titlePaddingTop) + arrayStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(arrayColor) + stringStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(stringColor) + objectStyle = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(objectColor) + stringLiteralStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("2")) +) + +type JSONView interface { + GetPath() string + GetData() gjson.Result + Update(tea.Msg) tea.Cmd + View() string + Resize(width, height int) +} + +type TableView struct { + path string + data gjson.Result + table table.Model + rowData []gjson.Result +} + +func (tv *TableView) GetPath() string { return tv.path } +func (tv *TableView) GetData() gjson.Result { return tv.data } +func (tv *TableView) View() string { return tv.table.View() } + +func (tv *TableView) Update(msg tea.Msg) tea.Cmd { + var cmd tea.Cmd + tv.table, cmd = tv.table.Update(msg) + return cmd +} + +func (tv *TableView) Resize(width, height int) { + tv.updateColumnWidths(width) + tv.table.SetHeight(min(height-heightOffset, tableMinHeight+len(tv.table.Rows()))) +} + +func (tv *TableView) updateColumnWidths(width int) { + columns := tv.table.Columns() + widths := make([]int, len(columns)) + + // Calculate required widths from headers and content + for i, col := range columns { + widths[i] = lipgloss.Width(col.Title) + } + + for _, row := range tv.table.Rows() { + for i, cell := range row { + if i < len(widths) { + widths[i] = max(widths[i], lipgloss.Width(cell)) + } + } + } + + totalWidth := sum(widths) + available := width - borderPadding*len(columns) + + if totalWidth <= available { + for i, w := range widths { + columns[i].Width = w + } + return + } + + fairShare := float64(available) / float64(len(columns)) + shrinkable := 0.0 + + for _, w := range widths { + if float64(w) > fairShare { + shrinkable += float64(w) - fairShare + } + } + + if shrinkable > 0 { + excess := float64(totalWidth - available) + for i, w := range widths { + if float64(w) > fairShare { + reduction := (float64(w) - fairShare) * (excess / shrinkable) + widths[i] = int(math.Round(float64(w) - reduction)) + } + } + } + + for i, w := range widths { + columns[i].Width = w + } + + tv.table.SetColumns(columns) +} + +type TextView struct { + path string + data gjson.Result + viewport viewport.Model + ready bool +} + +func (tv *TextView) GetPath() string { return tv.path } +func (tv *TextView) GetData() gjson.Result { return tv.data } +func (tv *TextView) View() string { return tv.viewport.View() } +func (tv *TextView) Update(msg tea.Msg) tea.Cmd { + var cmd tea.Cmd + tv.viewport, cmd = tv.viewport.Update(msg) + return cmd +} + +func (tv *TextView) Resize(width, height int) { + h := height - heightOffset + if !tv.ready { + tv.viewport = viewport.New(width, h) + tv.viewport.SetContent(wordwrap.String(tv.data.String(), width)) + tv.ready = true + return + } + tv.viewport.Width = width + tv.viewport.Height = h +} + +type JSONViewer struct { + stack []JSONView + root string + width int + height int + rawMode bool + message string + help help.Model +} + +func ExploreJSON(title string, json gjson.Result) error { + view, err := newView("", json, false) + if err != nil { + return err + } + + viewer := &JSONViewer{stack: []JSONView{view}, root: title, rawMode: false, help: help.New()} + _, err = tea.NewProgram(viewer).Run() + if viewer.message != "" { + fmt.Println("\n" + viewer.message) + } + return err +} + +func (v *JSONViewer) current() JSONView { return v.stack[len(v.stack)-1] } +func (v *JSONViewer) Init() tea.Cmd { return nil } + +func (v *JSONViewer) resize(width, height int) { + v.width, v.height = width, height + v.help.Width = width + for i := range v.stack { + v.stack[i].Resize(width, height) + } +} + +func (v *JSONViewer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + v.resize(msg.Width-borderPadding, msg.Height) + return v, nil + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.Quit): + return v, tea.Quit + case key.Matches(msg, keys.Enter): + return v.navigateForward() + case key.Matches(msg, keys.Back): + return v.navigateBack() + case key.Matches(msg, keys.Raw): + return v.toggleRaw() + case key.Matches(msg, keys.PrintValue): + v.message = v.getSelectedContent() + return v, tea.Quit + } + } + + return v, v.current().Update(msg) +} + +func (v *JSONViewer) getSelectedContent() string { + tableView, ok := v.current().(*TableView) + if !ok { + return v.current().GetData().Raw + } + + selected := tableView.rowData[tableView.table.Cursor()] + if selected.Type == gjson.String { + return selected.String() + } + return selected.Raw +} + +func (v *JSONViewer) navigateForward() (tea.Model, tea.Cmd) { + tableView, ok := v.current().(*TableView) + if !ok { + return v, nil + } + + cursor := tableView.table.Cursor() + selected := tableView.rowData[cursor] + if !v.canNavigateInto(selected) { + return v, nil + } + + path := v.buildNavigationPath(tableView, cursor) + forwardView, err := newView(path, selected, v.rawMode) + if err != nil { + return v, nil + } + + v.stack = append(v.stack, forwardView) + v.resize(v.width, v.height) + return v, nil +} + +func (v *JSONViewer) buildNavigationPath(tableView *TableView, cursor int) string { + if tableView.data.IsArray() { + return fmt.Sprintf("%s[%d]", tableView.path, cursor) + } + key := tableView.data.Get("@keys").Array()[cursor].Str + return fmt.Sprintf("%s[%s]", tableView.path, quoteString(key)) +} + +func quoteString(s string) string { + // Replace backslashes and quotes with escaped versions + s = strings.ReplaceAll(s, "\\", "\\\\") + s = strings.ReplaceAll(s, "\"", "\\\"") + return stringLiteralStyle.Render("\"" + s + "\"") +} + +func (v *JSONViewer) canNavigateInto(data gjson.Result) bool { + switch { + case data.IsArray(): + return len(data.Array()) > 0 + case data.IsObject(): + return len(data.Map()) > 0 + case data.Type == gjson.String: + str := data.String() + return strings.Contains(str, "\n") || lipgloss.Width(str) >= maxStringLength + } + return false +} + +func (v *JSONViewer) navigateBack() (tea.Model, tea.Cmd) { + if len(v.stack) > 1 { + v.stack = v.stack[:len(v.stack)-1] + } + return v, nil +} + +func (v *JSONViewer) toggleRaw() (tea.Model, tea.Cmd) { + v.rawMode = !v.rawMode + + for i, view := range v.stack { + rawView, err := newView(view.GetPath(), view.GetData(), v.rawMode) + if err != nil { + return v, tea.Printf("Error: %s", err) + } + v.stack[i] = rawView + } + + v.resize(v.width, v.height) + return v, nil +} + +func (v *JSONViewer) View() string { + view := v.current() + title := v.buildTitle(view) + content := titleStyle.Render(title) + style := v.getStyleForData(view.GetData()) + content += "\n" + style.Render(view.View()) + content += "\n" + v.help.View(keys) + return content +} + +func (v *JSONViewer) buildTitle(view JSONView) string { + title := v.root + if len(view.GetPath()) > 0 { + title += " → " + view.GetPath() + } + if v.rawMode { + title += " (JSON)" + } + return title +} + +func (v *JSONViewer) getStyleForData(data gjson.Result) lipgloss.Style { + switch { + case data.Type == gjson.String: + return stringStyle + case data.IsArray(): + return arrayStyle + default: + return objectStyle + } +} + +func newView(path string, data gjson.Result, raw bool) (JSONView, error) { + if data.Type == gjson.String { + return newTextView(path, data) + } + return newTableView(path, data, raw) +} + +func newTextView(path string, data gjson.Result) (*TextView, error) { + if !data.Exists() || data.Type != gjson.String { + return nil, fmt.Errorf("invalid text JSON") + } + return &TextView{path: path, data: data}, nil +} + +func newTableView(path string, data gjson.Result, raw bool) (*TableView, error) { + if !data.Exists() || data.Type != gjson.JSON { + return nil, fmt.Errorf("invalid table JSON") + } + + switch { + case data.IsArray(): + array := data.Array() + if isArrayOfObjects(array) { + return newArrayOfObjectsTableView(path, data, array, raw), nil + } else { + return newArrayTableView(path, data, array, raw), nil + } + case data.IsObject(): + return newObjectTableView(path, data, raw), nil + default: + return nil, fmt.Errorf("unsupported JSON type") + } +} + +func newArrayTableView(path string, data gjson.Result, array []gjson.Result, raw bool) *TableView { + columns := []table.Column{{Title: "Items", Width: defaultColumnWidth}} + rows := make([]table.Row, 0, len(array)) + rowData := make([]gjson.Result, 0, len(array)) + + for _, item := range array { + rows = append(rows, table.Row{formatValue(item, raw)}) + rowData = append(rowData, item) + } + + t := createTable(columns, rows, arrayColor) + return &TableView{path: path, data: data, table: t, rowData: rowData} +} + +func newArrayOfObjectsTableView(path string, data gjson.Result, array []gjson.Result, raw bool) *TableView { + // Collect unique keys + keySet := make(map[string]struct{}) + var columns []table.Column + + for _, item := range array { + for _, key := range item.Get("@keys").Array() { + if _, exists := keySet[key.Str]; !exists { + keySet[key.Str] = struct{}{} + title := key.Str + columns = append(columns, table.Column{Title: title, Width: defaultColumnWidth}) + } + } + } + + rows := make([]table.Row, 0, len(array)) + rowData := make([]gjson.Result, 0, len(array)) + + for _, item := range array { + row := make(table.Row, len(columns)) + for i, col := range columns { + row[i] = formatValue(item.Get(col.Title), raw) + } + rows = append(rows, row) + rowData = append(rowData, item) + } + + t := createTable(columns, rows, arrayColor) + return &TableView{path: path, data: data, table: t, rowData: rowData} +} + +func newObjectTableView(path string, data gjson.Result, raw bool) *TableView { + columns := []table.Column{{Title: "Object"}, {}} + + keys := data.Get("@keys").Array() + rows := make([]table.Row, 0, len(keys)) + rowData := make([]gjson.Result, 0, len(keys)) + + for _, key := range keys { + value := data.Get(key.Str) + title := key.Str + rows = append(rows, table.Row{title, formatValue(value, raw)}) + rowData = append(rowData, value) + } + + // Adjust column widths based on content + for _, row := range rows { + for i, cell := range row { + if i < len(columns) { + columns[i].Width = max(columns[i].Width, lipgloss.Width(cell)) + } + } + } + + t := createTable(columns, rows, objectColor) + return &TableView{path: path, data: data, table: t, rowData: rowData} +} + +func createTable(columns []table.Column, rows []table.Row, bgColor lipgloss.Color) table.Model { + t := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + ) + + // Set common table styles + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(true) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(bgColor). + Bold(false) + t.SetStyles(s) + + return t +} + +func formatValue(value gjson.Result, raw bool) string { + if raw { + return value.Get("@ugly").Raw + } + + switch { + case value.IsObject(): + return formatObject(value) + case value.IsArray(): + return formatArray(value) + case value.Type == gjson.String: + return value.Str + default: + return value.Raw + } +} + +func formatObject(value gjson.Result) string { + keys := value.Get("@keys").Array() + keyStrs := make([]string, len(keys)) + + for i, key := range keys { + val := value.Get(key.Str) + keyStrs[i] = formatObjectKey(key.Str, val) + } + + return "{" + strings.Join(keyStrs, ", ") + "}" +} + +func formatObjectKey(key string, val gjson.Result) string { + switch { + case val.IsObject(): + return key + ":{…}" + case val.IsArray(): + return key + ":[…]" + case val.Type == gjson.String: + str := val.Str + if lipgloss.Width(str) <= maxPreviewLength { + return fmt.Sprintf(`%s:"%s"`, key, str) + } + return fmt.Sprintf(`%s:"%s…"`, key, truncate.String(str, uint(maxPreviewLength))) + default: + return key + ":" + val.Raw + } +} + +func formatArray(value gjson.Result) string { + switch count := len(value.Array()); count { + case 0: + return "[]" + case 1: + return "[...1 item...]" + default: + return fmt.Sprintf("[...%d items...]", count) + } +} + +func isArrayOfObjects(array []gjson.Result) bool { + for _, item := range array { + if !item.IsObject() { + return false + } + } + return len(array) > 0 +} + +func sum(ints []int) int { + total := 0 + for _, n := range ints { + total += n + } + return total +} diff --git a/pkg/jsonview/staticdisplay.go b/pkg/jsonview/staticdisplay.go new file mode 100644 index 0000000..4eaf65b --- /dev/null +++ b/pkg/jsonview/staticdisplay.go @@ -0,0 +1,139 @@ +package jsonview + +import ( + "fmt" + "os" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/term" + "github.com/muesli/reflow/truncate" + "github.com/tidwall/gjson" +) + +const ( + tabWidth = 2 +) + +var ( + keyStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("75")).Bold(false) + stringValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("113")) + numberValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215")) + boolValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("207")) + nullValueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("245")).Italic(true) + bulletStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("242")) + containerStyle = lipgloss.NewStyle(). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("63")). + Padding(0, 1) +) + +func formatJSON(json gjson.Result, width int) string { + if !json.Exists() { + return nullValueStyle.Render("Invalid JSON") + } + return formatResult(json, 0, width) +} + +func formatResult(result gjson.Result, indent, width int) string { + switch result.Type { + case gjson.String: + str := result.Str + if str == "" { + return nullValueStyle.Render("(empty)") + } + if lipgloss.Width(str) > width { + str = truncate.String(str, uint(width-1)) + "…" + } + return stringValueStyle.Render(str) + case gjson.Number: + return numberValueStyle.Render(result.Raw) + case gjson.True: + return boolValueStyle.Render("yes") + case gjson.False: + return boolValueStyle.Render("no") + case gjson.Null: + return nullValueStyle.Render("null") + case gjson.JSON: + if result.IsArray() { + return formatJSONArray(result, indent, width) + } + return formatJSONObject(result, indent, width) + default: + return stringValueStyle.Render(result.String()) + } +} + +func isSingleLine(result gjson.Result, indent int) bool { + return !(result.IsObject() || result.IsArray()) +} + +func formatJSONArray(result gjson.Result, indent, width int) string { + items := result.Array() + if len(items) == 0 { + return nullValueStyle.Render(" (none)") + } + + numberWidth := lipgloss.Width(fmt.Sprintf("%d. ", len(items))) + + var formattedItems []string + for i, item := range items { + number := fmt.Sprintf("%d.", i+1) + numbering := getIndent(indent) + bulletStyle.Render(number) + + // If the item will be a one-liner, put it inline after the numbering, + // otherwise it starts with a newline and goes below the numbering. + itemWidth := width + if isSingleLine(item, indent+1) { + // Add right-padding: + numbering += strings.Repeat(" ", numberWidth-lipgloss.Width(number)) + itemWidth = width - lipgloss.Width(numbering) + } + value := formatResult(item, indent+1, itemWidth) + formattedItems = append(formattedItems, numbering+value) + } + return "\n" + strings.Join(formattedItems, "\n") +} + +func formatJSONObject(result gjson.Result, indent, width int) string { + keys := result.Get("@keys").Array() + if len(keys) == 0 { + return nullValueStyle.Render("(empty)") + } + + var items []string + for _, key := range keys { + value := result.Get(key.String()) + keyStr := getIndent(indent) + keyStyle.Render(key.String()+":") + // If item will be a one-liner, put it inline after the key, otherwise + // it starts with a newline and goes below the key. + itemWidth := width + if isSingleLine(value, indent+1) { + keyStr += " " + itemWidth = width - lipgloss.Width(keyStr) + } + formattedValue := formatResult(value, indent+1, itemWidth) + items = append(items, keyStr+formattedValue) + } + + return "\n" + strings.Join(items, "\n") +} + +func getIndent(indent int) string { + return strings.Repeat(" ", indent*tabWidth) +} + +func RenderJSON(title string, json gjson.Result) string { + width, _, err := term.GetSize(os.Stdout.Fd()) + if err != nil { + width = 80 + } + width -= containerStyle.GetBorderLeftSize() + containerStyle.GetBorderRightSize() + + containerStyle.GetPaddingLeft() + containerStyle.GetPaddingRight() + content := strings.TrimLeft(formatJSON(json, width), "\n") + return titleStyle.Render(title) + "\n" + containerStyle.Render(content) +} + +func DisplayJSON(title string, json gjson.Result) { + fmt.Println(RenderJSON(title, json)) +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..53619de --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,67 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "simple", + "extra-files": [ + "pkg/cmd/version.go", + "README.md" + ] +} \ No newline at end of file diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 0000000..a73aff9 --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then + brew bundle check >/dev/null 2>&1 || { + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo + } +fi + +echo "==> Installing Go dependencies…" + +go mod tidy diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..db2a3fa --- /dev/null +++ b/scripts/format @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running gofmt -s -w" +gofmt -s -w . diff --git a/scripts/link b/scripts/link new file mode 100755 index 0000000..88a199b --- /dev/null +++ b/scripts/link @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" ]]; then + LOCAL_GO="$1" + shift +else + LOCAL_GO=../hypeman-go +fi + +echo "==> Linking with local directory" +go mod tidy -e +go mod edit -replace https://github.com/stainless-sdks/hypeman-go="$LOCAL_GO" diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..fa7ba1f --- /dev/null +++ b/scripts/lint @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running Go build" +go build ./... diff --git a/scripts/mock b/scripts/mock new file mode 100755 index 0000000..0b28f6e --- /dev/null +++ b/scripts/mock @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [[ -n "$1" && "$1" != '--'* ]]; then + URL="$1" + shift +else + URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" +fi + +# Check if the URL is empty +if [ -z "$URL" ]; then + echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" + exit 1 +fi + +echo "==> Starting mock server with URL ${URL}" + +# Run prism mock on the given spec +if [ "$1" == "--daemon" ]; then + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + + # Wait for server to come online + echo -n "Waiting for server" + while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + echo -n "." + sleep 0.1 + done + + if grep -q "✖ fatal" ".prism.log"; then + cat .prism.log + exit 1 + fi + + echo +else + npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" +fi diff --git a/scripts/unlink b/scripts/unlink new file mode 100755 index 0000000..80893cc --- /dev/null +++ b/scripts/unlink @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Unlinking with local directory" +go mod edit -dropreplace https://github.com/stainless-sdks/hypeman-go From 489dbc83126ed1f9c506ec64d7f5291f3adfc0ac Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:28:49 +0000 Subject: [PATCH 2/5] feat(api): add homebrew --- .github/workflows/publish-release.yml | 3 ++- .goreleaser.yml | 16 ++++++++++++++++ .stats.yml | 2 +- README.md | 7 +++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 2634ef5..133a244 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -28,4 +28,5 @@ jobs: version: latest args: release --clean env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index 46a2f46..303fae8 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -78,3 +78,19 @@ nfpms: contents: - src: man/man1/*.1.gz dst: /usr/share/man/man1/ +homebrew_casks: + - name: hypeman + repository: + owner: onkernel + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + homepage: https://github.com/onkernel/hypeman + description: orchestration for cloud-hypervisor VMs + license: Apache-2.0 + binary: "hypeman" + completions: + bash: "completions/hypeman.bash" + zsh: "completions/hypeman.zsh" + fish: "completions/hypeman.fish" + manpages: + - man/man1/hypeman.1.gz diff --git a/.stats.yml b/.stats.yml index 91c6268..52b4487 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-7c27e323412e72166bce2de104f1bf82b57197e05b686e94cd81d07e288bd558.yml openapi_spec_hash: 4656d2b318d04a9fec0210897d76b505 -config_hash: 8d56572492f9a13ba410b9beddf2c57d +config_hash: eef61f4c936e0ca6c8b9d381907a24c9 diff --git a/README.md b/README.md index d8490da..0ff4bf2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,13 @@ It is generated with [Stainless](https://www.stainless.com/). ## Installation +### Installing with Homebrew + +```sh +brew tap onkernel/tap +brew install hypeman +``` + ### Installing with Go From a42708e4e7f906d338b2db8da0ef56355e2b6ba8 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 20:59:59 +0000 Subject: [PATCH 3/5] feat(api): make public --- .stats.yml | 2 +- cmd/hypeman/main.go | 2 +- go.mod | 7 +++++-- go.sum | 13 ++++++++++--- pkg/cmd/health.go | 2 +- pkg/cmd/image.go | 4 ++-- pkg/cmd/instance.go | 4 ++-- pkg/cmd/instancevolume.go | 4 ++-- pkg/cmd/util.go | 4 ++-- pkg/cmd/volume.go | 4 ++-- scripts/link | 2 +- scripts/unlink | 2 +- 12 files changed, 30 insertions(+), 20 deletions(-) diff --git a/.stats.yml b/.stats.yml index 52b4487..2485d17 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 18 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fhypeman-7c27e323412e72166bce2de104f1bf82b57197e05b686e94cd81d07e288bd558.yml openapi_spec_hash: 4656d2b318d04a9fec0210897d76b505 -config_hash: eef61f4c936e0ca6c8b9d381907a24c9 +config_hash: e44daab66300348998d041d8dfb505b4 diff --git a/cmd/hypeman/main.go b/cmd/hypeman/main.go index cc68416..15f6d40 100644 --- a/cmd/hypeman/main.go +++ b/cmd/hypeman/main.go @@ -6,11 +6,11 @@ import ( "context" "errors" "fmt" - "https://github.com/stainless-sdks/hypeman-go" "net/http" "os" "github.com/onkernel/hypeman-cli/pkg/cmd" + "github.com/onkernel/hypeman-go" "github.com/tidwall/gjson" ) diff --git a/go.mod b/go.mod index cf17680..d1e1185 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,15 @@ require ( github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/term v0.2.1 + github.com/itchyny/json2yaml v0.1.4 github.com/muesli/reflow v0.3.0 + github.com/onkernel/hypeman-go v0.0.2 github.com/tidwall/gjson v1.18.0 + github.com/tidwall/pretty v1.2.1 github.com/tidwall/sjson v1.2.5 github.com/urfave/cli-docs/v3 v3.0.0-alpha6 github.com/urfave/cli/v3 v3.3.2 + golang.org/x/term v0.37.0 ) require ( @@ -31,9 +35,8 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 118b317..d4ca66e 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8= +github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -41,6 +43,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/onkernel/hypeman-go v0.0.2 h1:2hFv9bBLGoSw0DGTN4RWG9YmmKo6HrO/kwtVcq9RCYY= +github.com/onkernel/hypeman-go v0.0.2/go.mod h1:pxRRFfVcLvafZpDD1O6IjwHnem3hKEuZTCClrnGiIKA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -56,8 +60,9 @@ github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= @@ -72,8 +77,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/cmd/health.go b/pkg/cmd/health.go index c73e01e..b8080a6 100644 --- a/pkg/cmd/health.go +++ b/pkg/cmd/health.go @@ -5,8 +5,8 @@ package cmd import ( "context" "fmt" - "https://github.com/stainless-sdks/hypeman-go/option" + "github.com/onkernel/hypeman-go/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/image.go b/pkg/cmd/image.go index 073e69d..658a731 100644 --- a/pkg/cmd/image.go +++ b/pkg/cmd/image.go @@ -5,10 +5,10 @@ package cmd import ( "context" "fmt" - "https://github.com/stainless-sdks/hypeman-go" - "https://github.com/stainless-sdks/hypeman-go/option" "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/onkernel/hypeman-go" + "github.com/onkernel/hypeman-go/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/instance.go b/pkg/cmd/instance.go index 8d826fa..d05aff6 100644 --- a/pkg/cmd/instance.go +++ b/pkg/cmd/instance.go @@ -5,10 +5,10 @@ package cmd import ( "context" "fmt" - "https://github.com/stainless-sdks/hypeman-go" - "https://github.com/stainless-sdks/hypeman-go/option" "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/onkernel/hypeman-go" + "github.com/onkernel/hypeman-go/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/instancevolume.go b/pkg/cmd/instancevolume.go index d7e62e5..3228e12 100644 --- a/pkg/cmd/instancevolume.go +++ b/pkg/cmd/instancevolume.go @@ -5,10 +5,10 @@ package cmd import ( "context" "fmt" - "https://github.com/stainless-sdks/hypeman-go" - "https://github.com/stainless-sdks/hypeman-go/option" "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/onkernel/hypeman-go" + "github.com/onkernel/hypeman-go/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go index 557960b..e7dea77 100644 --- a/pkg/cmd/util.go +++ b/pkg/cmd/util.go @@ -6,8 +6,6 @@ import ( "bytes" "fmt" "golang.org/x/term" - "https://github.com/stainless-sdks/hypeman-go" - "https://github.com/stainless-sdks/hypeman-go/option" "io" "log" "net/http" @@ -19,6 +17,8 @@ import ( "github.com/itchyny/json2yaml" "github.com/onkernel/hypeman-cli/pkg/jsonflag" "github.com/onkernel/hypeman-cli/pkg/jsonview" + "github.com/onkernel/hypeman-go" + "github.com/onkernel/hypeman-go/option" "github.com/tidwall/gjson" "github.com/tidwall/pretty" "github.com/urfave/cli/v3" diff --git a/pkg/cmd/volume.go b/pkg/cmd/volume.go index 5ee97fb..25a14c8 100644 --- a/pkg/cmd/volume.go +++ b/pkg/cmd/volume.go @@ -5,10 +5,10 @@ package cmd import ( "context" "fmt" - "https://github.com/stainless-sdks/hypeman-go" - "https://github.com/stainless-sdks/hypeman-go/option" "github.com/onkernel/hypeman-cli/pkg/jsonflag" + "github.com/onkernel/hypeman-go" + "github.com/onkernel/hypeman-go/option" "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) diff --git a/scripts/link b/scripts/link index 88a199b..1b034b0 100755 --- a/scripts/link +++ b/scripts/link @@ -13,4 +13,4 @@ fi echo "==> Linking with local directory" go mod tidy -e -go mod edit -replace https://github.com/stainless-sdks/hypeman-go="$LOCAL_GO" +go mod edit -replace github.com/onkernel/hypeman-go="$LOCAL_GO" diff --git a/scripts/unlink b/scripts/unlink index 80893cc..beefe66 100755 --- a/scripts/unlink +++ b/scripts/unlink @@ -5,4 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Unlinking with local directory" -go mod edit -dropreplace https://github.com/stainless-sdks/hypeman-go +go mod edit -dropreplace github.com/onkernel/hypeman-go From 9f24998683d69418f2120ed53a8d7aedd5ef3d99 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:00:11 +0000 Subject: [PATCH 4/5] release: 0.1.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 11 +++++++++++ pkg/cmd/version.go | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 1332969..3d2ac0b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1" + ".": "0.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0436894 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## 0.1.0 (2025-11-14) + +Full Changelog: [v0.0.1...v0.1.0](https://github.com/onkernel/hypeman-cli/compare/v0.0.1...v0.1.0) + +### Features + +* **api:** add homebrew ([489dbc8](https://github.com/onkernel/hypeman-cli/commit/489dbc83126ed1f9c506ec64d7f5291f3adfc0ac)) +* **api:** make public ([a42708e](https://github.com/onkernel/hypeman-cli/commit/a42708e4e7f906d338b2db8da0ef56355e2b6ba8)) +* **api:** manual updates ([2aa453f](https://github.com/onkernel/hypeman-cli/commit/2aa453f3c0cc4191329ade9a8c8b2b328eca97e1)) diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index 1f71453..9bb8168 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.0.1" // x-release-please-version +const Version = "0.1.0" // x-release-please-version From 3c7155a232af57149b8a5efba4d2e28f8f56263e Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Fri, 14 Nov 2025 16:06:26 -0500 Subject: [PATCH 5/5] fix build --- pkg/cmd/instance.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/instance.go b/pkg/cmd/instance.go index d05aff6..11438f1 100644 --- a/pkg/cmd/instance.go +++ b/pkg/cmd/instance.go @@ -399,8 +399,9 @@ func handleInstancesStreamLogs(ctx context.Context, cmd *cli.Command) error { params, option.WithMiddleware(cc.AsMiddleware()), ) + defer stream.Close() for stream.Next() { - fmt.Printf("%s\n", stream.Current().RawJSON()) + fmt.Printf("%s\n", stream.Current()) } return stream.Err() }