├── .config └── nextest.toml ├── .devcontainer ├── README.md └── devcontainer.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ ├── build-nix │ │ └── action.yml │ └── install-nix │ │ └── action.yml ├── dependabot.yml ├── stale.yml └── workflows │ ├── docker_check.yml │ ├── nix.yml │ ├── release.yml │ ├── release_debrpm.yml │ ├── release_docker.yml │ ├── release_snap.yml │ ├── rust_ci.yml │ └── washlib.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .rustfmt.toml ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Completions.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── build ├── nfpm.amd64.yaml └── nfpm.arm64.yaml ├── crates └── wash-lib │ ├── Cargo.toml │ ├── README.md │ ├── src │ ├── actor.rs │ ├── app.rs │ ├── build.rs │ ├── capture.rs │ ├── cli │ │ ├── capture.rs │ │ ├── claims.rs │ │ ├── dev.rs │ │ ├── get.rs │ │ ├── inspect.rs │ │ ├── link.rs │ │ ├── mod.rs │ │ ├── output.rs │ │ ├── par.rs │ │ ├── registry.rs │ │ ├── scale.rs │ │ ├── spy.rs │ │ ├── start.rs │ │ ├── stop.rs │ │ └── update.rs │ ├── common.rs │ ├── config.rs │ ├── context │ │ ├── fs.rs │ │ └── mod.rs │ ├── drain.rs │ ├── generate │ │ ├── emoji.rs │ │ ├── favorites.rs │ │ ├── favorites.toml │ │ ├── genconfig.rs │ │ ├── git.rs │ │ ├── interactive.rs │ │ ├── mod.rs │ │ ├── project_variables.rs │ │ └── template.rs │ ├── id.rs │ ├── keys │ │ ├── fs.rs │ │ └── mod.rs │ ├── lib.rs │ ├── parser │ │ └── mod.rs │ ├── registry.rs │ ├── spier.rs │ ├── start │ │ ├── github.rs │ │ ├── mod.rs │ │ ├── nats.rs │ │ ├── wadm.rs │ │ └── wasmcloud.rs │ └── wait.rs │ └── tests │ └── parser │ ├── files │ ├── folder │ │ └── wasmcloud.toml │ ├── minimal_rust_actor.toml │ ├── minimal_rust_actor_core_module.toml │ ├── minimal_rust_actor_preview1.toml │ ├── minimal_rust_actor_preview2.toml │ ├── no_actor.toml │ ├── no_interface.toml │ ├── no_provider.toml │ ├── noconfig │ │ └── .gitkeep │ ├── random.txt │ ├── rust_actor.toml │ ├── tinygo_actor_component.toml │ ├── tinygo_actor_module.toml │ └── withcargotoml │ │ ├── Cargo.toml │ │ ├── minimal_rust_actor_with_cargo.toml │ │ └── src │ │ └── main.rs │ └── main.rs ├── docs └── DEVELOPMENT.md ├── flake.lock ├── flake.nix ├── rust-toolchain.toml ├── sample-manifest.yaml ├── snap ├── local │ └── icon.png └── snapcraft.yaml ├── src ├── app │ ├── mod.rs │ └── output.rs ├── appearance │ ├── mod.rs │ └── spinner.rs ├── build.rs ├── call.rs ├── cfg.rs ├── common │ ├── get_cmd.rs │ ├── link_cmd.rs │ ├── mod.rs │ ├── registry_cmd.rs │ ├── scale_cmd.rs │ ├── start_cmd.rs │ ├── stop_cmd.rs │ └── update_cmd.rs ├── completions.rs ├── ctl │ ├── mod.rs │ └── output.rs ├── ctx │ └── mod.rs ├── dev.rs ├── down │ └── mod.rs ├── drain.rs ├── generate.rs ├── keys.rs ├── main.rs ├── par.rs ├── smithy.rs ├── ui │ ├── config.rs │ └── mod.rs ├── up │ ├── config.rs │ ├── credsfile.rs │ └── mod.rs └── util.rs ├── tests ├── common.rs ├── integration.rs ├── integration_build.rs ├── integration_claims.rs ├── integration_dev.rs ├── integration_drain.rs ├── integration_get.rs ├── integration_inspect.rs ├── integration_keys.rs ├── integration_link.rs ├── integration_par.rs ├── integration_reg.rs ├── integration_scale.rs ├── integration_start.rs ├── integration_stop.rs ├── integration_up.rs └── integration_update.rs ├── tools ├── README.md ├── deps_check.py ├── docker-compose.yml └── kvcounter-example.sh └── washboard ├── .env.development ├── .eslintrc.cjs ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── README.md ├── components.json ├── dist ├── assets │ ├── index-353cfcc3.js │ └── index-b2ad57cb.css └── index.html ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── src ├── App.tsx ├── actors │ ├── ActorsTable.tsx │ └── count-instances.ts ├── assets │ ├── logo-icon.svg │ └── logo-wide.svg ├── dashboard │ ├── Dashboard.tsx │ └── StatsTile.tsx ├── hosts │ ├── HostsSummary.tsx │ └── index.ts ├── index.css ├── index.tsx ├── lattice │ ├── LatticeSettings.tsx │ ├── cloud-event.type.ts │ ├── lattice-service.ts │ ├── use-lattice-config.ts │ └── use-lattice-data.ts ├── layout │ ├── AppLayout.tsx │ └── Navigation.tsx ├── lib │ ├── AppProvider.tsx │ └── utils.ts ├── links │ └── LinksTable.tsx ├── providers │ └── ProvidersTable.tsx ├── routes │ ├── get-breadcrumbs.ts │ └── index.tsx ├── settings │ ├── DarkModeToggle.tsx │ ├── Settings.tsx │ ├── SettingsContext.tsx │ ├── index.ts │ └── use-settings.ts ├── svgr.d.ts ├── table.d.ts ├── ui │ ├── accordion │ │ └── index.tsx │ ├── alert-dialog │ │ └── index.tsx │ ├── badge │ │ ├── Badge.tsx │ │ ├── index.ts │ │ └── variants.ts │ ├── button │ │ ├── Button.tsx │ │ ├── index.ts │ │ └── variants.ts │ ├── card │ │ └── index.tsx │ ├── checkbox │ │ └── index.tsx │ ├── collapsible │ │ └── index.tsx │ ├── copy-button │ │ └── index.tsx │ ├── data-table │ │ └── index.tsx │ ├── form │ │ ├── Form.tsx │ │ ├── context.ts │ │ ├── index.ts │ │ └── use-form-field.ts │ ├── input │ │ └── index.tsx │ ├── label │ │ └── index.tsx │ ├── popover │ │ └── index.tsx │ ├── radio-group │ │ └── index.tsx │ ├── select │ │ └── index.tsx │ ├── sheet │ │ └── index.tsx │ ├── short-copy │ │ └── index.tsx │ ├── skeleton │ │ └── index.tsx │ ├── slider │ │ └── index.tsx │ ├── status-indicator │ │ └── index.tsx │ ├── switch │ │ └── index.tsx │ ├── table │ │ └── index.tsx │ ├── tabs │ │ └── index.tsx │ ├── textarea │ │ └── index.tsx │ ├── toast │ │ ├── Toast.tsx │ │ ├── index.ts │ │ └── use-toast.ts │ ├── toaster │ │ └── index.tsx │ ├── toggle │ │ ├── Toggle.tsx │ │ ├── index.ts │ │ └── variants.ts │ └── tooltip │ │ └── index.tsx └── vite.d.ts ├── tailwind.config.js ├── tsconfig.eslint.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [store] 2 | # The directory under the workspace root at which nextest-related files are 3 | # written. Profile-specific storage is currently written to dir/. 4 | dir = "target/nextest" 5 | 6 | # This section defines the default nextest profile. Custom profiles are layered 7 | # on top of the default profile. 8 | [profile.default] 9 | # "retries" defines the number of times a test should be retried. If set to a 10 | # non-zero value, tests that succeed on a subsequent attempt will be marked as 11 | # non-flaky. Can be overridden through the `--retries` option. 12 | # Examples 13 | # * retries = 3 14 | # * retries = { backoff = "fixed", count = 2, delay = "1s" } 15 | # * retries = { backoff = "exponential", count = 10, delay = "1s", jitter = true, max-delay = "10s" } 16 | retries = 0 17 | 18 | # The number of threads to run tests with. Supported values are either an integer or 19 | # the string "num-cpus". Can be overridden through the `--test-threads` option. 20 | test-threads = "num-cpus" 21 | 22 | # The number of threads required for each test. This is generally used in overrides to 23 | # mark certain tests as heavier than others. However, it can also be set as a global parameter. 24 | threads-required = 1 25 | 26 | # Show these test statuses in the output. 27 | # 28 | # The possible values this can take are: 29 | # * none: no output 30 | # * fail: show failed (including exec-failed) tests 31 | # * retry: show flaky and retried tests 32 | # * slow: show slow tests 33 | # * pass: show passed tests 34 | # * skip: show skipped tests (most useful for CI) 35 | # * all: all of the above 36 | # 37 | # Each value includes all the values above it; for example, "slow" includes 38 | # failed and retried tests. 39 | # 40 | # Can be overridden through the `--status-level` flag. 41 | status-level = "pass" 42 | 43 | # Similar to status-level, show these test statuses at the end of the run. 44 | final-status-level = "flaky" 45 | 46 | # "failure-output" defines when standard output and standard error for failing tests are produced. 47 | # Accepted values are 48 | # * "immediate": output failures as soon as they happen 49 | # * "final": output failures at the end of the test run 50 | # * "immediate-final": output failures as soon as they happen and at the end of 51 | # the test run; combination of "immediate" and "final" 52 | # * "never": don't output failures at all 53 | # 54 | # For large test suites and CI it is generally useful to use "immediate-final". 55 | # 56 | # Can be overridden through the `--failure-output` option. 57 | failure-output = "immediate" 58 | 59 | # "success-output" controls production of standard output and standard error on success. This should 60 | # generally be set to "never". 61 | success-output = "never" 62 | 63 | # Cancel the test run on the first failure. For CI runs, consider setting this 64 | # to false. 65 | fail-fast = true 66 | 67 | # Treat a test that takes longer than the configured 'period' as slow, and print a message. 68 | # See for more information. 69 | # 70 | # Optional: specify the parameter 'terminate-after' with a non-zero integer, 71 | # which will cause slow tests to be terminated after the specified number of 72 | # periods have passed. 73 | # Example: slow-timeout = { period = "60s", terminate-after = 2 } 74 | slow-timeout = { period = "60s" } 75 | 76 | # Treat a test as leaky if after the process is shut down, standard output and standard error 77 | # aren't closed within this duration. 78 | # 79 | # This usually happens in case of a test that creates a child process and lets it inherit those 80 | # handles, but doesn't clean the child process up (especially when it fails). 81 | # 82 | # See for more information. 83 | leak-timeout = "100ms" 84 | 85 | [profile.ci] 86 | retries = { backoff = "exponential", count = 3, delay = "30s", jitter = true, max-delay = "300s" } 87 | fail-fast = false 88 | 89 | [profile.integration] 90 | retries = { backoff = "fixed", count = 2, delay = "1s" } 91 | fail-fast = false 92 | 93 | 94 | # See https://nexte.st/book/test-groups.html 95 | [test-groups] 96 | serial-integration = { max-threads = 1 } 97 | 98 | # All tests with the suffix '_serial' will be run serially 99 | [[profile.default.overrides]] 100 | filter = 'test(/_serial$/)' 101 | test-group = 'serial-integration' -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # wash devcontainer 2 | 3 | A simple devcontainer that has `rust` installed. Try it out with `devcontainer open` at the root of this repository! 4 | 5 | ## Prerequisites 6 | - [devcontainer CLI](https://code.visualstudio.com/docs/devcontainers/devcontainer-cli#_installation) 7 | - VSCode -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wash", 3 | "image": "rust:bullseye", 4 | "features": { 5 | "devwasm.azurecr.io/dev-wasm/dev-wasm-feature/rust-wasi:0": {}, 6 | "ghcr.io/lee-orr/rusty-dev-containers/cargo-binstall:latest": {}, 7 | "ghcr.io/lee-orr/rusty-dev-containers/cargo-watch:latest": {}, 8 | "ghcr.io/lee-orr/rusty-dev-containers/cargo-nextest:latest": {}, 9 | "ghcr.io/devcontainers/features/go:1": {}, 10 | "ghcr.io/lee-orr/rusty-dev-containers/tinygo:0": { 11 | "version": "0.27.0" 12 | } 13 | }, 14 | "workspaceMount": "source=${localWorkspaceFolder},target=/wash,type=bind,consistency=cached", 15 | "workspaceFolder": "/wash", 16 | "customizations": { 17 | "vscode": { 18 | "settings": { 19 | "git.alwaysSignOff": true 20 | }, 21 | "extensions": [ 22 | "rust-lang.rust-analyzer" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # wasmcloud team members 2 | * @autodidaddict @brooksmtownsend @thomastaylor312 @mattwilkinsonn 3 | 4 | # wasmcloud devops 5 | /.github/actions/ @brooksmtownsend @thomastaylor312 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Run command '...' 16 | 1. Run other command '...' 17 | 1. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environment (please complete the following information) ** 26 | - OS: [e.g. Linux, MacOS] 27 | - Shell [e.g. bash, zsh, powershell] 28 | - wash Version [e.g. 0.2.0] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE] " 5 | labels: enhancement, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/build-nix/action.yml: -------------------------------------------------------------------------------- 1 | name: build via Nix 2 | 3 | inputs: 4 | package: 5 | description: package specification to build 6 | required: true 7 | install-path: 8 | description: path within resulting output, from which to install (e.g. `/bin/wash`) 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - run: nix build -L '.#${{ inputs.package }}' 14 | shell: bash 15 | - run: nix run -L --inputs-from . 'nixpkgs#coreutils' -- --coreutils-prog=ginstall -p "./result${{ inputs.install-path }}" '${{ inputs.package }}' 16 | shell: bash 17 | - uses: actions/upload-artifact@v3 18 | with: 19 | name: ${{ inputs.package }} 20 | path: ${{ inputs.package }} 21 | -------------------------------------------------------------------------------- /.github/actions/install-nix/action.yml: -------------------------------------------------------------------------------- 1 | name: install Nix 2 | 3 | inputs: 4 | cachixAuthToken: 5 | description: auth token for https://app.cachix.org/organization/wasmcloud/cache/wasmcloud 6 | 7 | runs: 8 | using: composite 9 | steps: 10 | # Install Nix 11 | - uses: DeterminateSystems/nix-installer-action@v4 12 | with: 13 | extra-conf: | 14 | accept-flake-config = true 15 | 16 | # Setup magic Nix cache 17 | - uses: DeterminateSystems/magic-nix-cache-action@v2 18 | 19 | # Setup Cachix cache 20 | - uses: cachix/cachix-action@v12 21 | continue-on-error: true 22 | with: 23 | name: wasmcloud 24 | authToken: '${{ inputs.cachixAuthToken }}' 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | open-pull-requests-limit: 0 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "github-actions" 9 | open-pull-requests-limit: 0 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: npm 14 | open-pull-requests-limit: 0 15 | directory: "/washboard" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned # Pinned issues should stick around 8 | - security # Security issues need to be resolved 9 | - roadmap # Issue is captured on the wasmCloud roadmap and won't be lost 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. If this 16 | has been closed too eagerly, please feel free to tag a maintainer so we can 17 | keep working on the issue. Thank you for contributing to wasmCloud! 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | cargo_check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Build 14 | run: cargo build --verbose 15 | - name: Install nextest 16 | uses: taiki-e/install-action@nextest 17 | - name: Run tests 18 | run: make test 19 | - name: Check fmt 20 | run: cargo fmt -- --check 21 | 22 | clippy_check: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - run: rustup component add clippy 27 | - uses: actions-rs/clippy-check@v1 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | args: --all-features 31 | 32 | windows_build: 33 | runs-on: windows-latest-8-cores 34 | needs: [cargo_check] 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Compile wash 38 | run: cargo build --release 39 | - name: Upload artifact 40 | uses: actions/upload-artifact@v3 41 | with: 42 | name: windows 43 | path: target/release/wash.exe 44 | 45 | release: 46 | needs: [cargo_check, clippy_check, windows_build] 47 | runs-on: ubuntu-latest 48 | steps: 49 | # We need to put windows on the release so that chocolatey can download it 50 | - name: Download windows release 51 | uses: actions/download-artifact@v3 52 | with: 53 | path: release 54 | - name: Release 55 | id: create_release 56 | uses: softprops/action-gh-release@v1 57 | with: 58 | files: release/windows/wash.exe 59 | generate_release_notes: true 60 | tag_name: ${{ github.ref_name }} 61 | release_name: Release ${{ github.ref_name }} 62 | token: ${{ secrets.GITHUB_TOKEN }} 63 | draft: false 64 | prerelease: true 65 | 66 | release_wash_cli: 67 | needs: release 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v4 71 | - id: crates-release-action 72 | uses: wasmcloud/common-actions/crates-release@main 73 | with: 74 | crates-token: ${{ secrets.CRATES_PUBLISH_TOKEN }} 75 | -------------------------------------------------------------------------------- /.github/workflows/release_debrpm.yml: -------------------------------------------------------------------------------- 1 | name: Release - Deb / RPM 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | cargo_check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Build 14 | run: cargo build --verbose 15 | - name: Install nextest 16 | uses: taiki-e/install-action@nextest 17 | - name: Run tests 18 | run: make test 19 | - name: Check fmt 20 | run: cargo fmt -- --check 21 | 22 | clippy_check: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - run: rustup component add clippy 27 | - uses: actions-rs/clippy-check@v1 28 | with: 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | args: --all-features 31 | 32 | package: 33 | needs: [cargo_check, clippy_check] 34 | strategy: 35 | matrix: 36 | arch: [arm64, amd64] 37 | include: 38 | - arch: arm64 39 | rpmarch: aarch64 40 | - arch: amd64 41 | rpmarch: x86_64 42 | 43 | runs-on: ubuntu-20.04 44 | env: 45 | REF: ${{ github.ref }} 46 | PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_API_TOKEN }} 47 | 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | - name: Compile wash 52 | if: ${{ matrix.arch }} == 'amd64' 53 | run: cargo build --release 54 | - name: Install cross 55 | if: ${{ matrix.arch }} == 'arm64' 56 | run: | 57 | cargo install cross --git https://github.com/cross-rs/cross 58 | - name: Compile wash 59 | if: ${{ matrix.arch }} == 'arm64' 60 | run: | 61 | cross build --target aarch64-unknown-linux-gnu --release 62 | - name: Install NFPM 63 | run: | 64 | echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list 65 | sudo apt update 66 | sudo apt install nfpm 67 | - name: Package Debian 68 | run: | 69 | export VERSION=$(echo $REF | cut -d/ -f3) 70 | nfpm pkg --packager deb -f build/nfpm.${{matrix.arch}}.yaml 71 | nfpm pkg --packager rpm -f build/nfpm.${{matrix.arch}}.yaml 72 | - name: Push amd64 (deb) 73 | run: | 74 | debs=(35 203 206 207 210 215 219 220 221 233 235 237 261 266) 75 | for distro_version in "${debs[@]}"; do 76 | curl -F "package[distro_version_id]=${distro_version}" -F "package[package_file]=@$(ls wash_*_${{matrix.arch}}.deb)" https://$PACKAGECLOUD_TOKEN:@packagecloud.io/api/v1/repos/wasmcloud/core/packages.json; 77 | done 78 | - name: Push x86_64 (rpm) 79 | run: | 80 | rpms=(194 204 209 216 226 231 236 239 240 244 260 273) 81 | for distro_version in "${rpms[@]}"; do 82 | curl -F "package[distro_version_id]=${distro_version}" -F "package[package_file]=@$(ls wash-*.${{matrix.rpmarch}}.rpm)" https://$PACKAGECLOUD_TOKEN:@packagecloud.io/api/v1/repos/wasmcloud/core/packages.json; 83 | done 84 | -------------------------------------------------------------------------------- /.github/workflows/release_snap.yml: -------------------------------------------------------------------------------- 1 | name: Release - snap 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | cargo_check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Build 15 | run: cargo build --verbose 16 | - name: Install nextest 17 | uses: taiki-e/install-action@nextest 18 | - name: Run tests 19 | run: make test 20 | - name: Check fmt 21 | run: cargo fmt -- --check 22 | 23 | clippy_check: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - run: rustup component add clippy 28 | - uses: actions-rs/clippy-check@v1 29 | with: 30 | token: ${{ secrets.GITHUB_TOKEN }} 31 | args: --all-features 32 | 33 | snap_release: 34 | needs: [cargo_check, clippy_check] 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: snapcore/action-build@v1 39 | id: build 40 | - uses: snapcore/action-publish@v1 41 | env: 42 | SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_STORE_LOGIN }} 43 | with: 44 | snap: ${{ steps.build.outputs.snap }} 45 | release: edge 46 | -------------------------------------------------------------------------------- /.github/workflows/rust_ci.yml: -------------------------------------------------------------------------------- 1 | name: Rust CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: Swatinem/rust-cache@v2 19 | with: 20 | shared-key: "ubuntu-22.04-shared-cache" 21 | - name: Run Rust check + clippy 22 | run: make rust-check 23 | 24 | build_ui: 25 | name: Build UI 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-node@v4 30 | with: 31 | node-version: '18.x' 32 | - name: npm install 33 | run: npm install --prefix ./washboard 34 | - name: npm run build 35 | run: npm run build --prefix ./washboard 36 | 37 | unit_tests: 38 | name: Unit Tests 39 | strategy: 40 | fail-fast: false # Ensure we can run the full suite even if one OS fails 41 | matrix: 42 | os: [ubuntu-22.04, windows-latest-8-cores, macos-11] 43 | runs-on: ${{ matrix.os }} 44 | steps: 45 | - uses: actions/setup-node@v4 46 | with: 47 | node-version: '18.x' 48 | - uses: actions/checkout@v4 49 | - uses: Swatinem/rust-cache@v2 50 | with: 51 | shared-key: "${{ matrix.os }}-shared-cache" 52 | - name: Install nextest 53 | uses: taiki-e/install-action@nextest 54 | 55 | - name: Build wash 56 | run: make build 57 | 58 | - name: Run all wash & wash-lib unit tests 59 | run: make test-wash-ci 60 | 61 | integration_tests: 62 | name: Integration Tests 63 | runs-on: ubuntu-22.04 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: Swatinem/rust-cache@v2 67 | with: 68 | shared-key: "ubuntu-22.04-shared-cache" 69 | - uses: acifani/setup-tinygo@v1 70 | with: 71 | tinygo-version: '0.27.0' 72 | install-binaryen: 'false' 73 | - name: Add wasm32-unknown-unknown 74 | run: rustup target add wasm32-unknown-unknown 75 | - name: Launch integration test services 76 | uses: sudo-bot/action-docker-compose@latest 77 | with: 78 | cli-args: "-f ./tools/docker-compose.yml up --detach" 79 | - name: Install nextest 80 | uses: taiki-e/install-action@nextest 81 | - name: Run integration tests 82 | run: make test-integration-ci 83 | -------------------------------------------------------------------------------- /.github/workflows/washlib.yml: -------------------------------------------------------------------------------- 1 | name: Wash Lib Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "wash-lib-v*" # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | env: 9 | working-directory: ./crates/wash-lib 10 | jobs: 11 | rust_check: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-20.04, windows-latest-8-cores, macos-11] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install nextest 20 | uses: taiki-e/install-action@nextest 21 | - name: Run wash-lib unit tests (${{ matrix.name }}) 22 | run: cargo nextest run --profile ci -p wash-lib 23 | 24 | release_wash_lib: 25 | needs: rust_check 26 | if: startswith(github.ref, 'refs/tags/') # Only run on tag push 27 | runs-on: ubuntu-latest 28 | env: 29 | working-directory: ./crates/wash-lib 30 | steps: 31 | - uses: actions/checkout@v4 32 | - id: crates-release-action 33 | uses: wasmcloud/common-actions/crates-release@main 34 | with: 35 | working-directory: ${{ env.working-directory }} 36 | crates-token: ${{ secrets.CRATES_PUBLISH_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk 7 | 8 | # Local development environment 9 | docker-compose.yml 10 | 11 | # Files created during tests 12 | /tests/fixtures 13 | /tmp/ 14 | 15 | # ide 16 | .idea 17 | 18 | # other 19 | *.par 20 | *.par.gz 21 | *.gz 22 | .vscode 23 | 24 | # Host config files 25 | host_config.json 26 | # No dumps in git 27 | *.dump 28 | 29 | # node modules 30 | node_modules 31 | 32 | # OS generated files 33 | .DS_Store -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/doublify/pre-commit-rust 5 | rev: master 6 | hooks: 7 | - id: fmt 8 | - id: cargo-check 9 | - id: clippy 10 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you have any feature suggestions, find any bugs, or otherwise have a question, please submit an issue [here][0]. 4 | Forking & submitting Pull Requests are welcome, and the [good first issue][1] label is a great way to find a place to 5 | start if you're looking to contribute. 6 | 7 | ## References 8 | 9 | - [Top-Level WasmCloud Contributing Guide][2] 10 | - [Developing `wash` Rust Development Guide](./docs/DEVELOPMENT.md) 11 | - [Contributing to Washboard UI](./washboard/CONTRIBUTING.md) 12 | 13 | [0]: https://github.com/wasmcloud/wash/issues/new/choose 14 | [1]: https://github.com/wasmcloud/wash/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22 15 | [2]: https://github.com/wasmCloud/wasmCloud/blob/main/CONTRIBUTING.md 16 | -------------------------------------------------------------------------------- /Completions.md: -------------------------------------------------------------------------------- 1 | 2 | # Shell completions 3 | 4 | `wash` can generate scripts that enable auto-completion for several popular shells. With completion enabled, 5 | you can press TAB while entering a `wash` command to see available options and 6 | subcommands. (Depending on configuration, you may need to type a `-`, then TAB, to see options) 7 | 8 | In the set-up instructions below, `wash` generates the completion script file when the shell starts, ensuring that you always 9 | have the latest version of the script even if wash was just updated. 10 | 11 | 12 | ## Zsh 13 | 14 | Modify `~/.zshrc` by adding the following lines. The folder `$HOME/.wash` must be added to the `fpath` array before calling oh-my-zsh: 15 | 16 | ``` 17 | $HOME/.cargo/bin/wash completions -d $HOME/.wash zsh 18 | fpath=( $HOME/.wash "${fpath[@]}" ) 19 | [ -n "$ZSH" ] && [ -r $ZSH/oh-my-zsh.sh ] && source $ZSH/oh-my-zsh.sh 20 | ``` 21 | 22 | Completions will be enabled the next time you start a shell. Or, you can `source ~/.zshrc` for the completions to take effect in the current shell. 23 | 24 | 25 | ## Bash 26 | 27 | Modify `~/.bashrc` by adding the following lines. 28 | 29 | ``` 30 | $HOME/.cargo/bin/wash completions -d $HOME/.wash bash 31 | source $HOME/.wash/wash.bash 32 | ``` 33 | 34 | Completions will be enabled the next time you start a shell. Or, you can `source ~/.bashrc` for the completions to take effect in the current shell. 35 | 36 | 37 | ## Fish 38 | 39 | Modify `~/.fishrc` by adding the following lines. 40 | 41 | ``` 42 | mkdir -p ~/.config/fish/completions 43 | $HOME/.cargo/bin/wash completions -d ~/.config/fish/completions fish 44 | ``` 45 | 46 | 47 | ## PowerShell 48 | 49 | Add the following line to your powershell profile script. This will generate `wash.ps1` in the specified folder. 50 | 51 | ``` 52 | wash completions -d "C:\Users\[User]\Documents\WindowsPowerShell" power-shell 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye-slim as base 2 | 3 | RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ca-certificates 4 | 5 | FROM base AS base-amd64 6 | ARG BIN_AMD64 7 | ARG BIN=$BIN_AMD64 8 | 9 | FROM base AS base-arm64 10 | ARG BIN_ARM64 11 | ARG BIN=$BIN_ARM64 12 | 13 | FROM base-$TARGETARCH 14 | 15 | ARG USERNAME=wash 16 | ARG USER_UID=1000 17 | ARG USER_GID=$USER_UID 18 | 19 | RUN addgroup --gid $USER_GID $USERNAME \ 20 | && adduser --disabled-login -u $USER_UID --ingroup $USERNAME $USERNAME 21 | 22 | # Copy application binary from disk 23 | COPY --chown=$USERNAME --chmod=755 ${BIN} /usr/local/bin/wash 24 | 25 | USER $USERNAME 26 | 27 | # Run the application 28 | ENTRYPOINT ["/usr/local/bin/wash"] 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL:=help 2 | 3 | .PHONY: build build-watch build-ui test test-integration test-all clean help run-ui 4 | 5 | CARGO ?= cargo 6 | DOCKER ?= docker 7 | PYTHON ?= python3 8 | NPM ?= npm 9 | 10 | ##@ Helpers 11 | 12 | help: ## Display this help 13 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_\-.*]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 14 | 15 | clean: ## Clean all tests 16 | @$(CARGO) clean 17 | wash drain all 18 | 19 | deps-check: 20 | @$(PYTHON) tools/deps_check.py 21 | 22 | run-ui: # Run UI from source 23 | @$(NPM) install --prefix washboard 24 | @$(NPM) run --prefix washboard dev 25 | 26 | ##@ Building 27 | 28 | build: ## Build the project 29 | @$(CARGO) build 30 | 31 | build-watch: ## Continuously build the project 32 | @$(CARGO) watch -x build 33 | 34 | build-ui: ## Build the UI from source 35 | @$(NPM) install --prefix .washboard 36 | @$(NPM) run build --prefix .washboard 37 | 38 | ##@ Testing 39 | 40 | test: ## Run unit test suite 41 | @$(CARGO) nextest run $(TARGET) --no-fail-fast --bin wash 42 | @$(CARGO) nextest run $(TARGET) --no-fail-fast -p wash-lib --features=cli 43 | 44 | test-wash-ci: 45 | @$(CARGO) nextest run --profile ci --workspace --all-features -E 'binary(wash)' -E 'package(wash-lib)' 46 | 47 | test-watch: ## Run unit tests continously, can optionally specify a target test filter. 48 | @$(CARGO) watch -- $(CARGO) nextest run $(TARGET) 49 | 50 | test-integration: ## Run the entire integration test suite (with docker compose) 51 | @$(DOCKER) compose -f ./tools/docker-compose.yml up --detach 52 | @$(CARGO) nextest run $(TARGET) --profile integration -E 'kind(test)' --nocapture 53 | @$(DOCKER) compose -f ./tools/docker-compose.yml down 54 | 55 | test-integration-ci: ## Run the entire integration test suite only 56 | @$(CARGO) nextest run --profile ci -E 'kind(test)' 57 | 58 | test-integration-watch: ## Run integration test suite continuously 59 | @$(CARGO) watch -- $(MAKE) test-integration 60 | 61 | test-unit: ## Run one or more unit tests 62 | @$(CARGO) nextest run $(TARGET) 63 | 64 | test-unit-watch: ## Run tests continuously 65 | @$(CARGO) watch -- $(MAKE) test-unit 66 | 67 | rust-check: ## Run rust checks 68 | @$(CARGO) fmt --all --check 69 | @$(CARGO) clippy --all-features --all-targets --workspace 70 | 71 | test-all: ## Run all tests 72 | $(MAKE) test 73 | $(MAKE) test-integration 74 | $(MAKE) rust-check 75 | -------------------------------------------------------------------------------- /build/nfpm.amd64.yaml: -------------------------------------------------------------------------------- 1 | # NFPM: check https://nfpm.goreleaser.com/configuration for detailed usage 2 | # wash cli [amd64] 3 | 4 | name: wash 5 | arch: amd64 6 | platform: linux 7 | version: ${VERSION} 8 | section: default 9 | priority: extra 10 | maintainer: Bill Young 11 | description: | 12 | WAsmcloud SHell - the comprehensive command-line tool for wasmCloud development 13 | vendor: wasmcloud 14 | homepage: https://wasmcloud.com 15 | license: Apache-2.0 16 | contents: 17 | - src: target/release/wash 18 | dst: /usr/local/bin/wash 19 | -------------------------------------------------------------------------------- /build/nfpm.arm64.yaml: -------------------------------------------------------------------------------- 1 | # NFPM: check https://nfpm.goreleaser.com/configuration for detailed usage 2 | # wash cli [arm64] 3 | 4 | name: wash 5 | arch: arm64 6 | platform: linux 7 | version: ${VERSION} 8 | section: default 9 | priority: extra 10 | maintainer: Bill Young 11 | description: | 12 | WAsmcloud SHell - the comprehensive command-line tool for wasmCloud development 13 | vendor: wasmcloud 14 | homepage: https://wasmcloud.com 15 | license: Apache-2.0 16 | contents: 17 | - src: target/aarch64-unknown-linux-gnu/release/wash 18 | dst: /usr/local/bin/wash 19 | -------------------------------------------------------------------------------- /crates/wash-lib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wash-lib" 3 | version = "0.12.1" 4 | authors = ["wasmCloud Team"] 5 | categories = ["wasm", "wasmcloud"] 6 | description = "wasmcloud Shell (wash) libraries" 7 | edition = "2021" 8 | keywords = ["webassembly", "wasmcloud", "wash", "cli"] 9 | license = "Apache-2.0" 10 | readme = "README.md" 11 | repository = "https://github.com/wasmcloud/wash" 12 | 13 | [badges] 14 | maintenance = { status = "actively-developed" } 15 | 16 | [features] 17 | default = ["start", "parser", "nats"] 18 | start = ["semver"] 19 | parser = ["config", "semver", "serde", "serde_json"] 20 | cli = [ 21 | "clap", 22 | "term-table", 23 | "console", 24 | "dialoguer", 25 | "heck", 26 | "ignore", 27 | "indicatif", 28 | "path-absolutize", 29 | ] 30 | nats = ["async-nats", "wadm"] 31 | docs = ["wasmcloud-component-adapters/docs"] 32 | 33 | [package.metadata.docs.rs] 34 | features = ["start", "parser", "nats", "docs"] 35 | 36 | [dependencies] 37 | anyhow = { workspace = true } 38 | async-compression = { workspace = true, features = ["tokio", "gzip"] } 39 | async-nats = { workspace = true, optional = true } 40 | bytes = { version = "1", features = ["serde"] } 41 | cargo_metadata = "0.18" 42 | cargo_toml = { workspace = true } 43 | chrono = "0.4" 44 | clap = { workspace = true, features = ["derive", "env"], optional = true } 45 | cloudevents-sdk = { workspace = true } 46 | command-group = { workspace = true, features = ["with-tokio"] } 47 | config = { workspace = true, features = ["toml"], optional = true } 48 | console = { workspace = true, optional = true } 49 | dialoguer = { workspace = true, optional = true } 50 | dirs = { workspace = true } 51 | futures = { workspace = true } 52 | heck = { workspace = true, optional = true } 53 | ignore = { workspace = true, optional = true } 54 | indicatif = { workspace = true, optional = true } 55 | log = { workspace = true } 56 | nkeys = { workspace = true } 57 | oci-distribution = { workspace = true, features = ["rustls-tls"] } 58 | path-absolutize = { workspace = true, features = [ 59 | "once_cell_cache", 60 | ], optional = true } 61 | provider-archive = { workspace = true } 62 | regex = { workspace = true } 63 | reqwest = { workspace = true, features = ["json", "rustls-tls", "stream"] } 64 | rmp-serde = "1" 65 | semver = { workspace = true, features = ["serde"], optional = true } 66 | serde = { workspace = true, features = ["derive"], optional = true } 67 | serde-transcode = "1" 68 | serde_cbor = "0.11" 69 | serde_json = { workspace = true, optional = true } 70 | serde_with = { workspace = true } 71 | tempfile = { workspace = true } 72 | term-table = { workspace = true, optional = true } 73 | thiserror = { workspace = true } 74 | time = "0.3" 75 | tokio = { workspace = true, features = ["process", "fs", "io-std"] } 76 | tokio-stream = { workspace = true } 77 | tokio-tar = { workspace = true } 78 | tokio-util = { workspace = true } 79 | toml = { workspace = true } 80 | url = { workspace = true } 81 | wadm = { workspace = true, optional = true } 82 | walkdir = { workspace = true } 83 | wascap = { workspace = true } 84 | wasm-encoder = { workspace = true } 85 | wasmbus-rpc = { workspace = true } 86 | wasmcloud-component-adapters = { workspace = true } 87 | wasmcloud-control-interface = { workspace = true } 88 | wat = { workspace = true } 89 | weld-codegen = { workspace = true } 90 | wit-bindgen-core = { workspace = true } 91 | wit-bindgen-go = { workspace = true } 92 | wit-component = { workspace = true } 93 | wit-parser = { workspace = true } 94 | 95 | [dev-dependencies] 96 | claims = { workspace = true } 97 | dirs = { workspace = true } 98 | tempfile = { workspace = true } 99 | test-case = { workspace = true } 100 | tokio = { workspace = true, features = ["full"] } 101 | wasmparser = { workspace = true } -------------------------------------------------------------------------------- /crates/wash-lib/README.md: -------------------------------------------------------------------------------- 1 | [![crates.io](https://img.shields.io/crates/v/wash-lib.svg)](https://crates.io/crates/wash-lib)  2 | # wash Libary 3 | 4 | A crate that implements the functionality behind the wasmCloud shell 5 | 6 | The `wash` command line interface [wash](https://github.com/wasmcloud/wash) is a great place 7 | to find examples on how to fully utilize this library. -------------------------------------------------------------------------------- /crates/wash-lib/src/cli/dev.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use console::style; 3 | use wasmcloud_control_interface::Client; 4 | 5 | use crate::{ 6 | actor::{start_actor, stop_actor, StartActorArgs}, 7 | build::{build_project, SignConfig}, 8 | context::default_timeout_ms, 9 | generate::emoji, 10 | id::{ModuleId, ServerId}, 11 | parser::{ProjectConfig, TypeConfig}, 12 | }; 13 | 14 | /// Perform a single execution of the dev loop for an artifact 15 | pub async fn run_dev_loop( 16 | project_cfg: &ProjectConfig, 17 | actor_id: ModuleId, 18 | actor_ref: &str, 19 | host_id: ServerId, 20 | ctl_client: &Client, 21 | sign_cfg: Option, 22 | ) -> Result<()> { 23 | let built_artifact_path = build_project(project_cfg, sign_cfg)?.canonicalize()?; 24 | 25 | // Restart the artifact so that changes can be observed 26 | match project_cfg.project_type { 27 | TypeConfig::Interface(_) | TypeConfig::Provider(_) => { 28 | eprintln!( 29 | "{} {}", 30 | emoji::WARN, 31 | style("`wash build` interfaces and providers are not yet supported, skipping...") 32 | .bold(), 33 | ); 34 | } 35 | TypeConfig::Actor(_) => { 36 | eprintln!( 37 | "{} {}", 38 | emoji::RECYCLE, 39 | style(format!( 40 | "restarting actor @ [{}]...", 41 | built_artifact_path.display() 42 | )) 43 | .bold(), 44 | ); 45 | // TODO: Just use update actor here 46 | stop_actor( 47 | ctl_client, 48 | &host_id, 49 | &actor_id, 50 | None, 51 | default_timeout_ms(), 52 | false, 53 | ) 54 | .await?; 55 | start_actor(StartActorArgs { 56 | ctl_client, 57 | host_id: &host_id, 58 | actor_ref, 59 | count: 1, 60 | skip_wait: false, 61 | timeout_ms: None, 62 | }) 63 | .await?; 64 | } 65 | } 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /crates/wash-lib/src/cli/get.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use clap::Parser; 3 | use wasmcloud_control_interface::{Host, HostInventory}; 4 | 5 | use crate::{common::boxed_err_to_anyhow, config::WashConnectionOptions, id::ServerId}; 6 | 7 | use super::CliConnectionOpts; 8 | 9 | #[derive(Debug, Clone, Parser)] 10 | pub struct GetClaimsCommand { 11 | #[clap(flatten)] 12 | pub opts: CliConnectionOpts, 13 | } 14 | 15 | #[derive(Debug, Clone, Parser)] 16 | pub struct GetHostInventoriesCommand { 17 | #[clap(flatten)] 18 | pub opts: CliConnectionOpts, 19 | 20 | /// Host ID to retrieve inventory for. If not provided, wash will query the inventories of all running hosts. 21 | #[clap(name = "host-id", value_parser)] 22 | pub host_id: Option, 23 | } 24 | 25 | #[derive(Debug, Clone, Parser)] 26 | pub struct GetLinksCommand { 27 | #[clap(flatten)] 28 | pub opts: CliConnectionOpts, 29 | } 30 | 31 | #[derive(Debug, Clone, Parser)] 32 | pub struct GetHostsCommand { 33 | #[clap(flatten)] 34 | pub opts: CliConnectionOpts, 35 | } 36 | 37 | #[derive(Debug, Clone, Parser)] 38 | pub enum GetCommand { 39 | /// Retrieve all known links in the lattice 40 | #[clap(name = "links")] 41 | Links(GetLinksCommand), 42 | 43 | /// Retrieve all known claims inside the lattice 44 | #[clap(name = "claims")] 45 | Claims(GetClaimsCommand), 46 | 47 | /// Retrieve all responsive hosts in the lattice 48 | #[clap(name = "hosts")] 49 | Hosts(GetHostsCommand), 50 | 51 | /// Retrieve inventory a given host on in the lattice 52 | #[clap(name = "inventory", alias = "inventories")] 53 | HostInventories(GetHostInventoriesCommand), 54 | } 55 | 56 | /// Retreive host inventory 57 | pub async fn get_host_inventories(cmd: GetHostInventoriesCommand) -> Result> { 58 | let wco: WashConnectionOptions = cmd.opts.try_into()?; 59 | let client = wco.into_ctl_client(None).await?; 60 | 61 | let host_ids = if let Some(host_id) = cmd.host_id { 62 | vec![host_id.to_string()] 63 | } else { 64 | let hosts = client.get_hosts().await.map_err(boxed_err_to_anyhow)?; 65 | match hosts.len() { 66 | 0 => anyhow::bail!("No hosts are available for inventory query."), 67 | _ => hosts.iter().map(|h| h.id.clone()).collect(), 68 | } 69 | }; 70 | 71 | let futs = host_ids 72 | .into_iter() 73 | .map(|host_id| (client.clone(), host_id)) 74 | .map(|(client, host_id)| async move { 75 | client 76 | .get_host_inventory(&host_id.clone()) 77 | .await 78 | .map_err(boxed_err_to_anyhow) 79 | }); 80 | futures::future::join_all(futs) 81 | .await 82 | .into_iter() 83 | .collect::>>() 84 | } 85 | 86 | /// Retrieve hosts 87 | pub async fn get_hosts(cmd: GetHostsCommand) -> Result> { 88 | let wco: WashConnectionOptions = cmd.opts.try_into()?; 89 | let client = wco.into_ctl_client(None).await?; 90 | client 91 | .get_hosts() 92 | .await 93 | .map_err(boxed_err_to_anyhow) 94 | .context("Was able to connect to NATS, but failed to get hosts.") 95 | } 96 | -------------------------------------------------------------------------------- /crates/wash-lib/src/cli/output.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use wasmbus_rpc::core::ActorLinks; 3 | 4 | use serde::Deserialize; 5 | use wasmcloud_control_interface::{Host, HostInventory}; 6 | 7 | /// JSON Output of the `wash start` command 8 | #[derive(Debug, Deserialize)] 9 | pub struct StartCommandOutput { 10 | pub actor_id: Option, 11 | pub actor_ref: Option, 12 | 13 | pub provider_id: Option, 14 | pub provider_ref: Option, 15 | pub contract_id: Option, 16 | pub link_name: Option, 17 | 18 | pub host_id: Option, 19 | pub success: bool, 20 | } 21 | 22 | /// JSON Output representation of the `wash stop` command 23 | #[derive(Debug, Deserialize)] 24 | pub struct StopCommandOutput { 25 | pub host_id: Option, 26 | pub result: String, 27 | 28 | pub actor_ref: Option, 29 | pub actor_id: Option, 30 | 31 | pub provider_id: Option, 32 | pub link_name: Option, 33 | pub contract_id: Option, 34 | pub provider_ref: Option, 35 | 36 | pub success: bool, 37 | } 38 | 39 | /// JSON output representation of the `wash link query` command 40 | #[derive(Debug, Deserialize)] 41 | pub struct LinkQueryCommandOutput { 42 | pub links: Vec>, 43 | pub success: bool, 44 | } 45 | 46 | /// JSON output representation of the `wash get hosts` command 47 | #[derive(Debug, Clone, Deserialize)] 48 | pub struct GetHostsCommandOutput { 49 | pub success: bool, 50 | pub hosts: Vec, 51 | } 52 | 53 | /// JSON output representation of the `wash get inventory` command 54 | #[derive(Debug, Clone, Deserialize)] 55 | pub struct GetHostInventoriesCommandOutput { 56 | pub success: bool, 57 | pub inventories: Vec, 58 | } 59 | 60 | /// JSON output representation of the `wash get claims` command 61 | #[derive(Debug, Deserialize)] 62 | pub struct GetClaimsCommandOutput { 63 | pub claims: Vec>, 64 | pub success: bool, 65 | } 66 | 67 | /// JSON output representation of the `wash dev` command 68 | #[derive(Debug, Deserialize)] 69 | pub struct DevCommandOutput { 70 | pub success: bool, 71 | } 72 | 73 | /// JSON output representation of the `wash dev` command 74 | #[derive(Debug, Deserialize)] 75 | pub struct ScaleCommandOutput { 76 | pub success: bool, 77 | pub result: String, 78 | } 79 | -------------------------------------------------------------------------------- /crates/wash-lib/src/cli/par.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use provider_archive::ProviderArchive; 3 | use std::path::PathBuf; 4 | 5 | pub struct ParCreateArgs { 6 | pub capid: String, 7 | pub vendor: String, 8 | pub revision: Option, 9 | pub version: Option, 10 | pub schema: Option, 11 | pub name: String, 12 | pub arch: String, 13 | } 14 | 15 | pub async fn create_provider_archive( 16 | ParCreateArgs { 17 | capid, 18 | vendor, 19 | revision, 20 | version, 21 | schema, 22 | name, 23 | arch, 24 | }: ParCreateArgs, 25 | binary_bytes: &[u8], 26 | ) -> Result { 27 | let mut par = ProviderArchive::new(&capid, &name, &vendor, revision, version); 28 | 29 | par.add_library(&arch, binary_bytes) 30 | .map_err(convert_error)?; 31 | 32 | if let Some(ref schema) = schema { 33 | let bytes = std::fs::read(schema)?; 34 | par.set_schema( 35 | serde_json::from_slice::(&bytes) 36 | .with_context(|| "Unable to parse JSON from file contents".to_string())?, 37 | ) 38 | .map_err(convert_error) 39 | .with_context(|| format!("Error parsing JSON schema from file '{:?}'", schema))?; 40 | } 41 | 42 | Ok(par) 43 | } 44 | 45 | pub async fn insert_provider_binary( 46 | arch: String, 47 | binary_bytes: &[u8], 48 | mut par: ProviderArchive, 49 | ) -> Result { 50 | par.add_library(&arch, binary_bytes) 51 | .map_err(convert_error)?; 52 | 53 | Ok(par) 54 | } 55 | 56 | /// Converts error from Send + Sync error to standard anyhow error 57 | pub fn convert_error(e: Box) -> anyhow::Error { 58 | anyhow!(e.to_string()) 59 | } 60 | -------------------------------------------------------------------------------- /crates/wash-lib/src/cli/registry.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug, Clone)] 4 | pub struct AuthOpts { 5 | /// OCI username, if omitted anonymous authentication will be used 6 | #[clap( 7 | short = 'u', 8 | long = "user", 9 | env = "WASH_REG_USER", 10 | hide_env_values = true 11 | )] 12 | pub user: Option, 13 | 14 | /// OCI password, if omitted anonymous authentication will be used 15 | #[clap( 16 | short = 'p', 17 | long = "password", 18 | env = "WASH_REG_PASSWORD", 19 | hide_env_values = true 20 | )] 21 | pub password: Option, 22 | 23 | /// Allow insecure (HTTP) registry connections 24 | #[clap(long = "insecure")] 25 | pub insecure: bool, 26 | } 27 | 28 | #[derive(Debug, Clone, Subcommand)] 29 | pub enum RegistryCommand { 30 | /// Pull an artifact from an OCI compliant registry 31 | #[clap(name = "pull")] 32 | Pull(RegistryPullCommand), 33 | /// Push an artifact to an OCI compliant registry 34 | #[clap(name = "push")] 35 | Push(RegistryPushCommand), 36 | /// Ping (test url) to see if the OCI url has an artifact 37 | #[clap(name = "ping")] 38 | Ping(RegistryPingCommand), 39 | } 40 | 41 | #[derive(Parser, Debug, Clone)] 42 | pub struct RegistryPullCommand { 43 | /// URL of artifact 44 | #[clap(name = "url")] 45 | pub url: String, 46 | 47 | /// File destination of artifact 48 | #[clap(long = "destination")] 49 | pub destination: Option, 50 | 51 | /// Digest to verify artifact against 52 | #[clap(short = 'd', long = "digest")] 53 | pub digest: Option, 54 | 55 | /// Allow latest artifact tags 56 | #[clap(long = "allow-latest")] 57 | pub allow_latest: bool, 58 | 59 | #[clap(flatten)] 60 | pub opts: AuthOpts, 61 | } 62 | 63 | #[derive(Parser, Debug, Clone)] 64 | pub struct RegistryPushCommand { 65 | /// URL to push artifact to 66 | #[clap(name = "url")] 67 | pub url: String, 68 | 69 | /// Path to artifact to push 70 | #[clap(name = "artifact")] 71 | pub artifact: String, 72 | 73 | /// Path to config file, if omitted will default to a blank configuration 74 | #[clap(short = 'c', long = "config")] 75 | pub config: Option, 76 | 77 | /// Allow latest artifact tags 78 | #[clap(long = "allow-latest")] 79 | pub allow_latest: bool, 80 | 81 | /// Optional set of annotations to apply to the OCI artifact manifest 82 | #[clap(short = 'a', long = "annotation", name = "annotations")] 83 | pub annotations: Option>, 84 | 85 | #[clap(flatten)] 86 | pub opts: AuthOpts, 87 | } 88 | 89 | #[derive(Parser, Debug, Clone)] 90 | pub struct RegistryPingCommand { 91 | /// URL of artifact 92 | #[clap(name = "url")] 93 | pub url: String, 94 | 95 | #[clap(flatten)] 96 | pub opts: AuthOpts, 97 | } 98 | -------------------------------------------------------------------------------- /crates/wash-lib/src/cli/scale.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | 4 | use crate::{ 5 | actor::scale_actor, 6 | cli::{labels_vec_to_hashmap, CliConnectionOpts, CommandOutput}, 7 | config::WashConnectionOptions, 8 | id::ServerId, 9 | }; 10 | 11 | #[derive(Debug, Clone, Parser)] 12 | pub enum ScaleCommand { 13 | /// Scale an actor running in a host to a certain level of concurrency 14 | #[clap(name = "actor")] 15 | Actor(ScaleActorCommand), 16 | } 17 | 18 | #[derive(Debug, Clone, Parser)] 19 | pub struct ScaleActorCommand { 20 | #[clap(flatten)] 21 | pub opts: CliConnectionOpts, 22 | 23 | /// Id of host 24 | #[clap(name = "host-id", value_parser)] 25 | pub host_id: ServerId, 26 | 27 | /// Actor reference, e.g. the OCI URL for the actor. 28 | #[clap(name = "actor-ref")] 29 | pub actor_ref: String, 30 | 31 | /// Maximum number of instances this actor can run concurrently. Omitting this value means there is no maximum. 32 | #[clap(short = 'c', long = "max-concurrent", alias = "max", alias = "count")] 33 | pub max_concurrent: Option, 34 | 35 | /// Optional set of annotations used to describe the nature of this actor scale command. 36 | /// For example, autonomous agents may wish to “tag” scale requests as part of a given deployment 37 | #[clap(short = 'a', long = "annotations")] 38 | pub annotations: Vec, 39 | } 40 | 41 | pub async fn handle_scale_actor(cmd: ScaleActorCommand) -> Result { 42 | let wco: WashConnectionOptions = cmd.opts.try_into()?; 43 | let client = wco.into_ctl_client(None).await?; 44 | 45 | let annotations = labels_vec_to_hashmap(cmd.annotations)?; 46 | 47 | scale_actor( 48 | &client, 49 | &cmd.host_id, 50 | &cmd.actor_ref, 51 | cmd.max_concurrent, 52 | Some(annotations), 53 | ) 54 | .await?; 55 | 56 | let max = cmd 57 | .max_concurrent 58 | .map(|max| max.to_string()) 59 | .unwrap_or_else(|| "unbounded".to_string()); 60 | 61 | Ok(CommandOutput::from_key_and_text( 62 | "result", 63 | format!( 64 | "Request to scale actor {} to {} max concurrent instances received", 65 | cmd.actor_ref, max 66 | ), 67 | )) 68 | } 69 | -------------------------------------------------------------------------------- /crates/wash-lib/src/cli/spy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use futures::StreamExt; 4 | 5 | use super::{CliConnectionOpts, CommandOutput}; 6 | use crate::{config::WashConnectionOptions, spier::Spier}; 7 | 8 | #[derive(Debug, Parser, Clone)] 9 | pub struct SpyCommand { 10 | /// Actor ID or name to match on. If a name is provided, we will attempt to resolve it to an ID 11 | /// by checking if the actor_name or call_alias fields from the actor's claims contains the 12 | /// given string. If more than one matches, then an error will be returned indicating the 13 | /// options to choose from 14 | #[clap(name = "actor")] 15 | pub actor: String, 16 | 17 | #[clap(flatten)] 18 | pub opts: CliConnectionOpts, 19 | } 20 | 21 | /// Handles the spy command, printing all output to stdout until the command is interrupted 22 | pub async fn handle_command(cmd: SpyCommand) -> Result { 23 | let wco: WashConnectionOptions = cmd.opts.try_into()?; 24 | let ctl_client = wco.clone().into_ctl_client(None).await?; 25 | let nats_client = wco.into_nats_client().await?; 26 | 27 | let mut spier = Spier::new(&cmd.actor, &ctl_client, &nats_client).await?; 28 | 29 | println!("Spying on actor {}\n", spier.actor_id()); 30 | 31 | while let Some(msg) = spier.next().await { 32 | println!( 33 | r#" 34 | [{}] 35 | From: {:<25}To: {:<25}Host: {} 36 | 37 | Operation: {} 38 | Message: {}"#, 39 | msg.timestamp, 40 | msg.from, 41 | msg.to, 42 | msg.invocation.host_id, 43 | msg.invocation.operation, 44 | msg.message 45 | ); 46 | } 47 | 48 | println!("Message subscribers closed"); 49 | 50 | Ok(CommandOutput::default()) 51 | } 52 | -------------------------------------------------------------------------------- /crates/wash-lib/src/cli/update.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use clap::Parser; 3 | 4 | use crate::{ 5 | actor::update_actor, 6 | config::WashConnectionOptions, 7 | id::{ModuleId, ServerId}, 8 | }; 9 | 10 | use super::{CliConnectionOpts, CommandOutput}; 11 | 12 | #[derive(Debug, Clone, Parser)] 13 | pub enum UpdateCommand { 14 | /// Update an actor running in a host to a newer version 15 | #[clap(name = "actor")] 16 | Actor(UpdateActorCommand), 17 | } 18 | 19 | #[derive(Debug, Clone, Parser)] 20 | pub struct UpdateActorCommand { 21 | #[clap(flatten)] 22 | pub opts: CliConnectionOpts, 23 | 24 | /// Id of host 25 | #[clap(name = "host-id", value_parser)] 26 | pub host_id: ServerId, 27 | 28 | /// Actor Id, e.g. the public key for the actor 29 | #[clap(name = "actor-id", value_parser)] 30 | pub actor_id: ModuleId, 31 | 32 | /// Actor reference, e.g. the OCI URL for the actor. 33 | #[clap(name = "new-actor-ref")] 34 | pub new_actor_ref: String, 35 | } 36 | 37 | pub async fn handle_update_actor(cmd: UpdateActorCommand) -> Result { 38 | let wco: WashConnectionOptions = cmd.opts.try_into()?; 39 | let client = wco.into_ctl_client(None).await?; 40 | 41 | let ack = update_actor(&client, &cmd.host_id, &cmd.actor_id, &cmd.new_actor_ref).await?; 42 | if !ack.accepted { 43 | bail!("Operation failed: {}", ack.error); 44 | } 45 | 46 | Ok(CommandOutput::from_key_and_text( 47 | "result", 48 | format!("Actor {} updated to {}", cmd.actor_id, cmd.new_actor_ref), 49 | )) 50 | } 51 | -------------------------------------------------------------------------------- /crates/wash-lib/src/common.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::id::ModuleId; 4 | 5 | const CLAIMS_CALL_ALIAS: &str = "call_alias"; 6 | pub(crate) const CLAIMS_NAME: &str = "name"; 7 | pub(crate) const CLAIMS_SUBJECT: &str = "sub"; 8 | 9 | /// Converts error from Send + Sync error to standard anyhow error 10 | pub(crate) fn boxed_err_to_anyhow(e: Box) -> anyhow::Error { 11 | anyhow::anyhow!(e.to_string()) 12 | } 13 | 14 | #[derive(Debug, thiserror::Error)] 15 | pub enum FindIdError { 16 | /// No matches were found 17 | #[error("No actor found with the search term")] 18 | NoMatches, 19 | /// Multiple matches were found. The vector contains the list of actors that matched 20 | #[error("Multiple actors found with the search term: {0:?}")] 21 | MultipleMatches(Vec), 22 | #[error(transparent)] 23 | Error(#[from] anyhow::Error), 24 | } 25 | 26 | /// Given a string, attempts to resolve an actor ID. Returning the actor ID and an optional friendly name 27 | /// 28 | /// If the string is a valid actor ID, it will be returned unchanged. Resolution works by checking 29 | /// if the actor_name or call_alias fields from the actor's claims contains the given string. If 30 | /// more than one matches, then an error will be returned indicating the options to choose from 31 | pub async fn find_actor_id( 32 | value: &str, 33 | ctl_client: &wasmcloud_control_interface::Client, 34 | ) -> Result<(ModuleId, Option), FindIdError> { 35 | if let Ok(id) = ModuleId::from_str(value) { 36 | return Ok((id, None)); 37 | } 38 | 39 | // Case insensitive searching here to make things nicer 40 | let value = value.to_lowercase(); 41 | // If it wasn't an ID, get the claims 42 | let claims = ctl_client 43 | .get_claims() 44 | .await 45 | .map_err(|e| FindIdError::Error(anyhow::anyhow!("Unable to get claims: {}", e)))?; 46 | let all_matches = claims 47 | .iter() 48 | .filter_map(|v| { 49 | let id = v 50 | .get(CLAIMS_SUBJECT) 51 | .map(|s| s.as_str()) 52 | .unwrap_or_default(); 53 | // If it isn't a module, just skip 54 | let id = match ModuleId::from_str(id) { 55 | Ok(id) => id, 56 | Err(_) => return None, 57 | }; 58 | (v.get(CLAIMS_CALL_ALIAS) 59 | .map(|s| s.to_lowercase()) 60 | .unwrap_or_default() 61 | .contains(&value) 62 | || v.get(CLAIMS_NAME) 63 | .map(|s| s.to_ascii_lowercase()) 64 | .unwrap_or_default() 65 | .contains(&value)) 66 | .then(|| (id, v.get(CLAIMS_NAME).map(|s| s.to_string()))) 67 | }) 68 | .collect::>(); 69 | if all_matches.is_empty() { 70 | Err(FindIdError::NoMatches) 71 | } else if all_matches.len() > 1 { 72 | Err(FindIdError::MultipleMatches( 73 | all_matches 74 | .into_iter() 75 | .map(|(id, friendly_name)| { 76 | if let Some(name) = friendly_name { 77 | format!("{} ({})", id, name) 78 | } else { 79 | id.into_string() 80 | } 81 | }) 82 | .collect(), 83 | )) 84 | } else { 85 | // SAFETY: We know we have exactly one match at this point 86 | Ok(all_matches.into_iter().next().unwrap()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /crates/wash-lib/src/drain.rs: -------------------------------------------------------------------------------- 1 | //! Remove cached wasmCloud files like OCI artifacts or downloaded binaries 2 | 3 | use std::{env, fs, io::Result, path::PathBuf}; 4 | 5 | use crate::config::{downloads_dir, model_cache_dir}; 6 | 7 | /// A type that allows you to clean up (i.e. drain) a set of caches and folders used by wasmcloud 8 | #[derive(Debug, Clone)] 9 | #[cfg_attr(feature = "cli", derive(clap::Subcommand))] 10 | pub enum Drain { 11 | /// Remove all cached files created by wasmcloud 12 | All, 13 | /// Remove cached files downloaded from OCI registries by wasmCloud 14 | Oci, 15 | /// Remove cached binaries extracted from provider archives 16 | Lib, 17 | /// Remove cached smithy files downloaded from model urls 18 | Smithy, 19 | /// Remove downloaded and generated files from launching wasmCloud hosts 20 | Downloads, 21 | } 22 | 23 | impl IntoIterator for &Drain { 24 | type Item = PathBuf; 25 | type IntoIter = std::vec::IntoIter; 26 | 27 | fn into_iter(self) -> Self::IntoIter { 28 | let paths = match self { 29 | Drain::All => vec![ 30 | /* Lib */ env::temp_dir().join("wasmcloudcache"), 31 | /* Oci */ env::temp_dir().join("wasmcloud_ocicache"), 32 | /* Smithy */ model_cache_dir().unwrap_or_default(), 33 | /* Downloads */ downloads_dir().unwrap_or_default(), 34 | ], 35 | Drain::Lib => vec![env::temp_dir().join("wasmcloudcache")], 36 | Drain::Oci => vec![env::temp_dir().join("wasmcloud_ocicache")], 37 | Drain::Smithy => vec![model_cache_dir().unwrap_or_default()], 38 | Drain::Downloads => vec![downloads_dir().unwrap_or_default()], 39 | }; 40 | paths.into_iter() 41 | } 42 | } 43 | 44 | impl Drain { 45 | /// Cleans up all data based on the type of Drain requested. Returns a list of paths that were 46 | /// cleaned 47 | pub fn drain(self) -> Result> { 48 | self.into_iter() 49 | .filter(|path| path.exists()) 50 | .map(remove_dir_contents) 51 | .collect::>>() 52 | } 53 | } 54 | 55 | fn remove_dir_contents(path: PathBuf) -> Result { 56 | for entry in fs::read_dir(&path)? { 57 | let path = entry?.path(); 58 | if path.is_dir() { 59 | fs::remove_dir_all(&path)?; 60 | } else if path.is_file() { 61 | fs::remove_file(&path)?; 62 | } 63 | } 64 | Ok(path) 65 | } 66 | 67 | #[cfg(test)] 68 | mod test { 69 | use super::*; 70 | 71 | #[test] 72 | fn test_dir_clean() { 73 | let tempdir = tempfile::tempdir().expect("Unable to create tempdir"); 74 | 75 | let subdir = tempdir.path().join("foobar"); 76 | fs::create_dir(&subdir).unwrap(); 77 | 78 | // Create the files and drop the handles 79 | { 80 | fs::File::create(subdir.join("baz")).unwrap(); 81 | fs::File::create(tempdir.path().join("baz")).unwrap(); 82 | } 83 | 84 | remove_dir_contents(tempdir.path().to_owned()) 85 | .expect("Shouldn't get an error when cleaning files"); 86 | assert!( 87 | tempdir 88 | .path() 89 | .read_dir() 90 | .expect("Top level dir should still exist") 91 | .next() 92 | .is_none(), 93 | "Directory should be empty" 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /crates/wash-lib/src/generate/emoji.rs: -------------------------------------------------------------------------------- 1 | //! A few standard emojis used in interactive prompting 2 | // This file is from emoji.rs from cargo-generate 3 | // source: https://github.com/cargo-generate/cargo-generate 4 | // version: 0.9.0 5 | // license: MIT/Apache-2.0 6 | // 7 | use console::Emoji; 8 | 9 | pub static ERROR: Emoji<'_, '_> = Emoji("⛔ ", ""); 10 | pub static SPARKLE: Emoji<'_, '_> = Emoji("✨ ", ""); 11 | pub static GREEN_CHECK: Emoji<'_, '_> = Emoji("✅ ", ""); 12 | pub static WARN: Emoji<'_, '_> = Emoji("⚠️ ", ""); 13 | pub static WRENCH: Emoji<'_, '_> = Emoji("🔧 ", ""); 14 | pub static SHRUG: Emoji<'_, '_> = Emoji("🤷 ", ""); 15 | pub static INFO: Emoji<'_, '_> = Emoji("💡 ", ""); 16 | pub static RECYCLE: Emoji<'_, '_> = Emoji("♻️ ", ""); 17 | pub static INFO_SQUARE: Emoji<'_, '_> = Emoji("ℹ️️ ", ""); 18 | pub static HOURGLASS_DRAINING: Emoji<'_, '_> = Emoji("⏳ ", ""); 19 | pub static HOURGLASS_FULL: Emoji<'_, '_> = Emoji("⌛ ", ""); 20 | pub static CONSTRUCTION_BARRIER: Emoji<'_, '_> = Emoji("🚧 ", ""); 21 | -------------------------------------------------------------------------------- /crates/wash-lib/src/generate/favorites.toml: -------------------------------------------------------------------------------- 1 | # Default template choices for project generation 2 | # 3 | # Template type is `[[actor]]`, `[[interface]]`, or `[[provider]]` 4 | # There should be at least one of each template type in this file 5 | # 6 | # Settings per template: 7 | # --------------------- 8 | # name short template name - required 9 | # description one-line template description - required 10 | # git https or ssh uri - either git or path is required 11 | # subfolder relative path within git repo - optional if git uri is used 12 | # branch git branch name - optional if git uri is used 13 | # path path to template on disk - either git or path is required 14 | 15 | [[actor]] 16 | name = "hello" 17 | description = "a hello-world actor (in Rust) that responds over an http connection" 18 | git = "wasmCloud/project-templates" 19 | subfolder = "actor/hello" 20 | 21 | [[actor]] 22 | name = "echo-tinygo" 23 | description = "a hello-world actor (in TinyGo) that responds over an http connection" 24 | git = "wasmCloud/project-templates" 25 | subfolder = "actor/echo-tinygo" 26 | 27 | [[actor]] 28 | name = "echo-messaging" 29 | description = "a hello-world actor (in Rust) that echoes a request back over a NATS connection" 30 | git = "wasmCloud/project-templates" 31 | subfolder = "actor/echo-messaging" 32 | 33 | [[actor]] 34 | name = "kvcounter" 35 | description = "an example actor (in Rust) that increments a counter in a key-value store" 36 | git = "wasmCloud/project-templates" 37 | subfolder = "actor/kvcounter" 38 | 39 | [[interface]] 40 | name = "converter-interface" 41 | description = "an interface for actor-to-actor messages with a single Convert method" 42 | git = "wasmCloud/project-templates" 43 | subfolder = "interface/converter-actor" 44 | 45 | [[interface]] 46 | name = "factorial-interface" 47 | description = "an interface for a capability provider with capability contract" 48 | git = "wasmCloud/project-templates" 49 | subfolder = "interface/factorial" 50 | 51 | [[provider]] 52 | name = "factorial-provider" 53 | description = "a capability provider that computes factorials" 54 | git = "wasmCloud/project-templates" 55 | subfolder = "provider/factorial" 56 | 57 | [[provider]] 58 | name = "messaging-provider" 59 | description = "a capability provider that implements pubsub messaging" 60 | git = "wasmCloud/project-templates" 61 | subfolder = "provider/messaging" -------------------------------------------------------------------------------- /crates/wash-lib/src/generate/git.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, bail, Result}; 2 | use std::{path::PathBuf, process::Stdio}; 3 | use tokio::process::Command; 4 | 5 | pub struct CloneTemplate { 6 | /// temp folder where project will be cloned - deleted after 'wash new' completes 7 | pub clone_tmp: PathBuf, 8 | /// github repository URL, e.g., "https://github.com/wasmcloud/project-templates". 9 | /// For convenience, either prefix 'https://' or 'https://github.com' may be omitted. 10 | /// ssh urls may be used if ssh-config is setup appropriately. 11 | /// If a private repository is used, user will be prompted for credentials. 12 | pub repo_url: String, 13 | /// sub-folder of project template within the repo, e.g. "actor/hello" 14 | pub sub_folder: Option, 15 | /// repo branch, e.g., main 16 | pub repo_branch: String, 17 | } 18 | 19 | pub async fn clone_git_template(opts: CloneTemplate) -> Result<()> { 20 | let cwd = 21 | std::env::current_dir().map_err(|e| anyhow!("could not get current directory: {}", e))?; 22 | std::env::set_current_dir(&opts.clone_tmp).map_err(|e| { 23 | anyhow!( 24 | "could not cd to tmp dir {}: {}", 25 | opts.clone_tmp.display(), 26 | e 27 | ) 28 | })?; 29 | // for convenience, allow omission of prefix 'https://' or 'https://github.com' 30 | let repo_url = { 31 | if opts.repo_url.starts_with("http://") || opts.repo_url.starts_with("https://") { 32 | opts.repo_url 33 | } else if opts.repo_url.starts_with("github.com/") { 34 | format!("https://{}", &opts.repo_url) 35 | } else { 36 | format!( 37 | "https://github.com/{}", 38 | opts.repo_url.trim_start_matches('/') 39 | ) 40 | } 41 | }; 42 | 43 | let cmd_out = Command::new("git") 44 | .args(["clone", &repo_url, "--depth", "1", "--no-checkout", "."]) 45 | .stdin(Stdio::piped()) 46 | .stdout(Stdio::piped()) 47 | .stderr(Stdio::piped()) 48 | .spawn()? 49 | .wait_with_output() 50 | .await?; 51 | if !cmd_out.status.success() { 52 | bail!( 53 | "git clone error: {}", 54 | String::from_utf8_lossy(&cmd_out.stderr) 55 | ); 56 | } 57 | 58 | if let Some(sub_folder) = opts.sub_folder { 59 | let cmd_out = Command::new("git") 60 | .args(["sparse-checkout", "set", &sub_folder]) 61 | .stdin(Stdio::piped()) 62 | .stdout(Stdio::piped()) 63 | .stderr(Stdio::piped()) 64 | .spawn()? 65 | .wait_with_output() 66 | .await?; 67 | if !cmd_out.status.success() { 68 | bail!( 69 | "git sparse-checkout set error: {}", 70 | String::from_utf8_lossy(&cmd_out.stderr) 71 | ); 72 | } 73 | } 74 | 75 | let cmd_out = Command::new("git") 76 | .args(["checkout", &opts.repo_branch]) 77 | .stdin(Stdio::piped()) 78 | .stdout(Stdio::piped()) 79 | .stderr(Stdio::piped()) 80 | .spawn()? 81 | .wait_with_output() 82 | .await?; 83 | if !cmd_out.status.success() { 84 | bail!( 85 | "git checkout error: {}", 86 | String::from_utf8_lossy(&cmd_out.stderr) 87 | ); 88 | } 89 | std::env::set_current_dir(cwd)?; 90 | Ok(()) 91 | } 92 | 93 | /// Information to find a specific commit in a Git repository. 94 | #[allow(dead_code)] 95 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 96 | pub enum GitReference { 97 | /// From a tag. 98 | Tag(String), 99 | /// From a branch. 100 | Branch(String), 101 | /// From a specific revision. 102 | Rev(String), 103 | /// The default branch of the repository, the reference named `HEAD`. 104 | DefaultBranch, 105 | } 106 | -------------------------------------------------------------------------------- /crates/wash-lib/src/keys/mod.rs: -------------------------------------------------------------------------------- 1 | //! A common set of types and traits for managing collections of nkeys used for wasmCloud 2 | 3 | use anyhow::Result; 4 | use nkeys::KeyPair; 5 | 6 | /// Convenience re-export of nkeys to make key functionality easier to manage 7 | pub use nkeys; 8 | 9 | pub mod fs; 10 | 11 | /// A trait that can be implemented by anything that needs to manage nkeys 12 | pub trait KeyManager { 13 | /// Returns the named keypair. Returns None if the key doesn't exist in the manager 14 | fn get(&self, name: &str) -> Result>; 15 | 16 | /// List all key names available 17 | fn list_names(&self) -> Result>; 18 | 19 | /// Retrieves all keys. Note that this could be an expensive operation depending on the 20 | /// implementation 21 | fn list(&self) -> Result>; 22 | 23 | /// Deletes a named keypair 24 | fn delete(&self, name: &str) -> Result<()>; 25 | 26 | /// Saves the given keypair with the given name 27 | fn save(&self, name: &str, key: &KeyPair) -> Result<()>; 28 | } 29 | -------------------------------------------------------------------------------- /crates/wash-lib/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A crate that implements the functionality behind the wasmCloud shell 2 | //! 3 | //! The `wash` command line interface is a great place 4 | //! to find examples on how to fully utilize this library. 5 | //! 6 | //! This library contains a few feature flags, most enabled by default but optional in order to 7 | //! allow consumers to omit some functionality. This is especially useful when considering compiling this 8 | //! library to restrictive targets, e.g. `wasm32-unknown-unknown` or `wasm32-wasi`. Support for `wasm` targets 9 | //! is a goal but has not been tested yet. 10 | //! 11 | //! | Feature Name | Default Enabled | Description | 12 | //! | --- | --- | --- | 13 | //! | start | true | Contains the [start](start) module, with utilities to start wasmCloud runtimes, NATS, and wadm | 14 | //! | parser | true | Contains the [parser](parser) module, with utilities to parse `wasmcloud.toml` files | 15 | //! | cli | false | Contains the build, cli, and generate modules with additional trait derives for usage in building CLI applications | 16 | //! | nats| true| Contains the [app](app) module with a dependency on `async_nats` | 17 | 18 | #[cfg(feature = "nats")] 19 | pub mod app; 20 | #[cfg(feature = "cli")] 21 | pub mod build; 22 | #[cfg(feature = "cli")] 23 | pub mod cli; 24 | #[cfg(feature = "cli")] 25 | pub mod generate; 26 | #[cfg(feature = "parser")] 27 | pub mod parser; 28 | #[cfg(feature = "start")] 29 | pub mod start; 30 | 31 | pub mod actor; 32 | pub mod capture; 33 | pub mod common; 34 | pub mod config; 35 | pub mod context; 36 | pub mod drain; 37 | pub mod id; 38 | pub mod keys; 39 | pub mod registry; 40 | pub mod spier; 41 | pub mod wait; 42 | -------------------------------------------------------------------------------- /crates/wash-lib/src/start/github.rs: -------------------------------------------------------------------------------- 1 | //! Reusable code for downloading tarballs from GitHub releases 2 | 3 | use anyhow::{anyhow, bail, Result}; 4 | use async_compression::tokio::bufread::GzipDecoder; 5 | #[cfg(target_family = "unix")] 6 | use std::os::unix::prelude::PermissionsExt; 7 | use std::path::{Path, PathBuf}; 8 | use std::{ffi::OsStr, io::Cursor}; 9 | use tokio::fs::{create_dir_all, metadata, File}; 10 | use tokio_stream::StreamExt; 11 | use tokio_tar::Archive; 12 | 13 | /// Reusable function to download a release tarball from GitHub and extract an embedded binary to a specified directory 14 | /// 15 | /// # Arguments 16 | /// 17 | /// * `url` - URL of the GitHub release artifact tarball (Usually in the form of https://github.com///releases/download//.tar.gz) 18 | /// * `dir` - Directory on disk to install the binary into. This will be created if it doesn't exist 19 | /// * `bin_name` - Name of the binary inside of the tarball, e.g. `nats-server` or `wadm` 20 | /// # Examples 21 | /// 22 | /// ```rust,ignore 23 | /// # #[tokio::main] 24 | /// # async fn main() { 25 | /// let url = "https://github.com/wasmCloud/wadm/releases/download/v0.4.0-alpha.1/wadm-v0.4.0-alpha.1-linux-amd64.tar.gz"; 26 | /// let res = download_binary_from_github(url, "/tmp/", "wadm").await; 27 | /// assert!(res.is_ok()); 28 | /// assert!(res.unwrap().to_string_lossy() == "/tmp/wadm"); 29 | /// # } 30 | /// ``` 31 | pub async fn download_binary_from_github

(url: &str, dir: P, bin_name: &str) -> Result 32 | where 33 | P: AsRef, 34 | { 35 | let bin_path = dir.as_ref().join(bin_name); 36 | // Download release tarball 37 | let body = match reqwest::get(url).await { 38 | Ok(resp) => resp.bytes().await?, 39 | Err(e) => bail!("Failed to request release tarball: {:?}", e), 40 | }; 41 | let cursor = Cursor::new(body); 42 | let mut bin_tarball = Archive::new(Box::new(GzipDecoder::new(cursor))); 43 | 44 | // Look for binary within tarball and only extract that 45 | let mut entries = bin_tarball.entries()?; 46 | while let Some(res) = entries.next().await { 47 | let mut entry = res.map_err(|e| { 48 | anyhow!( 49 | "Failed to retrieve file from archive, ensure {bin_name} exists. Original error: {e}", 50 | ) 51 | })?; 52 | if let Ok(tar_path) = entry.path() { 53 | match tar_path.file_name() { 54 | Some(name) if name == OsStr::new(bin_name) => { 55 | // Ensure target directory exists 56 | create_dir_all(&dir).await?; 57 | let mut bin_file = File::create(&bin_path).await?; 58 | // Make binary executable 59 | #[cfg(target_family = "unix")] 60 | { 61 | let mut permissions = bin_file.metadata().await?.permissions(); 62 | // Read/write/execute for owner and read/execute for others. This is what `cargo install` does 63 | permissions.set_mode(0o755); 64 | bin_file.set_permissions(permissions).await?; 65 | } 66 | 67 | tokio::io::copy(&mut entry, &mut bin_file).await?; 68 | return Ok(bin_path); 69 | } 70 | // Ignore all other files in the tarball 71 | _ => (), 72 | } 73 | } 74 | } 75 | 76 | bail!("{bin_name} binary could not be installed, please see logs") 77 | } 78 | 79 | /// Helper function to determine if the provided binary is present in a directory 80 | #[allow(unused)] 81 | pub(crate) async fn is_bin_installed

(dir: P, bin_name: &str) -> bool 82 | where 83 | P: AsRef, 84 | { 85 | metadata(dir.as_ref().join(bin_name)) 86 | .await 87 | .map_or(false, |m| m.is_file()) 88 | } 89 | -------------------------------------------------------------------------------- /crates/wash-lib/src/start/mod.rs: -------------------------------------------------------------------------------- 1 | //! The `start` module contains functionality relating to downloading and starting 2 | //! NATS servers and wasmCloud hosts. 3 | //! 4 | //! # Downloading and Starting NATS and wasmCloud 5 | //! ```no_run 6 | //! use anyhow::{anyhow, Result}; 7 | //! use wash_lib::start::{ 8 | //! start_wasmcloud_host, 9 | //! start_nats_server, 10 | //! ensure_nats_server, 11 | //! ensure_wasmcloud, 12 | //! NatsConfig 13 | //! }; 14 | //! use std::path::PathBuf; 15 | //! 16 | //! #[tokio::main] 17 | //! async fn main() -> Result<()> { 18 | //! let install_dir = PathBuf::from("/tmp"); 19 | //! 20 | //! // Download NATS if not already installed 21 | //! let nats_binary = ensure_nats_server("v2.8.4", &install_dir).await?; 22 | //! 23 | //! // Start NATS server, redirecting output to a log file 24 | //! let nats_log_path = install_dir.join("nats.log"); 25 | //! let nats_log_file = tokio::fs::File::create(&nats_log_path).await?.into_std().await; 26 | //! let config = NatsConfig::new_standalone("127.0.0.1", 4222, None); 27 | //! let mut nats_process = start_nats_server( 28 | //! nats_binary, 29 | //! nats_log_file, 30 | //! config, 31 | //! ).await?; 32 | //! 33 | //! // Download wasmCloud if not already installed 34 | //! let wasmcloud_executable = ensure_wasmcloud("v0.57.1", &install_dir).await?; 35 | //! 36 | //! // Redirect output (which is on stderr) to a log file 37 | //! let log_path = install_dir.join("wasmcloud_stderr.log"); 38 | //! let log_file = tokio::fs::File::create(&log_path).await?.into_std().await; 39 | //! 40 | //! let mut wasmcloud_process = start_wasmcloud_host( 41 | //! wasmcloud_executable, 42 | //! std::process::Stdio::null(), 43 | //! log_file, 44 | //! std::collections::HashMap::new(), 45 | //! ).await?; 46 | //! 47 | //! // Park thread, wasmCloud and NATS are running 48 | //! 49 | //! // Terminate processes 50 | //! nats_process.kill().await?; 51 | //! wasmcloud_process.kill().await?; 52 | //! Ok(()) 53 | //! } 54 | //! ``` 55 | 56 | use anyhow::Result; 57 | pub async fn wait_for_server(url: &str, service: &str) -> Result<()> { 58 | let mut wait_count = 1; 59 | loop { 60 | // Magic number: 10 + 1, since we are starting at 1 for humans 61 | if wait_count >= 11 { 62 | anyhow::bail!("Ran out of retries waiting for {service} to start"); 63 | } 64 | match tokio::net::TcpStream::connect(url).await { 65 | Ok(_) => break, 66 | Err(e) => { 67 | log::debug!("Waiting for {service} at {url} to come up, attempt {wait_count}. Will retry in 1 second. Got error {:?}", e); 68 | wait_count += 1; 69 | tokio::time::sleep(std::time::Duration::from_secs(1)).await; 70 | } 71 | } 72 | } 73 | Ok(()) 74 | } 75 | 76 | mod github; 77 | pub(crate) use github::*; 78 | mod nats; 79 | pub use nats::*; 80 | mod wadm; 81 | pub use self::wadm::*; 82 | mod wasmcloud; 83 | pub use wasmcloud::*; 84 | -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/folder/wasmcloud.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [actor] 7 | claims = ["wasmcloud:httpserver"] 8 | registry = "localhost:8080" 9 | push_insecure = false 10 | key_directory = "./keys" 11 | filename = "testactor.wasm" 12 | wasm_target = "wasm32-unknown-unknown" 13 | call_alias = "testactor" 14 | 15 | [rust] 16 | cargo_path = "./cargo" 17 | target_path = "./target" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/minimal_rust_actor.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [actor] 7 | claims = ["wasmcloud:httpserver"] -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/minimal_rust_actor_core_module.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [actor] 7 | claims = ["wasmcloud:httpserver"] 8 | wasm_target = "wasm32-unknown-unknown" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/minimal_rust_actor_preview1.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [actor] 7 | claims = ["wasmcloud:httpserver"] 8 | wasm_target = "wasm32-wasi-preview1" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/minimal_rust_actor_preview2.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [actor] 7 | claims = ["wasmcloud:httpserver"] 8 | wasm_target = "wasm32-wasi-preview2" 9 | wit_world = "test-world" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/no_actor.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [provider] 7 | capability_id = "wasmcloud:httpserver" 8 | vendor = "NoVendor" 9 | 10 | [rust] 11 | cargo_path = "./cargo" 12 | target_path = "./target" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/no_interface.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "interface" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [provider] 7 | capability_id = "wasmcloud:httpserver" 8 | vendor = "NoVendor" 9 | 10 | [rust] 11 | cargo_path = "./cargo" 12 | target_path = "./target" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/no_provider.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "provider" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [rust] 7 | cargo_path = "./cargo" 8 | target_path = "./target" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/noconfig/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wash/99f542c34aeec46fe7f18428fe767df9c7978684/crates/wash-lib/tests/parser/files/noconfig/.gitkeep -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/random.txt: -------------------------------------------------------------------------------- 1 | this is not a config file -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/rust_actor.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [actor] 7 | claims = ["wasmcloud:httpserver"] 8 | registry = "localhost:8080" 9 | push_insecure = false 10 | key_directory = "./keys" 11 | filename = "testactor.wasm" 12 | wasm_target = "wasm32-unknown-unknown" 13 | call_alias = "testactor" 14 | 15 | [rust] 16 | cargo_path = "./cargo" 17 | target_path = "./target" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/tinygo_actor_component.toml: -------------------------------------------------------------------------------- 1 | language = "tinygo" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [actor] 7 | claims = ["wasmcloud:httpserver"] 8 | registry = "localhost:8080" 9 | push_insecure = false 10 | key_directory = "./keys" 11 | filename = "testactor.wasm" 12 | wasm_target = "wasm32-wasi-preview2" 13 | call_alias = "testactor" 14 | 15 | [tinygo] 16 | tinygo_path = "path/to/tinygo" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/tinygo_actor_module.toml: -------------------------------------------------------------------------------- 1 | language = "tinygo" 2 | type = "actor" 3 | name = "testactor" 4 | version = "0.1.0" 5 | 6 | [actor] 7 | claims = ["wasmcloud:httpserver"] 8 | registry = "localhost:8080" 9 | push_insecure = false 10 | key_directory = "./keys" 11 | filename = "testactor.wasm" 12 | wasm_target = "wasm" 13 | call_alias = "testactor" 14 | 15 | [tinygo] 16 | tinygo_path = "path/to/tinygo" -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/withcargotoml/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "withcargotoml" 3 | version = "0.200.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/withcargotoml/minimal_rust_actor_with_cargo.toml: -------------------------------------------------------------------------------- 1 | language = "rust" 2 | type = "actor" 3 | 4 | [actor] 5 | claims = ["wasmcloud:httpserver"] -------------------------------------------------------------------------------- /crates/wash-lib/tests/parser/files/withcargotoml/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /docs/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Developer guide 2 | 3 | This document serves as a guide and reference for people looking to develop `wash`. 4 | 5 | - [Developer guide](#developer-guide) 6 | - [Development Prerequistes](#development-prerequistes) 7 | - [`build` Integration Tests](#build-integration-tests) 8 | - [Dependency Check Script](#dependency-check-script) 9 | - [Optional Tools](#optional-tools) 10 | - [Building the project](#building-the-project) 11 | - [Testing the project](#testing-the-project) 12 | - [Making Commits](#making-commits) 13 | 14 | ## Development Prerequistes 15 | 16 | To contribute to `wash`, you just need [Rust](https://rustup.rs/) installed. To run any `wash` tests, you need to install [`nextest`](https://nexte.st/index.html). With a Rust toolchain already installed, you can simply install this with: 17 | 18 | ```bash 19 | cargo install cargo-nextest --locked 20 | ``` 21 | 22 | The dependency check script will also install this for you, see that section below. 23 | 24 | ### `build` Integration Tests 25 | 26 | To run the `wash build` integration tests that compile actors using actual language toolchains, you must have those toolchains installed. Currently the requirements for this are: 27 | 28 | - [Rust](https://rustup.rs/) 29 | - The `wasm32-unknown-unknown` target must be installed. 30 | - You can install this with: `rustup target add wasm32-unknown-unknown`. 31 | - [TinyGo](https://tinygo.org/getting-started/install/) 32 | - TinyGo also requires [Go](https://go.dev/doc/install) to be installed. 33 | 34 | ### Dependency Check Script 35 | 36 | To make it easy to ensure you have all the right tools installed to run all of the `wash` tests, we've created a Python script at `tools/deps_check.py`. You can run this using `make deps-check` or `python3 ./tools/deps_check.py`. 37 | 38 | ### Optional Tools 39 | 40 | While developing `wash`, consider installing the following optional development tools: 41 | 42 | - [`cargo-watch`](https://crates.io/crates/cargo-watch) (`cargo install cargo-watch`) to enable the `*-watch` commands 43 | 44 | These will be automatically installed using the `deps_check.py` script as well. 45 | 46 | ## Building the project 47 | 48 | To build the project: 49 | 50 | ```console 51 | make build 52 | ``` 53 | 54 | To build continuously (thanks to [`cargo-watch`](https://crates.io/crates/cargo-watch)): 55 | 56 | ```console 57 | make build-watch 58 | ``` 59 | 60 | ## Testing the project 61 | 62 | To test all unit tests: 63 | 64 | ```console 65 | make test 66 | ``` 67 | 68 | To test all unit tests continuously: 69 | 70 | ```console 71 | make test-watch 72 | ``` 73 | 74 | To test a *specific* target test(s) continuously: 75 | 76 | ```console 77 | TARGET=integration_new_handles_dashed_names make test-watch 78 | ``` 79 | 80 | ## Making Commits 81 | 82 | For us to be able to merge in any commits, they need to be signed off. If you didn't do so, the PR bot will let you know how to fix it, but it's worth knowing how to do it in advance. 83 | 84 | There are a few options: 85 | - use `git commit -s` in the CLI 86 | - in `vscode`, go to settings and set the `git.alwaysSignOff` setting. Note that the dev container configuration in this repo sets this up by default. 87 | - manually add "Signed-off-by: NAME " at the end of each commit 88 | 89 | You may also be able to use GPG signing in lieu of a sign off. 90 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | nixConfig.extra-substituters = [ 3 | "https://wasmcloud.cachix.org" 4 | "https://nix-community.cachix.org" 5 | "https://cache.garnix.io" 6 | ]; 7 | nixConfig.extra-trusted-public-keys = [ 8 | "wasmcloud.cachix.org-1:9gRBzsKh+x2HbVVspreFg/6iFRiD4aOcUQfXVDl3hiM=" 9 | "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" 10 | "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" 11 | ]; 12 | 13 | description = "wash - wasmCloud Shell"; 14 | 15 | inputs.nixify.url = github:rvolosatovs/nixify; 16 | inputs.wasmcloud-component-adapters.url = github:wasmCloud/wasmcloud-component-adapters/v0.3.0; 17 | 18 | outputs = { 19 | self, 20 | nixify, 21 | wasmcloud-component-adapters, 22 | }: 23 | with nixify.lib; 24 | rust.mkFlake { 25 | name = "wash"; 26 | src = ./.; 27 | 28 | targets.wasm32-unknown-unknown = false; # `wash` does not compile for wasm32-unknown-unknown 29 | targets.wasm32-wasi = false; # `wash` does not compile for wasm32-wasi 30 | 31 | doCheck = false; # testing is performed in checks via `nextest` 32 | 33 | buildOverrides = { 34 | pkgs, 35 | pkgsCross ? pkgs, 36 | ... 37 | }: { 38 | nativeBuildInputs ? [], 39 | ... 40 | } @ args: 41 | with pkgsCross; 42 | with pkgs.lib; { 43 | WASI_PREVIEW1_COMMAND_COMPONENT_ADAPTER = wasmcloud-component-adapters.packages.${pkgs.stdenv.system}.wasi-preview1-command-component-adapter; 44 | WASI_PREVIEW1_REACTOR_COMPONENT_ADAPTER = wasmcloud-component-adapters.packages.${pkgs.stdenv.system}.wasi-preview1-reactor-component-adapter; 45 | 46 | nativeBuildInputs = 47 | nativeBuildInputs 48 | ++ [ 49 | pkgs.protobuf # build dependency of prost-build v0.9.0 50 | ]; 51 | }; 52 | 53 | withDevShells = { 54 | pkgs, 55 | devShells, 56 | ... 57 | }: 58 | extendDerivations { 59 | buildInputs = [ 60 | pkgs.git # required for integration tests 61 | pkgs.tinygo # required for integration tests 62 | pkgs.protobuf # build dependency of prost-build v0.9.0 63 | ]; 64 | } 65 | devShells; 66 | 67 | excludePaths = [ 68 | ".devcontainer" 69 | ".github" 70 | ".gitignore" 71 | ".pre-commit-config.yaml" 72 | "Completions.md" 73 | "Dockerfile" 74 | "flake.lock" 75 | "flake.nix" 76 | "LICENSE" 77 | "Makefile" 78 | "README.md" 79 | "rust-toolchain.toml" 80 | "sample-manifest.yaml" 81 | "snap" 82 | "tools" 83 | 84 | # Exclude tests, which require either: 85 | # - non-deterministic networking, which is not available within Nix sandbox 86 | # - external services running, which would require a more involved setup 87 | "tests/integration_build.rs" 88 | "tests/integration_claims.rs" 89 | "tests/integration_dev.rs" 90 | "tests/integration_get.rs" 91 | "tests/integration_inspect.rs" 92 | "tests/integration_keys.rs" 93 | "tests/integration_link.rs" 94 | "tests/integration_par.rs" 95 | "tests/integration_reg.rs" 96 | "tests/integration_scale.rs" 97 | "tests/integration_start.rs" 98 | "tests/integration_stop.rs" 99 | "tests/integration_up.rs" 100 | "tests/integration_update.rs" 101 | ]; 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["clippy", "rustfmt"] 4 | targets = [ 5 | "wasm32-unknown-unknown" 6 | ] 7 | -------------------------------------------------------------------------------- /sample-manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | actors: 3 | - wasmcloud.azurecr.io/echo:0.3.4 4 | capabilities: 5 | - image_ref: wasmcloud.azurecr.io/httpserver:0.16.3 6 | link_name: default 7 | links: 8 | - actor: ${ECHO_ACTOR:MBCFOPM6JW2APJLXJD3Z5O4CN7CPYJ2B4FTKLJUR5YR5MITIU7HD3WD5} 9 | provider_id: VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M 10 | contract_id: wasmcloud:httpserver 11 | link_name: default 12 | values: 13 | PORT: 8080 -------------------------------------------------------------------------------- /snap/local/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wash/99f542c34aeec46fe7f18428fe767df9c7978684/snap/local/icon.png -------------------------------------------------------------------------------- /snap/snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: wash 2 | base: core20 # the base snap is the execution environment for this snap 3 | version: "0.12.0" 4 | summary: WAsmcloud SHell - a comprehensive command-line tool for wasmCloud development 5 | icon: snap/local/icon.png 6 | description: | 7 | wash is a bundle of command line tools that, together, form a comprehensive CLI for wasmcloud development. 8 | Everything from generating signing keys to a fully interactive REPL environment is contained within the subcommands of wash. 9 | Our goal with wash is to encapsulate our tools into a single binary to make developing WebAssembly with wasmcloud painless and simple. 10 | grade: stable 11 | confinement: devmode 12 | apps: 13 | wash: 14 | command: bin/wash 15 | parts: 16 | wash: 17 | plugin: rust 18 | source: https://github.com/wasmCloud/wash.git 19 | build-packages: 20 | - libssl-dev 21 | - pkg-config 22 | - clang 23 | -------------------------------------------------------------------------------- /src/app/output.rs: -------------------------------------------------------------------------------- 1 | use term_table::{ 2 | row::Row, 3 | table_cell::{Alignment, TableCell}, 4 | Table, 5 | }; 6 | use wadm::server::VersionInfo; 7 | 8 | use super::ModelSummary; 9 | 10 | pub(crate) fn list_revisions_table(revisions: Vec) -> String { 11 | let mut table = Table::new(); 12 | crate::util::configure_table_style(&mut table); 13 | 14 | table.add_row(Row::new(vec![ 15 | TableCell::new_with_alignment("Version", 1, Alignment::Left), 16 | TableCell::new_with_alignment("Deployed?", 1, Alignment::Left), 17 | ])); 18 | 19 | revisions.iter().for_each(|r| { 20 | table.add_row(Row::new(vec![ 21 | TableCell::new_with_alignment(r.version.clone(), 1, Alignment::Left), 22 | TableCell::new_with_alignment(r.deployed, 1, Alignment::Left), 23 | ])); 24 | }); 25 | 26 | table.render() 27 | } 28 | 29 | pub(crate) fn list_models_table(models: Vec) -> String { 30 | let mut table = Table::new(); 31 | crate::util::configure_table_style(&mut table); 32 | 33 | table.add_row(Row::new(vec![ 34 | TableCell::new_with_alignment("Name", 1, Alignment::Left), 35 | TableCell::new_with_alignment("Latest Version", 1, Alignment::Left), 36 | TableCell::new_with_alignment("Deployed Version", 1, Alignment::Left), 37 | TableCell::new_with_alignment("Deploy Status", 1, Alignment::Right), 38 | TableCell::new_with_alignment("Description", 1, Alignment::Left), 39 | ])); 40 | models.iter().for_each(|m| { 41 | table.add_row(Row::new(vec![ 42 | TableCell::new_with_alignment(m.name.clone(), 1, Alignment::Left), 43 | TableCell::new_with_alignment(m.version.clone(), 1, Alignment::Left), 44 | TableCell::new_with_alignment( 45 | m.deployed_version 46 | .clone() 47 | .unwrap_or_else(|| "N/A".to_string()), 48 | 1, 49 | Alignment::Left, 50 | ), 51 | TableCell::new_with_alignment(format!("{:?}", m.status), 1, Alignment::Right), 52 | TableCell::new_with_alignment( 53 | m.description.clone().unwrap_or_else(|| "N/A".to_string()), 54 | 1, 55 | Alignment::Left, 56 | ), 57 | ])) 58 | }); 59 | 60 | table.render() 61 | } 62 | -------------------------------------------------------------------------------- /src/appearance/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod spinner; 2 | -------------------------------------------------------------------------------- /src/appearance/spinner.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use indicatif::{ProgressBar, ProgressStyle}; 3 | use wash_lib::cli::OutputKind; 4 | 5 | // For more spinners check out the cli-spinners project: 6 | // https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json 7 | pub const DOTS_12: &[&str; 56] = &[ 8 | "⢀⠀", "⡀⠀", "⠄⠀", "⢂⠀", "⡂⠀", "⠅⠀", "⢃⠀", "⡃⠀", "⠍⠀", "⢋⠀", "⡋⠀", "⠍⠁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", 9 | "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⢈⠩", "⡀⢙", "⠄⡙", "⢂⠩", "⡂⢘", "⠅⡘", "⢃⠨", "⡃⢐", "⠍⡐", "⢋⠠", 10 | "⡋⢀", "⠍⡁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⠈⠩", "⠀⢙", "⠀⡙", "⠀⠩", 11 | "⠀⢘", "⠀⡘", "⠀⠨", "⠀⢐", "⠀⡐", "⠀⠠", "⠀⢀", "⠀⡀", 12 | ]; 13 | 14 | pub struct Spinner { 15 | spinner: Option, 16 | } 17 | 18 | impl Spinner { 19 | pub(crate) fn new(output_kind: &OutputKind) -> Result { 20 | match output_kind { 21 | OutputKind::Text => { 22 | let style = ProgressStyle::default_spinner() 23 | .tick_strings(DOTS_12) 24 | .template("{prefix:.bold.dim} {spinner:.bold.dim} {wide_msg:.bold.dim}")?; 25 | 26 | let spinner = ProgressBar::new_spinner().with_style(style); 27 | 28 | spinner.enable_steady_tick(std::time::Duration::from_millis(200)); 29 | Ok(Self { 30 | spinner: Some(spinner), 31 | }) 32 | } 33 | OutputKind::Json => Ok(Self { spinner: None }), 34 | } 35 | } 36 | 37 | /// Handles updating the spinner for text output 38 | /// JSON output will be corrupted with a spinner 39 | pub fn update_spinner_message(&self, msg: String) { 40 | match &self.spinner { 41 | Some(spinner) => { 42 | spinner.set_prefix(">>>"); 43 | spinner.set_message(msg); 44 | } 45 | None => {} 46 | } 47 | } 48 | 49 | pub fn finish_and_clear(&self) { 50 | match &self.spinner { 51 | Some(progress_bar) => progress_bar.finish_and_clear(), 52 | None => {} 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/cfg.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::{ 3 | fs, 4 | io::{Error, ErrorKind}, 5 | path::PathBuf, 6 | }; 7 | 8 | const WASH_DIR: &str = ".wash"; 9 | 10 | /// Get the path to the `.wash` configuration directory. 11 | /// Creates the directory if it does not exist. 12 | pub fn cfg_dir() -> Result { 13 | let home = dirs::home_dir().ok_or_else(|| { 14 | Error::new( 15 | ErrorKind::NotFound, 16 | "No home directory found. Please set $HOME.", 17 | ) 18 | })?; 19 | 20 | let wash = home.join(WASH_DIR); 21 | 22 | if !wash.exists() { 23 | fs::create_dir_all(&wash)?; 24 | } 25 | 26 | Ok(wash) 27 | } 28 | -------------------------------------------------------------------------------- /src/common/get_cmd.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use wash_lib::cli::{ 3 | claims::get_claims, 4 | get::{get_host_inventories, get_hosts, GetCommand, GetLinksCommand}, 5 | link::{LinkCommand, LinkQueryCommand}, 6 | }; 7 | 8 | use crate::{ 9 | appearance::spinner::Spinner, 10 | common::link_cmd::handle_command as handle_link_command, 11 | ctl::{get_claims_output, get_host_inventories_output, get_hosts_output}, 12 | CommandOutput, OutputKind, 13 | }; 14 | 15 | pub(crate) async fn handle_command( 16 | command: GetCommand, 17 | output_kind: OutputKind, 18 | ) -> Result { 19 | let sp: Spinner = Spinner::new(&output_kind)?; 20 | let out: CommandOutput = match command { 21 | GetCommand::Links(GetLinksCommand { opts }) => { 22 | handle_link_command(LinkCommand::Query(LinkQueryCommand { opts }), output_kind).await? 23 | } 24 | GetCommand::Claims(cmd) => { 25 | sp.update_spinner_message("Retrieving claims ... ".to_string()); 26 | let claims = get_claims(cmd).await?; 27 | get_claims_output(claims) 28 | } 29 | GetCommand::Hosts(cmd) => { 30 | sp.update_spinner_message(" Retrieving Hosts ...".to_string()); 31 | let hosts = get_hosts(cmd).await?; 32 | get_hosts_output(hosts) 33 | } 34 | GetCommand::HostInventories(cmd) => { 35 | if let Some(id) = cmd.host_id.as_ref() { 36 | sp.update_spinner_message(format!(" Retrieving inventory for host {} ...", id)); 37 | } else { 38 | sp.update_spinner_message(" Retrieving hosts for inventory query ...".to_string()); 39 | } 40 | let invs = get_host_inventories(cmd).await?; 41 | get_host_inventories_output(invs) 42 | } 43 | }; 44 | 45 | Ok(out) 46 | } 47 | -------------------------------------------------------------------------------- /src/common/link_cmd.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::{bail, Result}; 4 | use wash_lib::{ 5 | cli::{ 6 | link::{ 7 | create_link, delete_link, query_links, LinkCommand, LinkDelCommand, LinkPutCommand, 8 | LinkQueryCommand, 9 | }, 10 | CommandOutput, 11 | }, 12 | id::{validate_contract_id, ModuleId, ServiceId}, 13 | }; 14 | use wasmcloud_control_interface::LinkDefinition; 15 | 16 | use crate::{ 17 | appearance::spinner::Spinner, 18 | ctl::{link_del_output, links_table}, 19 | json, OutputKind, 20 | }; 21 | 22 | /// Generate output for link put command 23 | pub(crate) fn link_put_output( 24 | actor_id: &ModuleId, 25 | provider_id: &ServiceId, 26 | failure: Option, 27 | ) -> Result { 28 | match failure { 29 | None => { 30 | let mut map = HashMap::new(); 31 | map.insert("actor_id".to_string(), json!(actor_id)); 32 | map.insert("provider_id".to_string(), json!(provider_id)); 33 | Ok(CommandOutput::new( 34 | format!("Published link ({actor_id}) <-> ({provider_id}) successfully"), 35 | map, 36 | )) 37 | } 38 | Some(f) => bail!("Error advertising link: {}", f), 39 | } 40 | } 41 | 42 | /// Generate output for the link query command 43 | pub(crate) fn link_query_output(list: Vec) -> CommandOutput { 44 | let mut map = HashMap::new(); 45 | map.insert("links".to_string(), json!(list)); 46 | CommandOutput::new(links_table(list), map) 47 | } 48 | 49 | pub(crate) async fn handle_command( 50 | command: LinkCommand, 51 | output_kind: OutputKind, 52 | ) -> Result { 53 | let sp: Spinner = Spinner::new(&output_kind)?; 54 | let out: CommandOutput = match command { 55 | LinkCommand::Del(LinkDelCommand { 56 | actor_id, 57 | contract_id, 58 | link_name, 59 | opts, 60 | }) => { 61 | let link_name = link_name.clone().unwrap_or_else(|| "default".to_string()); 62 | 63 | validate_contract_id(&contract_id)?; 64 | 65 | sp.update_spinner_message(format!( 66 | "Deleting link for {} on {} ({}) ... ", 67 | actor_id, contract_id, link_name, 68 | )); 69 | 70 | let failure = delete_link(opts.try_into()?, &contract_id, &actor_id, &link_name) 71 | .await 72 | .map_or_else(|e| Some(format!("{e}")), |_| None); 73 | 74 | link_del_output(&actor_id, &contract_id, &link_name, failure)? 75 | } 76 | LinkCommand::Put(LinkPutCommand { 77 | opts, 78 | contract_id, 79 | actor_id, 80 | provider_id, 81 | link_name, 82 | values, 83 | }) => { 84 | validate_contract_id(&contract_id)?; 85 | 86 | sp.update_spinner_message(format!( 87 | "Defining link between {actor_id} and {provider_id} ... ", 88 | )); 89 | 90 | let link_name = link_name.unwrap_or_else(|| "default".to_string()); 91 | 92 | let failure = create_link( 93 | opts.try_into()?, 94 | &contract_id, 95 | &actor_id, 96 | &provider_id, 97 | &link_name, 98 | &values, 99 | ) 100 | .await 101 | .map_or_else(|e| Some(format!("{e}")), |_| None); 102 | 103 | link_put_output(&actor_id, &provider_id, failure)? 104 | } 105 | LinkCommand::Query(LinkQueryCommand { opts }) => { 106 | sp.update_spinner_message("Querying Links ... ".to_string()); 107 | let result = query_links(opts.try_into()?).await?; 108 | link_query_output(result) 109 | } 110 | }; 111 | 112 | Ok(out) 113 | } 114 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod get_cmd; 2 | pub(crate) mod link_cmd; 3 | pub(crate) mod registry_cmd; 4 | pub(crate) mod scale_cmd; 5 | pub(crate) mod start_cmd; 6 | pub(crate) mod stop_cmd; 7 | pub(crate) mod update_cmd; 8 | -------------------------------------------------------------------------------- /src/common/scale_cmd.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use wash_lib::cli::{ 4 | scale::{handle_scale_actor, ScaleCommand}, 5 | CommandOutput, OutputKind, 6 | }; 7 | 8 | use crate::appearance::spinner::Spinner; 9 | 10 | pub(crate) async fn handle_command( 11 | command: ScaleCommand, 12 | output_kind: OutputKind, 13 | ) -> Result { 14 | let sp: Spinner = Spinner::new(&output_kind)?; 15 | let out = match command { 16 | ScaleCommand::Actor(cmd) => { 17 | let max = cmd 18 | .max_concurrent 19 | .map(|max| max.to_string()) 20 | .unwrap_or_else(|| "unbounded".to_string()); 21 | sp.update_spinner_message(format!( 22 | " Scaling Actor {} to {} max concurrent instances ... ", 23 | cmd.actor_ref, max 24 | )); 25 | handle_scale_actor(cmd.clone()).await? 26 | } 27 | }; 28 | 29 | sp.finish_and_clear(); 30 | 31 | Ok(out) 32 | } 33 | -------------------------------------------------------------------------------- /src/common/update_cmd.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use wash_lib::cli::{ 4 | update::{handle_update_actor, UpdateCommand}, 5 | CommandOutput, OutputKind, 6 | }; 7 | 8 | use crate::appearance::spinner::Spinner; 9 | 10 | pub(crate) async fn handle_command( 11 | command: UpdateCommand, 12 | output_kind: OutputKind, 13 | ) -> Result { 14 | let sp: Spinner = Spinner::new(&output_kind)?; 15 | let out = match command { 16 | UpdateCommand::Actor(cmd) => { 17 | sp.update_spinner_message(format!( 18 | " Updating Actor {} to {} ... ", 19 | cmd.actor_id, cmd.new_actor_ref 20 | )); 21 | 22 | handle_update_actor(cmd).await? 23 | } 24 | }; 25 | 26 | sp.finish_and_clear(); 27 | 28 | Ok(out) 29 | } 30 | -------------------------------------------------------------------------------- /src/completions.rs: -------------------------------------------------------------------------------- 1 | //! Generate shell completion files 2 | //! 3 | use crate::{cfg::cfg_dir, CommandOutput}; 4 | use anyhow::{anyhow, bail, Result}; 5 | use clap::{Args, Subcommand}; 6 | use clap_complete::{generator::generate_to, shells::Shell}; 7 | use std::collections::HashMap; 8 | use std::path::PathBuf; 9 | 10 | const TOKEN_FILE: &str = ".completion_suggested"; 11 | const COMPLETION_DOC_URL: &str = "https://github.com/wasmCloud/wash/blob/main/Completions.md"; 12 | 13 | fn instructions() -> String { 14 | format!( 15 | "For instructions on setting up auto-complete for your shell, please see '{}'", 16 | COMPLETION_DOC_URL 17 | ) 18 | } 19 | 20 | #[derive(Debug, Clone, Args)] 21 | pub(crate) struct CompletionOpts { 22 | /// Output directory (default '.') 23 | #[clap(short = 'd', long = "dir")] 24 | dir: Option, 25 | 26 | /// Shell 27 | #[clap(name = "shell", subcommand)] 28 | shell: ShellSelection, 29 | } 30 | 31 | #[derive(Subcommand, Debug, Clone)] 32 | pub(crate) enum ShellSelection { 33 | /// generate completions for Zsh 34 | Zsh, 35 | /// generate completions for Bash 36 | Bash, 37 | /// generate completions for Fish 38 | Fish, 39 | /// generate completions for PowerShell 40 | PowerShell, 41 | } 42 | 43 | /// Displays a message one time after wash install 44 | pub(crate) fn first_run_suggestion() -> Result> { 45 | let token = cfg_dir()?.join(TOKEN_FILE); 46 | if token.is_file() { 47 | return Ok(None); 48 | } 49 | let _ = std::fs::File::create(token).map_err(|e| { 50 | anyhow!( 51 | "can't create completion first-run token in {}: {}", 52 | // unwrap() ok because cfg_dir worked above 53 | cfg_dir().unwrap().display(), 54 | e 55 | ) 56 | })?; 57 | Ok(Some(format!( 58 | "Congratulations on installing wash! Shell auto-complete is available. {}", 59 | instructions() 60 | ))) 61 | } 62 | 63 | pub(crate) fn handle_command( 64 | opts: CompletionOpts, 65 | mut command: clap::builder::Command, 66 | ) -> Result { 67 | let output_dir = opts.dir.unwrap_or_else(|| PathBuf::from(".")); 68 | 69 | let shell = match opts.shell { 70 | ShellSelection::Zsh => Shell::Zsh, 71 | ShellSelection::Bash => Shell::Bash, 72 | ShellSelection::Fish => Shell::Fish, 73 | ShellSelection::PowerShell => Shell::PowerShell, 74 | }; 75 | 76 | match generate_to(shell, &mut command, "wash", &output_dir) { 77 | Ok(path) => { 78 | let mut map = HashMap::new(); 79 | map.insert( 80 | "path".to_string(), 81 | path.to_string_lossy().to_string().into(), 82 | ); 83 | Ok(CommandOutput::new( 84 | format!( 85 | "Generated completion file: {}. {}", 86 | path.display(), 87 | instructions() 88 | ), 89 | map, 90 | )) 91 | } 92 | Err(e) => bail!( 93 | "generating shell completion file in folder '{}': {}", 94 | output_dir.display(), 95 | e 96 | ), 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/drain.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::Result; 4 | use serde_json::json; 5 | use wash_lib::cli::CommandOutput; 6 | use wash_lib::drain::Drain; 7 | 8 | pub(crate) fn handle_command(cmd: Drain) -> Result { 9 | let paths = cmd.drain()?; 10 | let mut map = HashMap::new(); 11 | map.insert("drained".to_string(), json!(paths)); 12 | Ok(CommandOutput::new( 13 | format!("Successfully cleared caches at: {paths:?}"), 14 | map, 15 | )) 16 | } 17 | 18 | #[cfg(test)] 19 | mod test { 20 | use super::*; 21 | use clap::Parser; 22 | 23 | #[derive(Parser)] 24 | struct Cmd { 25 | #[clap(subcommand)] 26 | drain: Drain, 27 | } 28 | 29 | #[test] 30 | // Enumerates all options of drain subcommands to ensure 31 | // changes are not made to the drain API 32 | fn test_drain_comprehensive() { 33 | let all: Cmd = Parser::try_parse_from(["drain", "all"]).unwrap(); 34 | match all.drain { 35 | Drain::All => {} 36 | _ => panic!("drain constructed incorrect command"), 37 | } 38 | let lib: Cmd = Parser::try_parse_from(["drain", "lib"]).unwrap(); 39 | match lib.drain { 40 | Drain::Lib => {} 41 | _ => panic!("drain constructed incorrect command"), 42 | } 43 | let oci: Cmd = Parser::try_parse_from(["drain", "oci"]).unwrap(); 44 | match oci.drain { 45 | Drain::Oci => {} 46 | _ => panic!("drain constructed incorrect command"), 47 | } 48 | let smithy: Cmd = Parser::try_parse_from(["drain", "smithy"]).unwrap(); 49 | match smithy.drain { 50 | Drain::Smithy => {} 51 | _ => panic!("drain constructed incorrect command"), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/generate.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, path::PathBuf}; 2 | 3 | use anyhow::{Context, Result}; 4 | use clap::{Args, Subcommand}; 5 | use serde_json::json; 6 | use wash_lib::{ 7 | cli::CommandOutput, 8 | generate::{generate_project, Project, ProjectKind}, 9 | }; 10 | 11 | /// Create a new project from template 12 | #[derive(Debug, Clone, Subcommand)] 13 | pub enum NewCliCommand { 14 | /// Generate actor project 15 | #[clap(name = "actor")] 16 | Actor(NewProjectArgs), 17 | 18 | /// Generate a new interface project 19 | #[clap(name = "interface")] 20 | Interface(NewProjectArgs), 21 | 22 | /// Generate a new capability provider project 23 | #[clap(name = "provider")] 24 | Provider(NewProjectArgs), 25 | } 26 | 27 | #[derive(Args, Debug, Default, Clone)] 28 | pub struct NewProjectArgs { 29 | /// Project name 30 | #[clap(help = "Project name")] 31 | pub(crate) project_name: Option, 32 | 33 | /// Github repository url. Requires 'git' to be installed in PATH. 34 | #[clap(long)] 35 | pub(crate) git: Option, 36 | 37 | /// Optional subfolder of the git repository 38 | #[clap(long, alias = "subdir")] 39 | pub(crate) subfolder: Option, 40 | 41 | /// Optional github branch. Defaults to "main" 42 | #[clap(long)] 43 | pub(crate) branch: Option, 44 | 45 | /// Optional path for template project (alternative to --git) 46 | #[clap(short, long)] 47 | pub(crate) path: Option, 48 | 49 | /// Optional path to file containing placeholder values 50 | #[clap(short, long)] 51 | pub(crate) values: Option, 52 | 53 | /// Silent - do not prompt user. Placeholder values in the templates 54 | /// will be resolved from a '--values' file and placeholder defaults. 55 | #[clap(long)] 56 | pub(crate) silent: bool, 57 | 58 | /// Favorites file - to use for project selection 59 | #[clap(long)] 60 | pub(crate) favorites: Option, 61 | 62 | /// Template name - name of template to use 63 | #[clap(short, long)] 64 | pub(crate) template_name: Option, 65 | 66 | /// Don't run 'git init' on the new folder 67 | #[clap(long)] 68 | pub(crate) no_git_init: bool, 69 | } 70 | 71 | impl From for Project { 72 | fn from(cmd: NewCliCommand) -> Project { 73 | let (args, kind) = match cmd { 74 | NewCliCommand::Actor(args) => (args, ProjectKind::Actor), 75 | NewCliCommand::Interface(args) => (args, ProjectKind::Interface), 76 | NewCliCommand::Provider(args) => (args, ProjectKind::Provider), 77 | }; 78 | 79 | Project { 80 | kind, 81 | project_name: args.project_name, 82 | values: args.values, 83 | silent: args.silent, 84 | favorites: args.favorites, 85 | template_name: args.template_name, 86 | no_git_init: args.no_git_init, 87 | path: args.path, 88 | git: args.git, 89 | subfolder: args.subfolder, 90 | branch: args.branch, 91 | } 92 | } 93 | } 94 | 95 | pub(crate) async fn handle_command(cmd: NewCliCommand) -> Result { 96 | generate_project(cmd.into()) 97 | .await 98 | .map(|path| CommandOutput { 99 | map: HashMap::from([( 100 | "project_path".to_string(), 101 | json!(path.to_string_lossy().to_string()), 102 | )]), 103 | text: format!( 104 | "Project generated and is located at: {}", 105 | path.to_string_lossy() 106 | ), 107 | }) 108 | .context("Failed to generate project") 109 | } 110 | -------------------------------------------------------------------------------- /src/ui/config.rs: -------------------------------------------------------------------------------- 1 | pub const DEFAULT_WASH_UI_PORT: &str = "3030"; 2 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use rust_embed::RustEmbed; 4 | use warp::Filter; 5 | 6 | use wash_lib::cli::{CommandOutput, OutputKind}; 7 | 8 | mod config; 9 | pub use config::*; 10 | 11 | #[derive(RustEmbed)] 12 | #[folder = "./washboard/dist"] 13 | struct Asset; 14 | 15 | #[derive(Parser, Debug, Clone)] 16 | pub(crate) struct UiCommand { 17 | /// Whist port to run the UI on, defaults to 3030 18 | #[clap(short = 'p', long = "port", default_value = DEFAULT_WASH_UI_PORT)] 19 | pub(crate) port: u16, 20 | } 21 | 22 | pub(crate) async fn handle_command( 23 | command: UiCommand, 24 | output_kind: OutputKind, 25 | ) -> Result { 26 | handle_ui(command, output_kind) 27 | .await 28 | .map(|_| (CommandOutput::default())) 29 | } 30 | 31 | pub(crate) async fn handle_ui(cmd: UiCommand, _output_kind: OutputKind) -> Result<()> { 32 | let static_files = warp::any() 33 | .and(warp::get()) 34 | .and(warp_embed::embed(&Asset)) 35 | .boxed(); 36 | 37 | let cors = warp::cors() 38 | .allow_any_origin() 39 | .allow_methods(vec!["GET", "POST"]) 40 | .allow_headers(vec!["Content-Type"]); 41 | 42 | eprintln!("Washboard running on http://localhost:{}", cmd.port); 43 | eprintln!("Hit CTRL-C to stop"); 44 | 45 | warp::serve(static_files.with(cors)) 46 | .run(([127, 0, 0, 1], cmd.port)) 47 | .await; 48 | 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /src/up/credsfile.rs: -------------------------------------------------------------------------------- 1 | //! A temporary module to parse NATS credsfiles and translate 2 | //! their contents into a JWT and Seed value 3 | use std::path::Path; 4 | 5 | use anyhow::{anyhow, Result}; 6 | use regex::Regex; 7 | use tokio::fs::read_to_string; 8 | 9 | type Jwt = String; 10 | type Seed = String; 11 | 12 | // The code below is largely copied from https://github.com/nats-io/nats.rs/blob/main/async-nats/src/auth_utils.rs 13 | // This is a temporary solution to the fact that a host does not support credsfile authentication 14 | 15 | /// Helper function to parse a credsfile from a path and return a tuple 16 | /// with the JWT and Seed values that were in the credsfile 17 | pub(crate) async fn parse_credsfile

(path: P) -> Result<(Jwt, Seed)> 18 | where 19 | P: AsRef, 20 | { 21 | let contents = read_to_string(path).await?; 22 | let jwt = parse_decorated_jwt(&contents)?; 23 | let seed = parse_decorated_nkey(&contents)?; 24 | 25 | Ok((jwt, seed)) 26 | } 27 | 28 | fn user_config_re() -> Result { 29 | Ok(Regex::new( 30 | r"\s*(?:(?:[-]{3,}.*[-]{3,}\r?\n)([\w\-.=]+)(?:\r?\n[-]{3,}.*[-]{3,}\r?\n))", 31 | )?) 32 | } 33 | 34 | /// Parses a credentials file and returns its user JWT. 35 | fn parse_decorated_jwt(contents: &str) -> Result { 36 | let capture = user_config_re()? 37 | .captures_iter(contents) 38 | .next() 39 | .ok_or_else(|| anyhow!("cannot parse user JWT from the credentials file"))?; 40 | Ok(capture[1].to_string()) 41 | } 42 | 43 | /// Parses a credentials file and returns its nkey. 44 | fn parse_decorated_nkey(contents: &str) -> Result { 45 | let capture = user_config_re()? 46 | .captures_iter(contents) 47 | .nth(1) 48 | .ok_or_else(|| anyhow!("cannot parse user seed from the credentials file"))?; 49 | Ok(capture[1].to_string()) 50 | } 51 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | use common::{output_to_string, wash}; 3 | 4 | #[test] 5 | fn integration_help_subcommand_check() { 6 | let help_output = wash() 7 | .args(["--help"]) 8 | .output() 9 | .expect("failed to display help text"); 10 | let output = output_to_string(help_output).unwrap(); 11 | 12 | assert!(output.contains("claims")); 13 | assert!(output.contains("ctl")); 14 | assert!(output.contains("drain")); 15 | assert!(output.contains("keys")); 16 | assert!(output.contains("par")); 17 | } 18 | -------------------------------------------------------------------------------- /tests/integration_dev.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use anyhow::{anyhow, bail}; 4 | #[cfg(target_family = "unix")] 5 | use anyhow::{Context, Result}; 6 | use serial_test::serial; 7 | use tokio::{process::Command, sync::RwLock, time::Duration}; 8 | 9 | mod common; 10 | 11 | use crate::common::{ 12 | find_open_port, init, start_nats, test_dir_with_subfolder, wait_for_no_hosts, wait_for_no_nats, 13 | }; 14 | 15 | #[tokio::test] 16 | #[serial] 17 | #[cfg(target_family = "unix")] 18 | async fn integration_dev_hello_actor_serial() -> Result<()> { 19 | let test_setup = init( 20 | /* actor_name= */ "hello", /* template_name= */ "hello", 21 | ) 22 | .await?; 23 | let project_dir = test_setup.project_dir; 24 | 25 | let dir = test_dir_with_subfolder("dev_hello_actor"); 26 | 27 | wait_for_no_hosts() 28 | .await 29 | .context("one or more unexpected wasmcloud instances running")?; 30 | 31 | let nats_port = find_open_port().await?; 32 | let mut nats = start_nats(nats_port, &dir).await?; 33 | 34 | let dev_cmd = Arc::new(RwLock::new( 35 | Command::new(env!("CARGO_BIN_EXE_wash")) 36 | .args([ 37 | "dev", 38 | "--nats-port", 39 | nats_port.to_string().as_ref(), 40 | "--nats-connect-only", 41 | "--ctl-port", 42 | nats_port.to_string().as_ref(), 43 | "--use-host-subprocess", 44 | "--disable-wadm", 45 | ]) 46 | .kill_on_drop(true) 47 | .envs(HashMap::from([("WASH_EXPERIMENTAL", "true")])) 48 | .spawn() 49 | .context("failed running cargo dev")?, 50 | )); 51 | let watch_dev_cmd = dev_cmd.clone(); 52 | 53 | let signed_file_path = Arc::new(project_dir.join("build/hello_s.wasm")); 54 | let expected_path = signed_file_path.clone(); 55 | 56 | // Wait until the signed file is there (this means dev succeeded) 57 | let _ = tokio::time::timeout( 58 | Duration::from_secs(1200), 59 | tokio::spawn(async move { 60 | loop { 61 | // If the command failed (and exited early), bail 62 | if let Ok(Some(exit_status)) = watch_dev_cmd.write().await.try_wait() { 63 | if !exit_status.success() { 64 | bail!("dev command failed"); 65 | } 66 | } 67 | // If the file got built, we know dev succeeded 68 | if expected_path.exists() { 69 | break Ok(()); 70 | } 71 | tokio::time::sleep(Duration::from_secs(5)).await; 72 | } 73 | }), 74 | ) 75 | .await 76 | .context("timed out while waiting for file path to get created")?; 77 | assert!(signed_file_path.exists(), "signed actor file was built",); 78 | 79 | let process_pid = dev_cmd 80 | .write() 81 | .await 82 | .id() 83 | .context("failed to get child process pid")?; 84 | 85 | // Send ctrl + c signal to stop the process 86 | // send SIGINT to the child 87 | nix::sys::signal::kill( 88 | nix::unistd::Pid::from_raw(process_pid as i32), 89 | nix::sys::signal::Signal::SIGINT, 90 | ) 91 | .expect("cannot send ctrl-c"); 92 | 93 | // Wait until the process stops 94 | let _ = tokio::time::timeout(Duration::from_secs(15), dev_cmd.write().await.wait()) 95 | .await 96 | .context("dev command did not exit")?; 97 | 98 | wait_for_no_hosts() 99 | .await 100 | .context("wasmcloud instance failed to exit cleanly (processes still left over)")?; 101 | 102 | // Kill the nats instance 103 | nats.kill().await.map_err(|e| anyhow!(e))?; 104 | 105 | wait_for_no_nats() 106 | .await 107 | .context("nats instance failed to exit cleanly (processes still left over)")?; 108 | 109 | Ok(()) 110 | } 111 | -------------------------------------------------------------------------------- /tests/integration_link.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use serial_test::serial; 3 | use tokio::process::Command; 4 | use wash_lib::cli::output::LinkQueryCommandOutput; 5 | 6 | mod common; 7 | use common::TestWashInstance; 8 | 9 | #[tokio::test] 10 | #[serial] 11 | async fn integration_link_serial() -> Result<()> { 12 | let wash = TestWashInstance::create().await?; 13 | 14 | let output = Command::new(env!("CARGO_BIN_EXE_wash")) 15 | .args([ 16 | "link", 17 | "query", 18 | "--output", 19 | "json", 20 | "--ctl-port", 21 | &wash.nats_port.to_string(), 22 | ]) 23 | .kill_on_drop(true) 24 | .output() 25 | .await 26 | .context("failed to execute link query")?; 27 | 28 | assert!(output.status.success(), "executed link query"); 29 | 30 | let cmd_output: LinkQueryCommandOutput = serde_json::from_slice(&output.stdout)?; 31 | assert!(cmd_output.success, "command returned success"); 32 | assert_eq!( 33 | cmd_output.links.len(), 34 | 0, 35 | "links list is empty without any links" 36 | ); 37 | 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /tests/integration_start.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use serial_test::serial; 3 | use tokio::process::Command; 4 | use wash_lib::cli::output::StartCommandOutput; 5 | 6 | mod common; 7 | use common::{TestWashInstance, ECHO_OCI_REF, PROVIDER_HTTPSERVER_OCI_REF}; 8 | 9 | #[tokio::test] 10 | #[serial] 11 | async fn integration_start_actor_serial() -> Result<()> { 12 | let wash_instance = TestWashInstance::create().await?; 13 | 14 | let output = Command::new(env!("CARGO_BIN_EXE_wash")) 15 | .args([ 16 | "start", 17 | "actor", 18 | ECHO_OCI_REF, 19 | "--output", 20 | "json", 21 | "--timeout-ms", 22 | "40000", 23 | "--ctl-port", 24 | &wash_instance.nats_port.to_string(), 25 | ]) 26 | .kill_on_drop(true) 27 | .output() 28 | .await 29 | .context("failed to start actor")?; 30 | 31 | assert!(output.status.success(), "executed start"); 32 | 33 | let cmd_output: StartCommandOutput = 34 | serde_json::from_slice(&output.stdout).context("failed to parse output")?; 35 | assert!(cmd_output.success, "command returned success"); 36 | 37 | Ok(()) 38 | } 39 | 40 | #[tokio::test] 41 | #[serial] 42 | async fn integration_start_provider_serial() -> Result<()> { 43 | let wash_instance = TestWashInstance::create().await?; 44 | 45 | let output = Command::new(env!("CARGO_BIN_EXE_wash")) 46 | .args([ 47 | "start", 48 | "provider", 49 | PROVIDER_HTTPSERVER_OCI_REF, 50 | "--output", 51 | "json", 52 | "--timeout-ms", 53 | "40000", 54 | "--ctl-port", 55 | &wash_instance.nats_port.to_string(), 56 | ]) 57 | .kill_on_drop(true) 58 | .output() 59 | .await 60 | .context("failed to start provider")?; 61 | 62 | assert!(output.status.success(), "executed start"); 63 | 64 | let cmd_output: StartCommandOutput = 65 | serde_json::from_slice(&output.stdout).context("failed to parse output")?; 66 | assert!(cmd_output.success, "command returned success"); 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | ## docker-compose 4 | Bundles [NATS](https://hub.docker.com/_/nats/), [Redis](https://hub.docker.com/_/redis) and [Registry](https://hub.docker.com/_/registry) into a single manifest. These components are commonly used during wasmcloud development and when running our example actors and providers, so it's beneficial to use this compose file when starting your wasmcloud journey. 5 | 6 | ## kvcounter-example 7 | Helper script to run our [keyvalue counter](https://github.com/wasmcloud/examples/tree/master/kvcounter) actor, [redis](https://github.com/wasmcloud/capability-providers/tree/main/redis) capability provider and [httpserver](https://github.com/wasmcloud/capability-providers/tree/main/http-server) capability providers. This example shows the interaction that an actor can have with multiple capability providers, and serves as a sample reference for using `wash` in the CLI or in the REPL. 8 | 9 | Running `bash kvcounter-example.sh` will attempt to determine if the program prerequisites are running (NATS, Redis, and a wasmcloud host) and then execute the following `wash` commands to launch and configure our actors and providers. 10 | ```shell 11 | wash ctl start actor wasmcloud.azurecr.io/kvcounter:0.2.0 12 | wash ctl start provider wasmcloud.azurecr.io/redis:0.10.0 13 | wash ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAZVC4RX54J2NVCMCW7BPCAHGGG5XZXDBXFUMDUXGESTMQEJLC3YVZWB wasmcloud:keyvalue URL=redis://localhost:6379 14 | wash ctl start provider wasmcloud.azurecr.io/httpserver:0.10.0 15 | wash ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M wasmcloud:httpserver PORT=8080 16 | ``` 17 | If a wasmcloud host is not running, the script will simply output the above commands without the `wash` prefix, and indicate that you can run those commands in the `wash` REPL by running `wash up`. Running `wash up` will launch an interactive REPL environment that comes preconfigured with a wasmcloud host. 18 | ``` 19 | No hosts found, please run the wasmcloud binary, or proceed with the following commands in the REPL: 20 | 21 | ctl start actor wasmcloud.azurecr.io/kvcounter:0.2.0 22 | ctl start provider wasmcloud.azurecr.io/redis:0.10.0 23 | ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAZVC4RX54J2NVCMCW7BPCAHGGG5XZXDBXFUMDUXGESTMQEJLC3YVZWB wasmcloud:keyvalue URL=redis://localhost:6379 24 | ctl start provider wasmcloud.azurecr.io/httpserver:0.10.0 25 | ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M wasmcloud:httpserver PORT=8080 26 | ctl call MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E HandleRequest {"method": "GET", "path": "/mycounter", "body": "", "queryString":"", "header":{}} 27 | ``` -------------------------------------------------------------------------------- /tools/deps_check.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | 4 | cargo = shutil.which('cargo') 5 | if cargo is None: 6 | print('cargo not found. Please install Rust from https://rustup.rs/') 7 | exit(1) 8 | 9 | go = shutil.which('go') 10 | if go is None: 11 | print('go not found. Please install it from https://golang.org/') 12 | exit(1) 13 | 14 | tinygo = shutil.which('tinygo') 15 | if tinygo is None: 16 | print('tinygo not found. Please install it from https://tinygo.org/') 17 | exit(1) 18 | 19 | targets = subprocess.run("rustup target list --installed", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout 20 | if "wasm32-unknown-unknown" not in targets: 21 | print('Rust wasm32-unknown-unknown target not found. Installing..."') 22 | subprocess.run('rustup target add wasm32-unknown-unknown', shell=True) 23 | 24 | nextest_output = subprocess.run("cargo nextest --version", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout 25 | if "error: no such command" in nextest_output: 26 | print('cargo nextest not found. Installing..."') 27 | subprocess.run('cargo install cargo-nextest --locked', shell=True) 28 | 29 | watch_output = subprocess.run("cargo watch --version", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout 30 | if "error: no such command" in watch_output: 31 | print('cargo watch not found. Installing..."') 32 | subprocess.run('cargo install cargo-watch', shell=True) 33 | 34 | print("All dependencies are installed!") -------------------------------------------------------------------------------- /tools/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | registry: 4 | image: registry:2 5 | ports: 6 | - "5001:5000" 7 | nats: 8 | image: nats:2.1.9 9 | ports: 10 | - "6222:6222" 11 | - "4222:4222" 12 | - "8222:8222" 13 | redis: 14 | image: redis:6.0.9 15 | ports: 16 | - "6379:6379" 17 | -------------------------------------------------------------------------------- /tools/kvcounter-example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | # KVCounter wasmcloud example 5 | # 6 | # This example starts our `KVCounter` actor, `httpserver` provider and `redis` provider. 7 | # 8 | # The actor simply accepts HTTP requests and increments the value at a key matching the HTTP path. 9 | # e.g., running `curl localhost:8080/mycounter` will add 1 to the redis key `:mycounter` 10 | # 11 | # Please ensure you either run `redis-server` and `nats-server` or use the included 12 | # `docker-compose.yml` to run both of these services before you run this example. 13 | ## 14 | 15 | if ! command -v nc &> /dev/null 16 | then 17 | echo "`nc` program not found. Not able to check for redis and nats" 18 | else 19 | # Check if redis is running 20 | nc localhost 6379 -vz &> /dev/null 21 | if [ $? -ne 0 ] 22 | then 23 | echo "Redis not found on localhost:6379, please ensure redis is running" 24 | exit 25 | fi 26 | 27 | # Check if nats is running 28 | nc localhost 4222 -vz &> /dev/null 29 | if [ $? -ne 0 ] 30 | then 31 | echo "NATS not found on localhost:4222, please ensure nats is running" 32 | exit 33 | fi 34 | fi 35 | 36 | if ! command -v wash &> /dev/null 37 | then 38 | echo "`wash` not found in your path, please install wash or move it to your path" 39 | exit 40 | fi 41 | 42 | echo "Discovering hosts ..." 43 | HOSTS=$(wash ctl get hosts -o json) 44 | if [ $(echo $HOSTS | jq '.hosts | length') -gt 0 ] 45 | then 46 | # The following commands can be run as-is if you have a running wasmcloud host. 47 | # If you don't, you can omit the `wash` part of the command, and run the `ctl` commands in the REPL. 48 | HOST=$(echo $HOSTS | jq ".hosts[0].id" | tr -d "\"") 49 | wash ctl start actor wasmcloud.azurecr.io/kvcounter:0.2.0 -h $HOST 50 | wash ctl start provider wasmcloud.azurecr.io/redis:0.10.0 -h $HOST 51 | wash ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAZVC4RX54J2NVCMCW7BPCAHGGG5XZXDBXFUMDUXGESTMQEJLC3YVZWB wasmcloud:keyvalue URL=redis://localhost:6379 52 | wash ctl start provider wasmcloud.azurecr.io/httpserver:0.10.0 -h $HOST 53 | wash ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M wasmcloud:httpserver PORT=8080 54 | 55 | echo "" 56 | echo "Actors and providers linked and starting, try running one of the following commands to test your KVCounter!" 57 | echo "curl localhost:8080/mycounter 58 | wash ctl call MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E HandleRequest '{\"method\": \"GET\", \"path\": \"/mycounter\", \"body\": \"\", \"queryString\":\"\", \"header\":{}}'" 59 | else 60 | echo "No hosts found, please run the wasmcloud binary, or proceed with the following commands in the REPL:" 61 | echo "" 62 | echo "ctl start actor wasmcloud.azurecr.io/kvcounter:0.2.0 63 | ctl start provider wasmcloud.azurecr.io/redis:0.10.0 64 | ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAZVC4RX54J2NVCMCW7BPCAHGGG5XZXDBXFUMDUXGESTMQEJLC3YVZWB wasmcloud:keyvalue URL=redis://localhost:6379 65 | ctl start provider wasmcloud.azurecr.io/httpserver:0.10.0 66 | ctl link MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E VAG3QITQQ2ODAOWB5TTQSDJ53XK3SHBEIFNK4AYJ5RKAX2UNSCAPHA5M wasmcloud:httpserver PORT=8080 67 | ctl call MCFMFDWFHGKELOXPCNCDXKK5OFLHBVEWRAOXR5JSQUD2TOFRE3DFPM7E HandleRequest {\"method\": \"GET\", \"path\": \"/mycounter\", \"body\": \"\", \"queryString\":\"\", \"header\":{}}" 68 | exit 69 | fi 70 | 71 | -------------------------------------------------------------------------------- /washboard/.env.development: -------------------------------------------------------------------------------- 1 | VITE_NATS_WEBSOCKET_URL=ws://localhost:4001 2 | -------------------------------------------------------------------------------- /washboard/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | *.local 12 | 13 | # Editor directories and files 14 | .vscode/* 15 | !.vscode/extensions.json 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /washboard/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": false 9 | } 10 | -------------------------------------------------------------------------------- /washboard/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## How-To 4 | 5 | ### Contribute code 6 | 7 | 1. Start the wasmCloud host using the `wash` CLI. Read more about it [here](#start-the-wasmcloud-host-using-wash-cli). 8 | 1. Ensure the Nats service is running with the websocket listener enabled. 9 | 2. Start a local frontend development server. Read more about it [here](#start-a-local-ui-development-server). 10 | 3. Make changes to the UI. 11 | 4. Commit your changes. 12 | 5. Open a pull request. 13 | 6. Wait for the CI to pass. 14 | 7. Wait for a maintainer to review your changes. 15 | 8. Wait for a maintainer to merge your changes. 16 | 9. 🚀 🏁 Done 17 | 18 | ### Start a local UI development server 19 | 20 | Run the following command to start a local frontend development server: 21 | 22 | ```bash 23 | npm run dev 24 | ``` 25 | 26 | ### Start the wasmCloud Host using wash CLI 27 | 28 | Run the following command to start the wasmCloud host using the wash CLI: 29 | 30 | ```bash 31 | wash up 32 | ``` 33 | 34 | ### Explanations 35 | 36 | #### Nats 37 | 38 | `wasmcloud` uses [NATS](https://nats.io/) as its message broker. The `wash` CLI can be used to start a local NATS 39 | or connect to an existing NATS server. 40 | 41 | The Washboard UI connects to a NATS server at [ws://localhost:4001 by default][0], although this can be overridden via 42 | the UI. 43 | 44 | In case you use `wash up` to spawn up the Nats server, you can control the websocket port using the 45 | `--nats-websocket-port` flag or `NATS_WEBSOCKET_PORT` environment variable. For example: 46 | 47 | ```bash 48 | wash up --nats-websocket-port 4008 49 | ``` 50 | 51 | Otherwise, verify the port you are using to connect to the NATS server. Visit [Nats Websocket Configuration][1] for more 52 | information. 53 | 54 | [0]: https://github.com/wasmCloud/wash/blob/a74b50297496578e5e6c0ee806304a3ff05cd073/packages/washboard/src/lattice/lattice-service.ts#L70 55 | [1]: https://docs.nats.io/running-a-nats-service/configuration/websocket/websocket_conf 56 | -------------------------------------------------------------------------------- /washboard/README.md: -------------------------------------------------------------------------------- 1 | # Washboard 2 | 3 | ## References 4 | 5 | ### Environment Variables 6 | 7 | Please read more about [Env Variables and Modes][0]. 8 | 9 | - `VITE_NATS_WEBSOCKET_URL` - Default NATS websocket URL. 10 | 11 | [0]: https://vitejs.dev/guide/env-and-mode.html 12 | -------------------------------------------------------------------------------- /washboard/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tailwind": { 6 | "config": "tailwind.config.js", 7 | "css": "src/index.css", 8 | "baseColor": "slate", 9 | "cssVariables": true 10 | }, 11 | "aliases": { 12 | "components": "/", 13 | "utils": "lib/utils" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /washboard/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | wasmCloud UI | Washboard 🏄 9 | 19 | 20 | 21 | 22 | 23 | 24 |

25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /washboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | wasmCloud UI | Washboard 🏄 9 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /washboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "washboard", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@hookform/resolvers": "^3.3.2", 14 | "@radix-ui/react-accordion": "^1.1.2", 15 | "@radix-ui/react-alert-dialog": "^1.0.5", 16 | "@radix-ui/react-checkbox": "^1.0.4", 17 | "@radix-ui/react-dialog": "^1.0.5", 18 | "@radix-ui/react-icons": "^1.3.0", 19 | "@radix-ui/react-label": "^2.0.2", 20 | "@radix-ui/react-popover": "^1.0.7", 21 | "@radix-ui/react-radio-group": "^1.1.3", 22 | "@radix-ui/react-select": "^2.0.0", 23 | "@radix-ui/react-slider": "^1.1.2", 24 | "@radix-ui/react-slot": "^1.0.2", 25 | "@radix-ui/react-switch": "^1.0.3", 26 | "@radix-ui/react-tabs": "^1.0.4", 27 | "@radix-ui/react-toast": "^1.1.5", 28 | "@radix-ui/react-toggle": "^1.0.3", 29 | "@radix-ui/react-tooltip": "^1.0.7", 30 | "@tanstack/react-table": "^8.10.7", 31 | "class-variance-authority": "^0.7.0", 32 | "clsx": "^2.0.0", 33 | "date-fns": "^2.30.0", 34 | "immer": "^10.0.2", 35 | "lucide-react": "^0.289.0", 36 | "nats.ws": "^1.18.0", 37 | "react": "^18.2.0", 38 | "react-dom": "^18.2.0", 39 | "react-hook-form": "^7.46.2", 40 | "react-router-dom": "^6.17.0", 41 | "rxjs": "^7.8.1", 42 | "tailwind-merge": "^1.14.0", 43 | "tailwindcss-animate": "^1.0.7", 44 | "usehooks-ts": "^2.9.1", 45 | "vite-tsconfig-paths": "^4.2.1", 46 | "zod": "^3.22.3" 47 | }, 48 | "devDependencies": { 49 | "@types/node": "^20.8.9", 50 | "@types/react": "^18.2.33", 51 | "@types/react-dom": "^18.2.14", 52 | "@typescript-eslint/eslint-plugin": "^6.9.0", 53 | "@typescript-eslint/parser": "^6.9.0", 54 | "@vitejs/plugin-react": "^4.1.0", 55 | "@vitejs/plugin-react-swc": "^3.4.0", 56 | "autoprefixer": "^10.4.16", 57 | "eslint": "^8.52.0", 58 | "eslint-config-prettier": "^9.0.0", 59 | "eslint-import-resolver-typescript": "^3.6.1", 60 | "eslint-plugin-absolute-imports-only": "^1.0.1", 61 | "eslint-plugin-eslint-comments": "^3.2.0", 62 | "eslint-plugin-import": "^2.29.0", 63 | "eslint-plugin-jsx-a11y": "^6.7.1", 64 | "eslint-plugin-prettier": "^5.0.1", 65 | "eslint-plugin-react": "^7.33.2", 66 | "eslint-plugin-react-hooks": "^4.6.0", 67 | "eslint-plugin-react-refresh": "^0.4.3", 68 | "eslint-plugin-tailwindcss": "^3.13.0", 69 | "eslint-plugin-unicorn": "^48.0.1", 70 | "postcss": "^8.4.30", 71 | "prettier": "^3.0.3", 72 | "tailwindcss": "^3.3.5", 73 | "ts-node": "^10.9.1", 74 | "typescript": "^5.2.2", 75 | "vite": "^4.5.0", 76 | "vite-plugin-svgr": "^4.0.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /washboard/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /washboard/src/App.tsx: -------------------------------------------------------------------------------- 1 | import {ReactElement} from 'react'; 2 | import {RouterProvider, createBrowserRouter} from 'react-router-dom'; 3 | import AppProvider from 'lib/AppProvider'; 4 | import {routes} from 'routes'; 5 | import {SettingsProvider} from 'settings/SettingsContext'; 6 | 7 | function App(): ReactElement { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /washboard/src/actors/count-instances.ts: -------------------------------------------------------------------------------- 1 | import {WadmActor} from 'lattice/lattice-service'; 2 | 3 | function countInstances(instances: WadmActor['instances']): number { 4 | return Object.values(instances).reduce((accumulator, current) => accumulator + current.length, 0); 5 | } 6 | 7 | export {countInstances}; 8 | -------------------------------------------------------------------------------- /washboard/src/assets/logo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /washboard/src/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import {ReactElement} from 'react'; 2 | import ActorsTable from 'actors/ActorsTable'; 3 | import {HostsSummary} from 'hosts'; 4 | import useLatticeData from 'lattice/use-lattice-data'; 5 | import LinksTable from 'links/LinksTable'; 6 | import ProvidersTable from 'providers/ProvidersTable'; 7 | import {Card, CardContent, CardHeader} from 'ui/card'; 8 | import StatsTile from './StatsTile'; 9 | 10 | function Dashboard(): ReactElement { 11 | const {hosts, actors, providers, links} = useLatticeData(); 12 | 13 | const hostsCount = Object.keys(hosts).length.toString(); 14 | const actorsCount = Object.keys(actors).length.toString(); 15 | const providersCount = Object.keys(providers).length.toString(); 16 | const linksCount = links.length.toString(); 17 | 18 | return ( 19 |
20 |
21 |
22 |
23 |

Overview

24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 | 37 | Components 38 | 39 | 40 | 41 | 42 | 43 | Providers 44 | 45 | 46 | 47 | 48 | 49 | Links 50 | 51 | 52 | 53 | 54 |
55 | ); 56 | } 57 | 58 | export default Dashboard; 59 | -------------------------------------------------------------------------------- /washboard/src/dashboard/StatsTile.tsx: -------------------------------------------------------------------------------- 1 | import {ReactElement} from 'react'; 2 | import {Card} from 'ui/card'; 3 | 4 | interface StatsTileProps { 5 | title: string; 6 | value: string; 7 | } 8 | 9 | function StatsTile({title, value}: StatsTileProps): ReactElement { 10 | return ( 11 | 12 |
13 | {title} 14 | {value} 15 | 16 | ); 17 | } 18 | 19 | export default StatsTile; 20 | -------------------------------------------------------------------------------- /washboard/src/hosts/HostsSummary.tsx: -------------------------------------------------------------------------------- 1 | import {formatDistanceToNow, formatDuration, intervalToDuration} from 'date-fns'; 2 | import {ReactElement} from 'react'; 3 | import useLatticeData from 'lattice/use-lattice-data'; 4 | import {Accordion, AccordionContent, AccordionItem, AccordionTrigger} from 'ui/accordion'; 5 | import {Badge} from 'ui/badge'; 6 | import {ShortCopy} from 'ui/short-copy'; 7 | import {Table, TableBody, TableCell, TableHead, TableRow} from 'ui/table'; 8 | 9 | export function HostsSummary(): ReactElement { 10 | const {hosts} = useLatticeData(); 11 | 12 | const hostsArray = Object.values(hosts).sort((a, b) => (a.id > b.id ? 1 : -1)); 13 | 14 | return ( 15 |
16 |

Hosts

17 |
18 | 19 | {hostsArray.map((host) => ( 20 | 21 | 22 |
23 | {host.version} 24 | {host.friendly_name} 25 |
26 |
27 | 28 | 29 | 30 | 31 | Uptime 32 | 33 | {formatDuration( 34 | intervalToDuration({start: 0, end: host.uptime_seconds * 1000}), 35 | )} 36 | 37 | 38 | 39 | Last Seen 40 | 41 | {formatDistanceToNow(new Date(host.last_seen), {addSuffix: true})} 42 | 43 | 44 | 45 | Host ID 46 | 47 | 51 | 52 | 53 | 54 | Components 55 | {Object.values(host.actors).length.toString()} 56 | 57 | 58 | Providers 59 | {Object.values(host.providers).length.toString()} 60 | 61 | 62 | Labels 63 | 64 | {Object.entries(host.labels).map(([key, value]) => ( 65 | 66 | {key}={value} 67 | 68 | ))} 69 | 70 | 71 | 72 |
73 |
74 |
75 | ))} 76 |
77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /washboard/src/hosts/index.ts: -------------------------------------------------------------------------------- 1 | export {HostsSummary} from './HostsSummary'; 2 | -------------------------------------------------------------------------------- /washboard/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: 0 0% 100%; 7 | --foreground: 222.2 84% 4.9%; 8 | 9 | --muted: 210 40% 96.1%; 10 | --muted-foreground: 215.4 16.3% 46.9%; 11 | 12 | --popover: 0 0% 100%; 13 | --popover-foreground: 222.2 84% 4.9%; 14 | 15 | --card: 0 0% 100%; 16 | --card-foreground: 222.2 84% 4.9%; 17 | 18 | --border: 214.3 31.8% 91.4%; 19 | --input: 214.3 31.8% 91.4%; 20 | 21 | --primary: 222.2 47.4% 11.2%; 22 | --primary-foreground: 210 40% 98%; 23 | 24 | --secondary: 210 40% 96.1%; 25 | --secondary-foreground: 222.2 47.4% 11.2%; 26 | 27 | --accent: 165, 100%, 89%; 28 | --accent-foreground: 165, 100%, 16%; 29 | 30 | --destructive: 0 84.2% 60.2%; 31 | --destructive-foreground: 210 40% 98%; 32 | 33 | --brand: 165, 100%, 37%; 34 | --brand-foreground: 167, 100%, 5%; 35 | 36 | --ring: 215 20.2% 65.1%; 37 | 38 | --radius: 0.5rem; 39 | } 40 | 41 | .dark { 42 | --background: 222.2 84% 4.9%; 43 | --foreground: 210 40% 98%; 44 | 45 | --muted: 217.2 32.6% 17.5%; 46 | --muted-foreground: 215 20.2% 65.1%; 47 | 48 | --popover: 222.2 84% 4.9%; 49 | --popover-foreground: 210 40% 98%; 50 | 51 | --card: 222.2 84% 4.9%; 52 | --card-foreground: 210 40% 98%; 53 | 54 | --border: 217.2 32.6% 17.5%; 55 | --input: 217.2 32.6% 17.5%; 56 | 57 | --primary: 210 40% 98%; 58 | --primary-foreground: 222.2 47.4% 11.2%; 59 | 60 | --secondary: 217.2 32.6% 17.5%; 61 | --secondary-foreground: 210 40% 98%; 62 | 63 | --accent: 165, 100%, 9%; 64 | --accent-foreground: 165, 100%, 89%; 65 | 66 | --destructive: 0 84.2% 60.2%; 67 | --destructive-foreground: 210 40% 98%; 68 | 69 | --brand: 165, 100%, 23%; 70 | --brand-foreground: 165, 100%, 95%; 71 | 72 | --ring: 217.2 32.6% 17.5%; 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | body, 80 | #root { 81 | @apply min-h-[100vh] min-w-[100vw] w-screen h-screen; 82 | } 83 | body { 84 | @apply bg-background text-foreground; 85 | } 86 | #root { 87 | display: flex; 88 | flex-direction: column; 89 | overflow: auto; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /washboard/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom/client'; 4 | import App from './App.tsx'; 5 | 6 | ReactDOM.createRoot(document.querySelector('#root') as HTMLElement).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /washboard/src/lattice/LatticeSettings.tsx: -------------------------------------------------------------------------------- 1 | import {zodResolver} from '@hookform/resolvers/zod'; 2 | import {ReactElement, useEffect} from 'react'; 3 | import {useForm} from 'react-hook-form'; 4 | import * as z from 'zod'; 5 | import {Button} from 'ui/button'; 6 | import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from 'ui/form'; 7 | import {Input} from 'ui/input'; 8 | import LatticeService from './lattice-service'; 9 | import {useLatticeConfig} from './use-lattice-config'; 10 | 11 | type Props = { 12 | onSave: (event: z.infer) => void; 13 | }; 14 | 15 | const formSchema = z.object({ 16 | latticeUrl: z 17 | .string() 18 | .url({ 19 | message: 'Please enter a valid URL', 20 | }) 21 | .refine( 22 | (latticeId) => { 23 | return LatticeService.getInstance().testConnection(latticeId); 24 | }, 25 | {message: 'Could not connect to Lattice'}, 26 | ), 27 | }); 28 | 29 | function LatticeSettings({onSave}: Props): ReactElement { 30 | const { 31 | config: {latticeUrl}, 32 | setConfig, 33 | } = useLatticeConfig(); 34 | const form = useForm>({ 35 | resolver: zodResolver(formSchema), 36 | defaultValues: { 37 | latticeUrl: latticeUrl, 38 | }, 39 | }); 40 | 41 | const handleSave = (data: z.infer): void => { 42 | onSave(data); 43 | setConfig('latticeUrl', form.getValues('latticeUrl')); 44 | }; 45 | 46 | useEffect(() => { 47 | form.setValue('latticeUrl', latticeUrl); 48 | }, [form, latticeUrl]); 49 | 50 | const hasErrors = Object.values(form.formState.errors).length > 0; 51 | 52 | return ( 53 |
54 | 55 | ( 59 | 60 | Server URL 61 | 62 | 63 | 64 | 65 | 66 | )} 67 | /> 68 |
69 | 72 |
73 | 74 | 75 | ); 76 | } 77 | 78 | export default LatticeSettings; 79 | -------------------------------------------------------------------------------- /washboard/src/lattice/cloud-event.type.ts: -------------------------------------------------------------------------------- 1 | export interface CloudEvent { 2 | data: T; 3 | datacontenttype: string; 4 | id: string; 5 | source: string; 6 | specversion: string; 7 | time: string; 8 | type: string; 9 | } 10 | -------------------------------------------------------------------------------- /washboard/src/lattice/use-lattice-config.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import LatticeService from './lattice-service'; 3 | 4 | type SetConfigFunction = ( 5 | key: K, 6 | value: K extends keyof LatticeService ? LatticeService[typeof key] : never, 7 | ) => void; 8 | 9 | interface UseLatticeConfigResult { 10 | config: { 11 | latticeUrl: string; 12 | }; 13 | setConfig: SetConfigFunction; 14 | } 15 | 16 | function useLatticeConfig(): UseLatticeConfigResult { 17 | const service = React.useMemo(() => LatticeService.getInstance(), []); 18 | const setConfig = React.useCallback( 19 | (key, value) => { 20 | service[key] = value; 21 | }, 22 | [service], 23 | ); 24 | return { 25 | config: { 26 | latticeUrl: service.latticeUrl, 27 | }, 28 | setConfig, 29 | }; 30 | } 31 | 32 | export {useLatticeConfig}; 33 | -------------------------------------------------------------------------------- /washboard/src/lattice/use-lattice-data.ts: -------------------------------------------------------------------------------- 1 | import {useDebugValue, useEffect, useMemo, useState} from 'react'; 2 | import LatticeService, {LatticeCache} from './lattice-service'; 3 | 4 | function useLatticeData(): LatticeCache { 5 | const service = useMemo(() => LatticeService.getInstance(), []); 6 | const [state, handleStateUpdate] = useState({ 7 | hosts: {}, 8 | actors: {}, 9 | providers: {}, 10 | links: [], 11 | }); 12 | useDebugValue(state); 13 | 14 | useEffect(() => { 15 | const sub = service.getLatticeCache$().subscribe(handleStateUpdate); 16 | return () => { 17 | sub.unsubscribe(); 18 | }; 19 | }, [service]); 20 | 21 | return state; 22 | } 23 | 24 | export default useLatticeData; 25 | -------------------------------------------------------------------------------- /washboard/src/layout/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import {ReactElement} from 'react'; 2 | import {Outlet} from 'react-router'; 3 | import Navigation from './Navigation'; 4 | 5 | function AppLayout(): ReactElement { 6 | return ( 7 |
8 | 9 |
10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | export default AppLayout; 17 | -------------------------------------------------------------------------------- /washboard/src/layout/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import {ReactElement} from 'react'; 2 | import SvgLogo from 'assets/logo-wide.svg?react'; 3 | import {Settings} from 'settings'; 4 | 5 | function Navigation(): ReactElement { 6 | return ( 7 |
8 |
9 |
10 | 11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default Navigation; 19 | -------------------------------------------------------------------------------- /washboard/src/lib/AppProvider.tsx: -------------------------------------------------------------------------------- 1 | import {JSXElementConstructor, ReactElement, ReactNode} from 'react'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-explicit-any -- could be anything for props 4 | interface AppProps { 5 | components: Array>; 6 | children: ReactNode; 7 | } 8 | 9 | export default function AppProvider(props: AppProps): ReactElement { 10 | return ( 11 | <> 12 | {props.components.reduceRight((accumulator, Component) => { 13 | return {accumulator}; 14 | }, props.children)} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /washboard/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import {type ClassValue, clsx} from 'clsx'; 2 | import {twMerge} from 'tailwind-merge'; 3 | 4 | /** 5 | * Combine clsx objects into a single string, and merge tailwind classes (i.e. `mb-4 mb-8` becomes `mb-8`) 6 | * @param inputs 1 or more `ClassValue`s 7 | * @returns a single CSS class name string, with all clsx objects removed and tailwind classes merged 8 | * @see {@link https://github.com/lukeed/clsx} for more information on `clsx` 9 | * @see {@link https://github.com/dcastil/tailwind-merge} for more information on `twMerge` 10 | */ 11 | export function cn(...inputs: ClassValue[]): string { 12 | return twMerge(clsx(inputs)); 13 | } 14 | -------------------------------------------------------------------------------- /washboard/src/links/LinksTable.tsx: -------------------------------------------------------------------------------- 1 | import {ColumnDef, createColumnHelper} from '@tanstack/react-table'; 2 | import {ReactElement} from 'react'; 3 | import {WadmLink} from 'lattice/lattice-service'; 4 | import useLatticeData from 'lattice/use-lattice-data'; 5 | import {DataTable} from 'ui/data-table'; 6 | import {ShortCopy} from 'ui/short-copy'; 7 | 8 | const columnHelper = createColumnHelper(); 9 | 10 | const columns = [ 11 | columnHelper.accessor('contract_id', { 12 | header: 'Contract ID', 13 | }), 14 | columnHelper.accessor('link_name', { 15 | header: 'Link Name', 16 | }), 17 | columnHelper.accessor('provider_id', { 18 | header: 'Provider ID', 19 | cell: (info) => , 20 | }), 21 | columnHelper.accessor('actor_id', { 22 | header: 'Component ID', 23 | cell: (info) => , 24 | }), 25 | ]; 26 | 27 | function LinksTable(): ReactElement { 28 | const {links} = useLatticeData(); 29 | 30 | return ( 31 |
32 | []} /> 33 |
34 | ); 35 | } 36 | 37 | export default LinksTable; 38 | -------------------------------------------------------------------------------- /washboard/src/routes/get-breadcrumbs.ts: -------------------------------------------------------------------------------- 1 | import {AppRouteObject} from 'routes'; 2 | 3 | export function getBreadcrumbs(route: AppRouteObject): AppRouteObject[] { 4 | const breadcrumbs: AppRouteObject[] = []; 5 | let current: AppRouteObject | undefined = route; 6 | while (current) { 7 | if (!current.handle.hideInBreadcrumb) { 8 | breadcrumbs.unshift(current); 9 | } 10 | current = current.children?.[0]; 11 | } 12 | return breadcrumbs; 13 | } 14 | -------------------------------------------------------------------------------- /washboard/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import {Home} from 'lucide-react'; 2 | import {ReactElement} from 'react'; 3 | import {RouteObject} from 'react-router-dom'; 4 | import Dashboard from 'dashboard/Dashboard'; 5 | import AppLayout from 'layout/AppLayout'; 6 | 7 | export type AppRouteObject = RouteObject & { 8 | handle?: { 9 | title?: string; 10 | breadcrumbTitle?: string; 11 | icon?: ReactElement; 12 | hideInMenu?: boolean; 13 | hideInBreadcrumb?: boolean; 14 | }; 15 | children?: AppRouteObject[]; 16 | }; 17 | 18 | export const routes: RouteObject[] = [ 19 | { 20 | element: , 21 | children: [ 22 | { 23 | index: true, 24 | path: '/', 25 | element: , 26 | handle: { 27 | breadcrumbTitle: 'Washboard', 28 | title: 'Washboard', 29 | icon: , 30 | }, 31 | }, 32 | ], 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /washboard/src/settings/DarkModeToggle.tsx: -------------------------------------------------------------------------------- 1 | import * as SelectPrimitive from '@radix-ui/react-select'; 2 | import {ComponentPropsWithoutRef, ElementRef, forwardRef} from 'react'; 3 | import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from 'ui/select'; 4 | import {useSettings} from './use-settings'; 5 | 6 | const DarkModeToggle = forwardRef< 7 | ElementRef, 8 | ComponentPropsWithoutRef 9 | >((props, ref) => { 10 | const {darkMode, setDarkMode} = useSettings(); 11 | 12 | return ( 13 | 23 | ); 24 | }); 25 | 26 | DarkModeToggle.displayName = 'DarkModeToggle'; 27 | 28 | export {DarkModeToggle}; 29 | -------------------------------------------------------------------------------- /washboard/src/settings/Settings.tsx: -------------------------------------------------------------------------------- 1 | import {SettingsIcon} from 'lucide-react'; 2 | import {PropsWithChildren, ReactElement, useState} from 'react'; 3 | import LatticeSettings from 'lattice/LatticeSettings'; 4 | import {Button} from 'ui/button'; 5 | import {Label} from 'ui/label'; 6 | import {Popover, PopoverContent, PopoverTrigger} from 'ui/popover'; 7 | import {DarkModeToggle} from './DarkModeToggle'; 8 | 9 | function Settings(): ReactElement { 10 | const [open, setOpen] = useState(false); 11 | 12 | return ( 13 | 14 | setOpen(!open)}> 15 | 19 | 20 | 21 | 22 | Display 23 |
24 | 25 | 26 |
27 |
28 | 29 | Lattice Configuration 30 | setOpen(false)} /> 31 |
32 |
33 | ); 34 | } 35 | 36 | function SettingsSection({children}: PropsWithChildren): ReactElement { 37 | return
{children}
; 38 | } 39 | 40 | function SettingsSectionLabel({children}: PropsWithChildren): ReactElement { 41 | return
{children}
; 42 | } 43 | 44 | export {Settings}; 45 | -------------------------------------------------------------------------------- /washboard/src/settings/SettingsContext.tsx: -------------------------------------------------------------------------------- 1 | import {PropsWithChildren, ReactElement, createContext, useEffect} from 'react'; 2 | import {useLocalStorage} from 'usehooks-ts'; 3 | 4 | enum DarkModeOption { 5 | Dark = 'dark', 6 | Light = 'light', 7 | System = 'system', 8 | } 9 | 10 | export interface SettingsContextValue { 11 | darkMode: DarkModeOption; 12 | setDarkMode: (darkMode: DarkModeOption) => void; 13 | } 14 | 15 | export const SettingsContext = createContext({ 16 | darkMode: DarkModeOption.System, 17 | setDarkMode: () => null, 18 | }); 19 | 20 | export function SettingsProvider({children}: PropsWithChildren): ReactElement { 21 | const [darkMode, setDarkMode] = useLocalStorage('theme', DarkModeOption.System); 22 | 23 | // sync state with localStorage 24 | useEffect(() => { 25 | if ( 26 | darkMode === DarkModeOption.Dark || 27 | (darkMode === DarkModeOption.System && 28 | window.matchMedia('(prefers-color-scheme: dark)').matches) 29 | ) { 30 | document.documentElement.classList.add('dark'); 31 | } else { 32 | document.documentElement.classList.remove('dark'); 33 | } 34 | }, [darkMode]); 35 | 36 | return ( 37 | {children} 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /washboard/src/settings/index.ts: -------------------------------------------------------------------------------- 1 | export {Settings} from './Settings'; 2 | export {DarkModeToggle} from './DarkModeToggle'; 3 | -------------------------------------------------------------------------------- /washboard/src/settings/use-settings.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | import {SettingsContext, SettingsContextValue} from './SettingsContext'; 3 | 4 | export function useSettings(): SettingsContextValue { 5 | return useContext(SettingsContext); 6 | } 7 | -------------------------------------------------------------------------------- /washboard/src/svgr.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /washboard/src/table.d.ts: -------------------------------------------------------------------------------- 1 | import {CellContext, RowData} from '@tanstack/react-table'; 2 | import * as React from 'react'; 3 | 4 | declare module '@tanstack/table-core' { 5 | interface ColumnMeta { 6 | baseRow: 'visible' | 'hidden' | 'empty'; 7 | expandedRow: 'visible' | 'hidden' | 'empty'; 8 | expandedCell?: ( 9 | key: string, 10 | expandedRows: any, 11 | ) => (info: CellContext) => React.ReactElement | string | number; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /washboard/src/ui/accordion/index.tsx: -------------------------------------------------------------------------------- 1 | import * as AccordionPrimitive from '@radix-ui/react-accordion'; 2 | import {ChevronDownIcon} from '@radix-ui/react-icons'; 3 | import * as React from 'react'; 4 | 5 | import {cn} from 'lib/utils'; 6 | 7 | const Accordion = AccordionPrimitive.Root; 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({className, ...props}, ref) => ( 13 | 14 | )); 15 | AccordionItem.displayName = 'AccordionItem'; 16 | 17 | const AccordionTrigger = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({className, children, ...props}, ref) => ( 21 | 22 | svg]:rotate-180', 26 | className, 27 | )} 28 | {...props} 29 | > 30 | {children} 31 | 32 | 33 | 34 | )); 35 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 36 | 37 | const AccordionContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({className, children, ...props}, ref) => ( 41 | 49 |
{children}
50 |
51 | )); 52 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 53 | 54 | export {Accordion, AccordionItem, AccordionTrigger, AccordionContent}; 55 | -------------------------------------------------------------------------------- /washboard/src/ui/badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | import {type VariantProps} from 'class-variance-authority'; 2 | import * as React from 'react'; 3 | 4 | import {cn} from 'lib/utils'; 5 | import {badgeVariants} from './variants'; 6 | 7 | export interface BadgeProps 8 | extends React.HTMLAttributes, 9 | VariantProps {} 10 | 11 | function Badge({className, variant, ...props}: BadgeProps): React.ReactElement { 12 | return
; 13 | } 14 | 15 | export default Badge; 16 | -------------------------------------------------------------------------------- /washboard/src/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Badge} from './Badge'; 2 | export {badgeVariants} from './variants'; 3 | -------------------------------------------------------------------------------- /washboard/src/ui/badge/variants.ts: -------------------------------------------------------------------------------- 1 | import {cva} from 'class-variance-authority'; 2 | 3 | export const badgeVariants = cva( 4 | 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 5 | { 6 | variants: { 7 | variant: { 8 | default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', 9 | secondary: 10 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 11 | destructive: 12 | 'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', 13 | outline: 'text-foreground', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /washboard/src/ui/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import {Slot} from '@radix-ui/react-slot'; 2 | import {type VariantProps} from 'class-variance-authority'; 3 | import * as React from 'react'; 4 | import {cn} from 'lib/utils'; 5 | import {buttonVariants} from './variants'; 6 | 7 | export interface ButtonProps 8 | extends React.ButtonHTMLAttributes, 9 | VariantProps { 10 | asChild?: boolean; 11 | } 12 | 13 | const Button = React.forwardRef( 14 | ({className, variant, size, asChild = false, ...props}, ref) => { 15 | const Comp = asChild ? Slot : 'button'; 16 | return ; 17 | }, 18 | ); 19 | Button.displayName = 'Button'; 20 | 21 | export default Button; 22 | -------------------------------------------------------------------------------- /washboard/src/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Button} from './Button'; 2 | export {buttonVariants} from './variants'; 3 | -------------------------------------------------------------------------------- /washboard/src/ui/button/variants.ts: -------------------------------------------------------------------------------- 1 | import {cva} from 'class-variance-authority'; 2 | 3 | export const buttonVariants = cva( 4 | 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', 5 | { 6 | variants: { 7 | variant: { 8 | default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 9 | destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 10 | outline: 11 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 12 | secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 13 | ghost: 'hover:bg-accent hover:text-accent-foreground', 14 | link: 'text-primary underline-offset-4 hover:underline', 15 | }, 16 | size: { 17 | default: 'h-9 px-4 py-2', 18 | sm: 'h-8 rounded-md px-3 text-xs', 19 | lg: 'h-10 rounded-md px-8', 20 | icon: 'h-9 w-9', 21 | }, 22 | }, 23 | defaultVariants: { 24 | variant: 'default', 25 | size: 'default', 26 | }, 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /washboard/src/ui/card/index.tsx: -------------------------------------------------------------------------------- 1 | import {cva} from 'class-variance-authority'; 2 | import * as React from 'react'; 3 | 4 | import {cn} from 'lib/utils'; 5 | 6 | interface CardProps extends React.HTMLAttributes { 7 | variant?: 'default' | 'accent'; 8 | } 9 | 10 | const cardVariants = cva('rounded-xl border bg-card text-card-foreground', { 11 | variants: { 12 | variant: { 13 | default: 'border border-muted-foreground', 14 | accent: 'border-accent bg-accent text-accent-foreground', 15 | }, 16 | }, 17 | }); 18 | 19 | const Card = React.forwardRef( 20 | ({variant = 'default', className, ...props}, ref) => ( 21 |
22 | ), 23 | ); 24 | Card.displayName = 'Card'; 25 | 26 | const CardHeader = React.forwardRef>( 27 | ({className, ...props}, ref) => ( 28 |
29 | ), 30 | ); 31 | CardHeader.displayName = 'CardHeader'; 32 | 33 | const CardTitle = React.forwardRef>( 34 | ({className, ...props}, ref) => ( 35 |

40 | ), 41 | ); 42 | CardTitle.displayName = 'CardTitle'; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLParagraphElement, 46 | React.HTMLAttributes 47 | >(({className, ...props}, ref) => ( 48 |

49 | )); 50 | CardDescription.displayName = 'CardDescription'; 51 | 52 | const CardContent = React.forwardRef>( 53 | ({className, ...props}, ref) => ( 54 |

55 | ), 56 | ); 57 | CardContent.displayName = 'CardContent'; 58 | 59 | const CardFooter = React.forwardRef>( 60 | ({className, ...props}, ref) => ( 61 |
62 | ), 63 | ); 64 | CardFooter.displayName = 'CardFooter'; 65 | 66 | export {Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent}; 67 | -------------------------------------------------------------------------------- /washboard/src/ui/checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 2 | import {CheckIcon} from '@radix-ui/react-icons'; 3 | import * as React from 'react'; 4 | 5 | import {cn} from 'lib/utils'; 6 | 7 | const Checkbox = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({className, ...props}, ref) => ( 11 | 19 | 20 | 21 | 22 | 23 | )); 24 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 25 | 26 | export {Checkbox}; 27 | -------------------------------------------------------------------------------- /washboard/src/ui/collapsible/index.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 2 | import * as React from 'react'; 3 | 4 | const OpenContext = React.createContext<{ 5 | open: boolean; 6 | setOpen: React.Dispatch>; 7 | }>({ 8 | open: false, 9 | setOpen: () => null, 10 | }); 11 | 12 | function CollapsibleController({ 13 | controlledOpen = false, 14 | children, 15 | }: React.PropsWithChildren<{ 16 | controlledOpen?: boolean; 17 | }>): React.ReactElement { 18 | const [open, setOpen] = React.useState(controlledOpen); 19 | return {children}; 20 | } 21 | 22 | const Collapsible = React.forwardRef< 23 | HTMLDivElement, 24 | CollapsiblePrimitive.CollapsibleProps & React.RefAttributes 25 | >((props, ref) => { 26 | const [open, setOpen] = React.useState(false); 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }); 33 | Collapsible.displayName = 'Collapsible'; 34 | 35 | const CollapsibleTrigger = React.forwardRef< 36 | HTMLButtonElement, 37 | Omit & 38 | React.RefAttributes & { 39 | children: React.ReactElement | ((open: boolean) => React.ReactElement); 40 | } 41 | >(({children, ...props}, ref) => { 42 | const {open, setOpen} = React.useContext(OpenContext); 43 | return ( 44 | setOpen((previous) => !previous)} 46 | onKeyDown={(event): void => setOpen(event.key === 'Enter' || event.key === ' ')} 47 | ref={ref} 48 | tabIndex={0} 49 | role="button" 50 | {...props} 51 | > 52 | {typeof children === 'function' ? children(open) : children} 53 | 54 | ); 55 | }); 56 | CollapsibleTrigger.displayName = 'CollapsibleTrigger'; 57 | 58 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 59 | 60 | export {Collapsible, CollapsibleTrigger, CollapsibleContent, CollapsibleController}; 61 | -------------------------------------------------------------------------------- /washboard/src/ui/copy-button/index.tsx: -------------------------------------------------------------------------------- 1 | import {Slot} from '@radix-ui/react-slot'; 2 | import {Check, Copy} from 'lucide-react'; 3 | import {MouseEvent, ReactNode, forwardRef, useEffect, useState} from 'react'; 4 | import {cn} from 'lib/utils'; 5 | import {Button} from 'ui/button'; 6 | import {ButtonProps} from 'ui/button/Button'; 7 | 8 | interface CopyButtonProps extends Omit { 9 | text: string; 10 | children?: ReactNode | ((copied: boolean) => ReactNode); 11 | } 12 | 13 | const CopyButton = forwardRef( 14 | ({asChild, onClick, children, className, ...props}: CopyButtonProps, ref) => { 15 | const Comp = asChild ? Slot : Button; 16 | const [copied, setCopied] = useState(false); 17 | 18 | useEffect(() => { 19 | if (copied) { 20 | const timeout = setTimeout(() => setCopied(false), 2000); 21 | return (): void => clearTimeout(timeout); 22 | } 23 | }, [copied]); 24 | 25 | const handleClick = (event: MouseEvent): void => { 26 | navigator.clipboard.writeText(props.text); 27 | setCopied(true); 28 | onClick?.(event); 29 | }; 30 | 31 | if (children) { 32 | return ( 33 | 34 | {typeof children === 'function' ? children(copied) : children} 35 | 36 | ); 37 | } 38 | 39 | const iconClass = cn('w-3 h-3'); 40 | 41 | return ( 42 | 43 | {copied ? : } 44 | 45 | ); 46 | }, 47 | ); 48 | 49 | CopyButton.displayName = 'CopyButton'; 50 | 51 | export {CopyButton}; 52 | -------------------------------------------------------------------------------- /washboard/src/ui/data-table/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ColumnDef, 3 | SortingState, 4 | flexRender, 5 | getCoreRowModel, 6 | getFilteredRowModel, 7 | getPaginationRowModel, 8 | getSortedRowModel, 9 | useReactTable, 10 | } from '@tanstack/react-table'; 11 | import * as React from 'react'; 12 | import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from 'ui/table'; 13 | 14 | interface DataTableProps { 15 | columns: ColumnDef[]; 16 | data: TData[]; 17 | } 18 | export function DataTable({ 19 | data, 20 | columns, 21 | }: DataTableProps): React.ReactElement { 22 | const [sorting, setSorting] = React.useState([]); 23 | 24 | const table = useReactTable({ 25 | data, 26 | columns, 27 | onSortingChange: setSorting, 28 | getCoreRowModel: getCoreRowModel(), 29 | getPaginationRowModel: getPaginationRowModel(), 30 | getSortedRowModel: getSortedRowModel(), 31 | getFilteredRowModel: getFilteredRowModel(), 32 | state: {sorting}, 33 | }); 34 | 35 | return ( 36 |
37 |
38 | 39 | 40 | {table.getHeaderGroups().map((headerGroup) => ( 41 | 42 | {headerGroup.headers.map((header) => { 43 | return ( 44 | 48 | {header.isPlaceholder 49 | ? null 50 | : flexRender(header.column.columnDef.header, header.getContext())} 51 | 52 | ); 53 | })} 54 | 55 | ))} 56 | 57 | 58 | {table.getRowModel().rows?.length ? ( 59 | table.getRowModel().rows.map((row) => ( 60 | 61 | {row.getVisibleCells().map((cell) => ( 62 | 63 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 64 | 65 | ))} 66 | 67 | )) 68 | ) : ( 69 | 70 | 71 | No results. 72 | 73 | 74 | )} 75 | 76 |
77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /washboard/src/ui/form/Form.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label'; 2 | import {Slot} from '@radix-ui/react-slot'; 3 | import * as React from 'react'; 4 | import {Controller, ControllerProps, FieldPath, FieldValues, FormProvider} from 'react-hook-form'; 5 | 6 | import {cn} from 'lib/utils'; 7 | import {Label} from 'ui/label'; 8 | import {FormFieldContext, FormItemContext} from './context'; 9 | import {useFormField} from './use-form-field'; 10 | 11 | const Form = FormProvider; 12 | 13 | const FormField = < 14 | TFieldValues extends FieldValues = FieldValues, 15 | TName extends FieldPath = FieldPath, 16 | >({ 17 | ...props 18 | }: ControllerProps): React.ReactElement => { 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | const FormItem = React.forwardRef>( 27 | ({className, ...props}, ref) => { 28 | const id = React.useId(); 29 | 30 | return ( 31 | 32 |
33 | 34 | ); 35 | }, 36 | ); 37 | FormItem.displayName = 'FormItem'; 38 | 39 | const FormLabel = React.forwardRef< 40 | React.ElementRef, 41 | React.ComponentPropsWithoutRef 42 | >(({className, ...props}, ref) => { 43 | const {error, formItemId} = useFormField(); 44 | 45 | return ( 46 |