mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-05-10 01:43:47 +02:00
Compare commits
5 Commits
yaak-cli-0
...
cli-improv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0702864a11 | ||
|
|
487e66faa4 | ||
|
|
f71a3ea8fe | ||
|
|
39fc9e81cd | ||
|
|
a4f96fca11 |
62
.claude/commands/release/check-out-pr.md
Normal file
62
.claude/commands/release/check-out-pr.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
description: Review a PR in a new worktree
|
||||||
|
allowed-tools: Bash(git worktree:*), Bash(gh pr:*), Bash(git branch:*)
|
||||||
|
---
|
||||||
|
|
||||||
|
Check out a GitHub pull request for review.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/check-out-pr <PR_NUMBER>
|
||||||
|
```
|
||||||
|
|
||||||
|
## What to do
|
||||||
|
|
||||||
|
1. If no PR number is provided, list all open pull requests and ask the user to select one
|
||||||
|
2. Get PR information using `gh pr view <PR_NUMBER> --json number,headRefName`
|
||||||
|
3. **Ask the user** whether they want to:
|
||||||
|
- **A) Check out in current directory** — simple `gh pr checkout <PR_NUMBER>`
|
||||||
|
- **B) Create a new worktree** — isolated copy at `../yaak-worktrees/pr-<PR_NUMBER>`
|
||||||
|
4. Follow the appropriate path below
|
||||||
|
|
||||||
|
## Option A: Check out in current directory
|
||||||
|
|
||||||
|
1. Run `gh pr checkout <PR_NUMBER>`
|
||||||
|
2. Inform the user which branch they're now on
|
||||||
|
|
||||||
|
## Option B: Create a new worktree
|
||||||
|
|
||||||
|
1. Create a new worktree at `../yaak-worktrees/pr-<PR_NUMBER>` using `git worktree add` with a timeout of at least 300000ms (5 minutes) since the post-checkout hook runs a bootstrap script
|
||||||
|
2. Checkout the PR branch in the new worktree using `gh pr checkout <PR_NUMBER>`
|
||||||
|
3. The post-checkout hook will automatically:
|
||||||
|
- Create `.env.local` with unique ports
|
||||||
|
- Copy editor config folders
|
||||||
|
- Run `npm install && npm run bootstrap`
|
||||||
|
4. Inform the user:
|
||||||
|
- Where the worktree was created
|
||||||
|
- What ports were assigned
|
||||||
|
- How to access it (cd command)
|
||||||
|
- How to run the dev server
|
||||||
|
- How to remove the worktree when done
|
||||||
|
|
||||||
|
### Example worktree output
|
||||||
|
|
||||||
|
```
|
||||||
|
Created worktree for PR #123 at ../yaak-worktrees/pr-123
|
||||||
|
Branch: feature-auth
|
||||||
|
Ports: Vite (1421), MCP (64344)
|
||||||
|
|
||||||
|
To start working:
|
||||||
|
cd ../yaak-worktrees/pr-123
|
||||||
|
npm run app-dev
|
||||||
|
|
||||||
|
To remove when done:
|
||||||
|
git worktree remove ../yaak-worktrees/pr-123
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- If the PR doesn't exist, show a helpful error
|
||||||
|
- If the worktree already exists, inform the user and ask if they want to remove and recreate it
|
||||||
|
- If `gh` CLI is not available, inform the user to install it
|
||||||
@@ -37,7 +37,6 @@ The skill generates markdown-formatted release notes following this structure:
|
|||||||
|
|
||||||
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
|
**IMPORTANT**: Always add a blank lines around the markdown code fence and output the markdown code block last
|
||||||
**IMPORTANT**: PRs by `@gschier` should not mention the @username
|
**IMPORTANT**: PRs by `@gschier` should not mention the @username
|
||||||
**IMPORTANT**: These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
|
|
||||||
|
|
||||||
## After Generating Release Notes
|
## After Generating Release Notes
|
||||||
|
|
||||||
|
|||||||
35
.claude/skills/worktree.md
Normal file
35
.claude/skills/worktree.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Worktree Management Skill
|
||||||
|
|
||||||
|
## Creating Worktrees
|
||||||
|
|
||||||
|
When creating git worktrees for this project, ALWAYS use the path format:
|
||||||
|
```
|
||||||
|
../yaak-worktrees/<NAME>
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
- `git worktree add ../yaak-worktrees/feature-auth`
|
||||||
|
- `git worktree add ../yaak-worktrees/bugfix-login`
|
||||||
|
- `git worktree add ../yaak-worktrees/refactor-api`
|
||||||
|
|
||||||
|
## What Happens Automatically
|
||||||
|
|
||||||
|
The post-checkout hook will automatically:
|
||||||
|
1. Create `.env.local` with unique ports (YAAK_DEV_PORT and YAAK_PLUGIN_MCP_SERVER_PORT)
|
||||||
|
2. Copy gitignored editor config folders (.zed, .idea, etc.)
|
||||||
|
3. Run `npm install && npm run bootstrap`
|
||||||
|
|
||||||
|
## Deleting Worktrees
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git worktree remove ../yaak-worktrees/<NAME>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Port Assignments
|
||||||
|
|
||||||
|
- Main worktree: 1420 (Vite), 64343 (MCP)
|
||||||
|
- First worktree: 1421, 64344
|
||||||
|
- Second worktree: 1422, 64345
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
Each worktree can run `npm run app-dev` simultaneously without conflicts.
|
||||||
46
.codex/skills/release-check-out-pr/SKILL.md
Normal file
46
.codex/skills/release-check-out-pr/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
name: release-check-out-pr
|
||||||
|
description: Check out a GitHub pull request for review in this repo, either in the current directory or in a new isolated worktree at ../yaak-worktrees/pr-<PR_NUMBER>. Use when asked to run or replace the old Claude check-out-pr command.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Check Out PR
|
||||||
|
|
||||||
|
Check out a PR by number and let the user choose between current-directory checkout and isolated worktree checkout.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Confirm `gh` CLI is available.
|
||||||
|
2. If no PR number is provided, list open PRs (`gh pr list`) and ask the user to choose one.
|
||||||
|
3. Read PR metadata:
|
||||||
|
- `gh pr view <PR_NUMBER> --json number,headRefName`
|
||||||
|
4. Ask the user to choose:
|
||||||
|
- Option A: check out in the current directory
|
||||||
|
- Option B: create a new worktree at `../yaak-worktrees/pr-<PR_NUMBER>`
|
||||||
|
|
||||||
|
## Option A: Current Directory
|
||||||
|
|
||||||
|
1. Run:
|
||||||
|
- `gh pr checkout <PR_NUMBER>`
|
||||||
|
2. Report the checked-out branch.
|
||||||
|
|
||||||
|
## Option B: New Worktree
|
||||||
|
|
||||||
|
1. Use path:
|
||||||
|
- `../yaak-worktrees/pr-<PR_NUMBER>`
|
||||||
|
2. Create the worktree with a timeout of at least 5 minutes because checkout hooks run bootstrap.
|
||||||
|
3. In the new worktree, run:
|
||||||
|
- `gh pr checkout <PR_NUMBER>`
|
||||||
|
4. Report:
|
||||||
|
- Worktree path
|
||||||
|
- Assigned ports from `.env.local` if present
|
||||||
|
- How to start work:
|
||||||
|
- `cd ../yaak-worktrees/pr-<PR_NUMBER>`
|
||||||
|
- `npm run app-dev`
|
||||||
|
- How to remove when done:
|
||||||
|
- `git worktree remove ../yaak-worktrees/pr-<PR_NUMBER>`
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- If PR does not exist, show a clear error.
|
||||||
|
- If worktree already exists, ask whether to reuse it or remove/recreate it.
|
||||||
|
- If `gh` is missing, instruct the user to install/authenticate it.
|
||||||
@@ -32,7 +32,6 @@ Generate formatted markdown release notes for a Yaak tag.
|
|||||||
- Keep a blank line before and after the code fence.
|
- Keep a blank line before and after the code fence.
|
||||||
- Output the markdown code block last.
|
- Output the markdown code block last.
|
||||||
- Do not append `by @gschier` for PRs authored by `@gschier`.
|
- Do not append `by @gschier` for PRs authored by `@gschier`.
|
||||||
- These are app release notes. Exclude CLI-only changes (commits prefixed with `cli:` or only touching `crates-cli/`) since the CLI has its own release process.
|
|
||||||
|
|
||||||
## Release Creation Prompt
|
## Release Creation Prompt
|
||||||
|
|
||||||
|
|||||||
37
.codex/skills/worktree-management/SKILL.md
Normal file
37
.codex/skills/worktree-management/SKILL.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
name: worktree-management
|
||||||
|
description: Manage Yaak git worktrees using the standard ../yaak-worktrees/<NAME> layout, including creation, removal, and expected automatic setup behavior and port assignments.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Worktree Management
|
||||||
|
|
||||||
|
Use the Yaak-standard worktree path layout and lifecycle commands.
|
||||||
|
|
||||||
|
## Path Convention
|
||||||
|
|
||||||
|
Always create worktrees under:
|
||||||
|
|
||||||
|
`../yaak-worktrees/<NAME>`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `git worktree add ../yaak-worktrees/feature-auth`
|
||||||
|
- `git worktree add ../yaak-worktrees/bugfix-login`
|
||||||
|
- `git worktree add ../yaak-worktrees/refactor-api`
|
||||||
|
|
||||||
|
## Automatic Setup After Checkout
|
||||||
|
|
||||||
|
Project git hooks automatically:
|
||||||
|
1. Create `.env.local` with unique `YAAK_DEV_PORT` and `YAAK_PLUGIN_MCP_SERVER_PORT`
|
||||||
|
2. Copy gitignored editor config folders
|
||||||
|
3. Run `npm install && npm run bootstrap`
|
||||||
|
|
||||||
|
## Remove Worktree
|
||||||
|
|
||||||
|
`git worktree remove ../yaak-worktrees/<NAME>`
|
||||||
|
|
||||||
|
## Port Pattern
|
||||||
|
|
||||||
|
- Main worktree: Vite `1420`, MCP `64343`
|
||||||
|
- First extra worktree: `1421`, `64344`
|
||||||
|
- Second extra worktree: `1422`, `64345`
|
||||||
|
- Continue incrementally for additional worktrees
|
||||||
59
.github/workflows/release-api-npm.yml
vendored
59
.github/workflows/release-api-npm.yml
vendored
@@ -1,59 +0,0 @@
|
|||||||
name: Release API to NPM
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: [yaak-api-*]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: API version to publish (for example 0.9.0 or v0.9.0)
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish-npm:
|
|
||||||
name: Publish @yaakapp/api
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
registry-url: https://registry.npmjs.org
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Set @yaakapp/api version
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
WORKFLOW_VERSION: ${{ inputs.version }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
VERSION="$WORKFLOW_VERSION"
|
|
||||||
else
|
|
||||||
VERSION="${GITHUB_REF_NAME#yaak-api-}"
|
|
||||||
fi
|
|
||||||
VERSION="${VERSION#v}"
|
|
||||||
echo "Preparing @yaakapp/api version: $VERSION"
|
|
||||||
cd packages/plugin-runtime-types
|
|
||||||
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
|
||||||
|
|
||||||
- name: Build @yaakapp/api
|
|
||||||
working-directory: packages/plugin-runtime-types
|
|
||||||
run: npm run build
|
|
||||||
|
|
||||||
- name: Publish @yaakapp/api
|
|
||||||
working-directory: packages/plugin-runtime-types
|
|
||||||
run: npm publish --provenance --access public
|
|
||||||
218
.github/workflows/release-cli-npm.yml
vendored
218
.github/workflows/release-cli-npm.yml
vendored
@@ -1,218 +0,0 @@
|
|||||||
name: Release CLI to NPM
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: [yaak-cli-*]
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: CLI version to publish (for example 0.4.0 or v0.4.0)
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
prepare-vendored-assets:
|
|
||||||
name: Prepare vendored plugin assets
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build plugin assets
|
|
||||||
env:
|
|
||||||
SKIP_WASM_BUILD: "1"
|
|
||||||
run: |
|
|
||||||
npm run build
|
|
||||||
npm run vendor:vendor-plugins
|
|
||||||
|
|
||||||
- name: Upload vendored assets
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: vendored-assets
|
|
||||||
path: |
|
|
||||||
crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs
|
|
||||||
crates-tauri/yaak-app/vendored/plugins
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build-binaries:
|
|
||||||
name: Build ${{ matrix.pkg }}
|
|
||||||
needs: prepare-vendored-assets
|
|
||||||
runs-on: ${{ matrix.runner }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- pkg: cli-darwin-arm64
|
|
||||||
runner: macos-latest
|
|
||||||
target: aarch64-apple-darwin
|
|
||||||
binary: yaak
|
|
||||||
- pkg: cli-darwin-x64
|
|
||||||
runner: macos-latest
|
|
||||||
target: x86_64-apple-darwin
|
|
||||||
binary: yaak
|
|
||||||
- pkg: cli-linux-arm64
|
|
||||||
runner: ubuntu-22.04-arm
|
|
||||||
target: aarch64-unknown-linux-gnu
|
|
||||||
binary: yaak
|
|
||||||
- pkg: cli-linux-x64
|
|
||||||
runner: ubuntu-22.04
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
binary: yaak
|
|
||||||
- pkg: cli-win32-arm64
|
|
||||||
runner: windows-latest
|
|
||||||
target: aarch64-pc-windows-msvc
|
|
||||||
binary: yaak.exe
|
|
||||||
- pkg: cli-win32-x64
|
|
||||||
runner: windows-latest
|
|
||||||
target: x86_64-pc-windows-msvc
|
|
||||||
binary: yaak.exe
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
targets: ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Restore Rust cache
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
shared-key: release-cli-npm
|
|
||||||
cache-on-failure: true
|
|
||||||
|
|
||||||
- name: Install Linux build dependencies
|
|
||||||
if: startsWith(matrix.runner, 'ubuntu')
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y pkg-config libdbus-1-dev
|
|
||||||
|
|
||||||
- name: Download vendored assets
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: vendored-assets
|
|
||||||
path: crates-tauri/yaak-app/vendored
|
|
||||||
|
|
||||||
- name: Set CLI build version
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
WORKFLOW_VERSION: ${{ inputs.version }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
VERSION="$WORKFLOW_VERSION"
|
|
||||||
else
|
|
||||||
VERSION="${GITHUB_REF_NAME#yaak-cli-}"
|
|
||||||
fi
|
|
||||||
VERSION="${VERSION#v}"
|
|
||||||
echo "Building yaak version: $VERSION"
|
|
||||||
echo "YAAK_CLI_VERSION=$VERSION" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Build yaak
|
|
||||||
run: cargo build --locked --release -p yaak-cli --bin yaak --target ${{ matrix.target }}
|
|
||||||
|
|
||||||
- name: Stage binary artifact
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
mkdir -p "npm/dist/${{ matrix.pkg }}"
|
|
||||||
cp "target/${{ matrix.target }}/release/${{ matrix.binary }}" "npm/dist/${{ matrix.pkg }}/${{ matrix.binary }}"
|
|
||||||
|
|
||||||
- name: Upload binary artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ matrix.pkg }}
|
|
||||||
path: npm/dist/${{ matrix.pkg }}/${{ matrix.binary }}
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
publish-npm:
|
|
||||||
name: Publish @yaakapp/cli packages
|
|
||||||
needs: build-binaries
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: lts/*
|
|
||||||
registry-url: https://registry.npmjs.org
|
|
||||||
|
|
||||||
- name: Download binary artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
pattern: cli-*
|
|
||||||
path: npm/dist
|
|
||||||
merge-multiple: false
|
|
||||||
|
|
||||||
- name: Prepare npm packages
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
WORKFLOW_VERSION: ${{ inputs.version }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
VERSION="$WORKFLOW_VERSION"
|
|
||||||
else
|
|
||||||
VERSION="${GITHUB_REF_NAME#yaak-cli-}"
|
|
||||||
fi
|
|
||||||
VERSION="${VERSION#v}"
|
|
||||||
if [[ "$VERSION" == *-* ]]; then
|
|
||||||
PRERELEASE="${VERSION#*-}"
|
|
||||||
NPM_TAG="${PRERELEASE%%.*}"
|
|
||||||
else
|
|
||||||
NPM_TAG="latest"
|
|
||||||
fi
|
|
||||||
echo "Preparing CLI npm packages for version: $VERSION"
|
|
||||||
echo "Publishing with npm dist-tag: $NPM_TAG"
|
|
||||||
echo "NPM_TAG=$NPM_TAG" >> "$GITHUB_ENV"
|
|
||||||
YAAK_CLI_VERSION="$VERSION" node npm/prepare-publish.js
|
|
||||||
|
|
||||||
- name: Publish @yaakapp/cli-darwin-arm64
|
|
||||||
run: npm publish --provenance --access public --tag "$NPM_TAG"
|
|
||||||
working-directory: npm/cli-darwin-arm64
|
|
||||||
|
|
||||||
- name: Publish @yaakapp/cli-darwin-x64
|
|
||||||
run: npm publish --provenance --access public --tag "$NPM_TAG"
|
|
||||||
working-directory: npm/cli-darwin-x64
|
|
||||||
|
|
||||||
- name: Publish @yaakapp/cli-linux-arm64
|
|
||||||
run: npm publish --provenance --access public --tag "$NPM_TAG"
|
|
||||||
working-directory: npm/cli-linux-arm64
|
|
||||||
|
|
||||||
- name: Publish @yaakapp/cli-linux-x64
|
|
||||||
run: npm publish --provenance --access public --tag "$NPM_TAG"
|
|
||||||
working-directory: npm/cli-linux-x64
|
|
||||||
|
|
||||||
- name: Publish @yaakapp/cli-win32-arm64
|
|
||||||
run: npm publish --provenance --access public --tag "$NPM_TAG"
|
|
||||||
working-directory: npm/cli-win32-arm64
|
|
||||||
|
|
||||||
- name: Publish @yaakapp/cli-win32-x64
|
|
||||||
run: npm publish --provenance --access public --tag "$NPM_TAG"
|
|
||||||
working-directory: npm/cli-win32-x64
|
|
||||||
|
|
||||||
- name: Publish @yaakapp/cli
|
|
||||||
run: npm publish --provenance --access public --tag "$NPM_TAG"
|
|
||||||
working-directory: npm/cli
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Release App Artifacts
|
name: Generate Artifacts
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: [v*]
|
tags: [v*]
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
- name: Run JS Tests
|
- name: Run JS Tests
|
||||||
run: npm test
|
run: npm test
|
||||||
- name: Run Rust Tests
|
- name: Run Rust Tests
|
||||||
run: cargo test --all --exclude yaak-cli
|
run: cargo test --all
|
||||||
|
|
||||||
- name: Set version
|
- name: Set version
|
||||||
run: npm run replace-version
|
run: npm run replace-version
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -54,6 +54,3 @@ flatpak/node-sources.json
|
|||||||
|
|
||||||
# Local Codex desktop env state
|
# Local Codex desktop env state
|
||||||
.codex/environments/environment.toml
|
.codex/environments/environment.toml
|
||||||
|
|
||||||
# Claude Code local settings
|
|
||||||
.claude/settings.local.json
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
- Tag safety: app releases use `v*` tags and CLI releases use `yaak-cli-*` tags; always confirm which one is requested before retagging.
|
|
||||||
- Do not commit, push, or tag without explicit approval
|
|
||||||
2168
Cargo.lock
generated
2168
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
|||||||
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
<!-- sponsors-premium --><a href="https://github.com/MVST-Solutions"><img src="https://github.com/MVST-Solutions.png" width="80px" alt="User avatar: MVST-Solutions" /></a> <a href="https://github.com/dharsanb"><img src="https://github.com/dharsanb.png" width="80px" alt="User avatar: dharsanb" /></a> <a href="https://github.com/railwayapp"><img src="https://github.com/railwayapp.png" width="80px" alt="User avatar: railwayapp" /></a> <a href="https://github.com/caseyamcl"><img src="https://github.com/caseyamcl.png" width="80px" alt="User avatar: caseyamcl" /></a> <a href="https://github.com/bytebase"><img src="https://github.com/bytebase.png" width="80px" alt="User avatar: bytebase" /></a> <a href="https://github.com/"><img src="https://raw.githubusercontent.com/JamesIves/github-sponsors-readme-action/dev/.github/assets/placeholder.png" width="80px" alt="User avatar: " /></a> <!-- sponsors-premium -->
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a> <a href="https://github.com/Frostist"><img src="https://github.com/Frostist.png" width="50px" alt="User avatar: Frostist" /></a> <!-- sponsors-base -->
|
<!-- sponsors-base --><a href="https://github.com/seanwash"><img src="https://github.com/seanwash.png" width="50px" alt="User avatar: seanwash" /></a> <a href="https://github.com/jerath"><img src="https://github.com/jerath.png" width="50px" alt="User avatar: jerath" /></a> <a href="https://github.com/itsa-sh"><img src="https://github.com/itsa-sh.png" width="50px" alt="User avatar: itsa-sh" /></a> <a href="https://github.com/dmmulroy"><img src="https://github.com/dmmulroy.png" width="50px" alt="User avatar: dmmulroy" /></a> <a href="https://github.com/timcole"><img src="https://github.com/timcole.png" width="50px" alt="User avatar: timcole" /></a> <a href="https://github.com/VLZH"><img src="https://github.com/VLZH.png" width="50px" alt="User avatar: VLZH" /></a> <a href="https://github.com/terasaka2k"><img src="https://github.com/terasaka2k.png" width="50px" alt="User avatar: terasaka2k" /></a> <a href="https://github.com/andriyor"><img src="https://github.com/andriyor.png" width="50px" alt="User avatar: andriyor" /></a> <a href="https://github.com/majudhu"><img src="https://github.com/majudhu.png" width="50px" alt="User avatar: majudhu" /></a> <a href="https://github.com/axelrindle"><img src="https://github.com/axelrindle.png" width="50px" alt="User avatar: axelrindle" /></a> <a href="https://github.com/jirizverina"><img src="https://github.com/jirizverina.png" width="50px" alt="User avatar: jirizverina" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="50px" alt="User avatar: chip-well" /></a> <a href="https://github.com/GRAYAH"><img src="https://github.com/GRAYAH.png" width="50px" alt="User avatar: GRAYAH" /></a> <a href="https://github.com/flashblaze"><img src="https://github.com/flashblaze.png" width="50px" alt="User avatar: flashblaze" /></a> <!-- sponsors-base -->
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||||
"linter": {
|
"linter": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -48,8 +48,7 @@
|
|||||||
"!src-web/routeTree.gen.ts",
|
"!src-web/routeTree.gen.ts",
|
||||||
"!packages/plugin-runtime-types/lib",
|
"!packages/plugin-runtime-types/lib",
|
||||||
"!**/bindings",
|
"!**/bindings",
|
||||||
"!flatpak",
|
"!flatpak"
|
||||||
"!npm"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,36 +5,20 @@ edition = "2024"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "yaak"
|
name = "yaakcli"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
arboard = "3"
|
|
||||||
base64 = "0.22"
|
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
console = "0.15"
|
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
inquire = { version = "0.7", features = ["editor"] }
|
|
||||||
hex = { workspace = true }
|
|
||||||
include_dir = "0.7"
|
|
||||||
keyring = { workspace = true, features = ["apple-native", "windows-native", "sync-secret-service"] }
|
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
rand = "0.8"
|
|
||||||
reqwest = { workspace = true }
|
|
||||||
rolldown = "0.1.0"
|
|
||||||
oxc_resolver = "=11.10.0"
|
|
||||||
schemars = { workspace = true }
|
schemars = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
sha2 = { workspace = true }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "io-util", "net", "signal", "time"] }
|
|
||||||
walkdir = "2"
|
|
||||||
webbrowser = "1"
|
|
||||||
zip = "4"
|
|
||||||
yaak = { workspace = true }
|
yaak = { workspace = true }
|
||||||
yaak-api = { workspace = true }
|
|
||||||
yaak-crypto = { workspace = true }
|
yaak-crypto = { workspace = true }
|
||||||
yaak-http = { workspace = true }
|
yaak-http = { workspace = true }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
|
|||||||
@@ -1,66 +1,87 @@
|
|||||||
# Yaak CLI
|
# yaak-cli
|
||||||
|
|
||||||
The `yaak` CLI for publishing plugins and creating/updating/sending requests.
|
Command-line interface for Yaak.
|
||||||
|
|
||||||
## Installation
|
## Command Overview
|
||||||
|
|
||||||
```sh
|
Current top-level commands:
|
||||||
npm install @yaakapp/cli
|
|
||||||
```
|
|
||||||
|
|
||||||
## Agentic Workflows
|
|
||||||
|
|
||||||
The `yaak` CLI is primarily meant to be used by AI agents, and has the following features:
|
|
||||||
|
|
||||||
- `schema` subcommands to get the JSON Schema for any model (eg. `yaak request schema http`)
|
|
||||||
- `--json '{...}'` input format to create and update data
|
|
||||||
- `--verbose` mode for extracting debug info while sending requests
|
|
||||||
- The ability to send entire workspaces and folders (Supports `--parallel` and `--fail-fast`)
|
|
||||||
|
|
||||||
### Example Prompts
|
|
||||||
|
|
||||||
Use the `yaak` CLI with agents like Claude or Codex to do useful things for you.
|
|
||||||
|
|
||||||
Here are some example prompts:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Scan my API routes and create a workspace (using yaak cli) with
|
yaakcli send <request_id>
|
||||||
all the requests needed for me to do manual testing?
|
yaakcli workspace list
|
||||||
|
yaakcli workspace show <workspace_id>
|
||||||
|
yaakcli workspace create --name <name>
|
||||||
|
yaakcli workspace create --json '{"name":"My Workspace"}'
|
||||||
|
yaakcli workspace create '{"name":"My Workspace"}'
|
||||||
|
yaakcli workspace update --json '{"id":"wk_abc","description":"Updated"}'
|
||||||
|
yaakcli workspace delete <workspace_id> [--yes]
|
||||||
|
yaakcli request list <workspace_id>
|
||||||
|
yaakcli request show <request_id>
|
||||||
|
yaakcli request send <request_id>
|
||||||
|
yaakcli request create <workspace_id> --name <name> --url <url> [--method GET]
|
||||||
|
yaakcli request create --json '{"workspaceId":"wk_abc","name":"Users","url":"https://api.example.com/users"}'
|
||||||
|
yaakcli request create '{"workspaceId":"wk_abc","name":"Users","url":"https://api.example.com/users"}'
|
||||||
|
yaakcli request update --json '{"id":"rq_abc","name":"Users v2"}'
|
||||||
|
yaakcli request delete <request_id> [--yes]
|
||||||
|
yaakcli folder list <workspace_id>
|
||||||
|
yaakcli folder show <folder_id>
|
||||||
|
yaakcli folder create <workspace_id> --name <name>
|
||||||
|
yaakcli folder create --json '{"workspaceId":"wk_abc","name":"Auth"}'
|
||||||
|
yaakcli folder create '{"workspaceId":"wk_abc","name":"Auth"}'
|
||||||
|
yaakcli folder update --json '{"id":"fl_abc","name":"Auth v2"}'
|
||||||
|
yaakcli folder delete <folder_id> [--yes]
|
||||||
|
yaakcli environment list <workspace_id>
|
||||||
|
yaakcli environment show <environment_id>
|
||||||
|
yaakcli environment create <workspace_id> --name <name>
|
||||||
|
yaakcli environment create --json '{"workspaceId":"wk_abc","name":"Production"}'
|
||||||
|
yaakcli environment create '{"workspaceId":"wk_abc","name":"Production"}'
|
||||||
|
yaakcli environment update --json '{"id":"ev_abc","color":"#00ff00"}'
|
||||||
|
yaakcli environment delete <environment_id> [--yes]
|
||||||
```
|
```
|
||||||
|
|
||||||
```text
|
Global options:
|
||||||
Send all the GraphQL requests in my workspace
|
|
||||||
|
- `--data-dir <path>`: use a custom data directory
|
||||||
|
- `-e, --environment <id>`: environment to use during request rendering/sending
|
||||||
|
- `-v, --verbose`: verbose logging and send output
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `send` is currently a shortcut for sending an HTTP request ID.
|
||||||
|
- `delete` commands prompt for confirmation unless `--yes` is provided.
|
||||||
|
- In non-interactive mode, `delete` commands require `--yes`.
|
||||||
|
- `create` and `update` commands support `--json` and positional JSON shorthand.
|
||||||
|
- `update` uses JSON Merge Patch semantics (RFC 7386) for partial updates.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yaakcli workspace list
|
||||||
|
yaakcli workspace create --name "My Workspace"
|
||||||
|
yaakcli workspace show wk_abc
|
||||||
|
yaakcli workspace update --json '{"id":"wk_abc","description":"Team workspace"}'
|
||||||
|
yaakcli request list wk_abc
|
||||||
|
yaakcli request show rq_abc
|
||||||
|
yaakcli request create wk_abc --name "Users" --url "https://api.example.com/users"
|
||||||
|
yaakcli request update --json '{"id":"rq_abc","name":"Users v2"}'
|
||||||
|
yaakcli request send rq_abc -e ev_abc
|
||||||
|
yaakcli request delete rq_abc --yes
|
||||||
|
yaakcli folder create wk_abc --name "Auth"
|
||||||
|
yaakcli folder update --json '{"id":"fl_abc","name":"Auth v2"}'
|
||||||
|
yaakcli environment create wk_abc --name "Production"
|
||||||
|
yaakcli environment update --json '{"id":"ev_abc","color":"#00ff00"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Description
|
## Roadmap
|
||||||
|
|
||||||
Here's the current print of `yaak --help`
|
Planned command expansion (request schema and polymorphic send) is tracked in `PLAN.md`.
|
||||||
|
|
||||||
```text
|
When command behavior changes, update this README and verify with:
|
||||||
Yaak CLI - API client from the command line
|
|
||||||
|
|
||||||
Usage: yaak [OPTIONS] <COMMAND>
|
```bash
|
||||||
|
cargo run -q -p yaak-cli -- --help
|
||||||
Commands:
|
cargo run -q -p yaak-cli -- request --help
|
||||||
auth Authentication commands
|
cargo run -q -p yaak-cli -- workspace --help
|
||||||
plugin Plugin development and publishing commands
|
cargo run -q -p yaak-cli -- folder --help
|
||||||
send Send a request, folder, or workspace by ID
|
cargo run -q -p yaak-cli -- environment --help
|
||||||
workspace Workspace commands
|
|
||||||
request Request commands
|
|
||||||
folder Folder commands
|
|
||||||
environment Environment commands
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--data-dir <DATA_DIR> Use a custom data directory
|
|
||||||
-e, --environment <ENVIRONMENT> Environment ID to use for variable substitution
|
|
||||||
-v, --verbose Enable verbose send output (events and streamed response body)
|
|
||||||
--log [<LEVEL>] Enable CLI logging; optionally set level (error|warn|info|debug|trace) [possible values: error, warn, info, debug, trace]
|
|
||||||
-h, --help Print help
|
|
||||||
-V, --version Print version
|
|
||||||
|
|
||||||
Agent Hints:
|
|
||||||
- Template variable syntax is ${[ my_var ]}, not {{ ... }}
|
|
||||||
- Template function syntax is ${[ namespace.my_func(a='aaa',b='bbb') ]}
|
|
||||||
- View JSONSchema for models before creating or updating (eg. `yaak request schema http`)
|
|
||||||
- Deletion requires confirmation (--yes for non-interactive environments)
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,16 +2,8 @@ use clap::{Args, Parser, Subcommand, ValueEnum};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "yaak")]
|
#[command(name = "yaakcli")]
|
||||||
#[command(about = "Yaak CLI - API client from the command line")]
|
#[command(about = "Yaak CLI - API client from the command line")]
|
||||||
#[command(version = crate::version::cli_version())]
|
|
||||||
#[command(disable_help_subcommand = true)]
|
|
||||||
#[command(after_help = r#"Agent Hints:
|
|
||||||
- Template variable syntax is ${[ my_var ]}, not {{ ... }}
|
|
||||||
- Template function syntax is ${[ namespace.my_func(a='aaa',b='bbb') ]}
|
|
||||||
- View JSONSchema for models before creating or updating (eg. `yaak request schema http`)
|
|
||||||
- Deletion requires confirmation (--yes for non-interactive environments)
|
|
||||||
"#)]
|
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Use a custom data directory
|
/// Use a custom data directory
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
@@ -21,50 +13,19 @@ pub struct Cli {
|
|||||||
#[arg(long, short, global = true)]
|
#[arg(long, short, global = true)]
|
||||||
pub environment: Option<String>,
|
pub environment: Option<String>,
|
||||||
|
|
||||||
/// Cookie jar ID to use when sending requests
|
/// Enable verbose logging
|
||||||
#[arg(long = "cookie-jar", global = true, value_name = "COOKIE_JAR_ID")]
|
|
||||||
pub cookie_jar: Option<String>,
|
|
||||||
|
|
||||||
/// Enable verbose send output (events and streamed response body)
|
|
||||||
#[arg(long, short, global = true)]
|
#[arg(long, short, global = true)]
|
||||||
pub verbose: bool,
|
pub verbose: bool,
|
||||||
|
|
||||||
/// Enable CLI logging; optionally set level (error|warn|info|debug|trace)
|
|
||||||
#[arg(long, global = true, value_name = "LEVEL", num_args = 0..=1, ignore_case = true)]
|
|
||||||
pub log: Option<Option<LogLevel>>,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Authentication commands
|
|
||||||
Auth(AuthArgs),
|
|
||||||
|
|
||||||
/// Plugin development and publishing commands
|
|
||||||
Plugin(PluginArgs),
|
|
||||||
|
|
||||||
#[command(hide = true)]
|
|
||||||
Build(PluginPathArg),
|
|
||||||
|
|
||||||
#[command(hide = true)]
|
|
||||||
Dev(PluginPathArg),
|
|
||||||
|
|
||||||
/// Backward-compatible alias for `plugin generate`
|
|
||||||
#[command(hide = true)]
|
|
||||||
Generate(GenerateArgs),
|
|
||||||
|
|
||||||
/// Backward-compatible alias for `plugin publish`
|
|
||||||
#[command(hide = true)]
|
|
||||||
Publish(PluginPathArg),
|
|
||||||
|
|
||||||
/// Send a request, folder, or workspace by ID
|
/// Send a request, folder, or workspace by ID
|
||||||
Send(SendArgs),
|
Send(SendArgs),
|
||||||
|
|
||||||
/// Cookie jar commands
|
|
||||||
CookieJar(CookieJarArgs),
|
|
||||||
|
|
||||||
/// Workspace commands
|
/// Workspace commands
|
||||||
Workspace(WorkspaceArgs),
|
Workspace(WorkspaceArgs),
|
||||||
|
|
||||||
@@ -83,8 +44,12 @@ pub struct SendArgs {
|
|||||||
/// Request, folder, or workspace ID
|
/// Request, folder, or workspace ID
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|
||||||
|
/// Execute requests sequentially (default)
|
||||||
|
#[arg(long, conflicts_with = "parallel")]
|
||||||
|
pub sequential: bool,
|
||||||
|
|
||||||
/// Execute requests in parallel
|
/// Execute requests in parallel
|
||||||
#[arg(long)]
|
#[arg(long, conflicts_with = "sequential")]
|
||||||
pub parallel: bool,
|
pub parallel: bool,
|
||||||
|
|
||||||
/// Stop on first request failure when sending folders/workspaces
|
/// Stop on first request failure when sending folders/workspaces
|
||||||
@@ -93,23 +58,6 @@ pub struct SendArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[command(disable_help_subcommand = true)]
|
|
||||||
pub struct CookieJarArgs {
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: CookieJarCommands,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum CookieJarCommands {
|
|
||||||
/// List cookie jars in a workspace
|
|
||||||
List {
|
|
||||||
/// Workspace ID (optional when exactly one workspace exists)
|
|
||||||
workspace_id: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
#[command(disable_help_subcommand = true)]
|
|
||||||
pub struct WorkspaceArgs {
|
pub struct WorkspaceArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: WorkspaceCommands,
|
pub command: WorkspaceCommands,
|
||||||
@@ -120,13 +68,6 @@ pub enum WorkspaceCommands {
|
|||||||
/// List all workspaces
|
/// List all workspaces
|
||||||
List,
|
List,
|
||||||
|
|
||||||
/// Output JSON schema for workspace create/update payloads
|
|
||||||
Schema {
|
|
||||||
/// Pretty-print schema JSON output
|
|
||||||
#[arg(long)]
|
|
||||||
pretty: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Show a workspace as JSON
|
/// Show a workspace as JSON
|
||||||
Show {
|
Show {
|
||||||
/// Workspace ID
|
/// Workspace ID
|
||||||
@@ -171,7 +112,6 @@ pub enum WorkspaceCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[command(disable_help_subcommand = true)]
|
|
||||||
pub struct RequestArgs {
|
pub struct RequestArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: RequestCommands,
|
pub command: RequestCommands,
|
||||||
@@ -181,8 +121,8 @@ pub struct RequestArgs {
|
|||||||
pub enum RequestCommands {
|
pub enum RequestCommands {
|
||||||
/// List requests in a workspace
|
/// List requests in a workspace
|
||||||
List {
|
List {
|
||||||
/// Workspace ID (optional when exactly one workspace exists)
|
/// Workspace ID
|
||||||
workspace_id: Option<String>,
|
workspace_id: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Show a request as JSON
|
/// Show a request as JSON
|
||||||
@@ -201,10 +141,6 @@ pub enum RequestCommands {
|
|||||||
Schema {
|
Schema {
|
||||||
#[arg(value_enum)]
|
#[arg(value_enum)]
|
||||||
request_type: RequestSchemaType,
|
request_type: RequestSchemaType,
|
||||||
|
|
||||||
/// Pretty-print schema JSON output
|
|
||||||
#[arg(long)]
|
|
||||||
pretty: bool,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Create a new HTTP request
|
/// Create a new HTTP request
|
||||||
@@ -258,29 +194,7 @@ pub enum RequestSchemaType {
|
|||||||
Websocket,
|
Websocket,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
|
||||||
pub enum LogLevel {
|
|
||||||
Error,
|
|
||||||
Warn,
|
|
||||||
Info,
|
|
||||||
Debug,
|
|
||||||
Trace,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogLevel {
|
|
||||||
pub fn as_filter(self) -> log::LevelFilter {
|
|
||||||
match self {
|
|
||||||
LogLevel::Error => log::LevelFilter::Error,
|
|
||||||
LogLevel::Warn => log::LevelFilter::Warn,
|
|
||||||
LogLevel::Info => log::LevelFilter::Info,
|
|
||||||
LogLevel::Debug => log::LevelFilter::Debug,
|
|
||||||
LogLevel::Trace => log::LevelFilter::Trace,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[command(disable_help_subcommand = true)]
|
|
||||||
pub struct FolderArgs {
|
pub struct FolderArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: FolderCommands,
|
pub command: FolderCommands,
|
||||||
@@ -290,8 +204,8 @@ pub struct FolderArgs {
|
|||||||
pub enum FolderCommands {
|
pub enum FolderCommands {
|
||||||
/// List folders in a workspace
|
/// List folders in a workspace
|
||||||
List {
|
List {
|
||||||
/// Workspace ID (optional when exactly one workspace exists)
|
/// Workspace ID
|
||||||
workspace_id: Option<String>,
|
workspace_id: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Show a folder as JSON
|
/// Show a folder as JSON
|
||||||
@@ -337,7 +251,6 @@ pub enum FolderCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[command(disable_help_subcommand = true)]
|
|
||||||
pub struct EnvironmentArgs {
|
pub struct EnvironmentArgs {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: EnvironmentCommands,
|
pub command: EnvironmentCommands,
|
||||||
@@ -347,15 +260,8 @@ pub struct EnvironmentArgs {
|
|||||||
pub enum EnvironmentCommands {
|
pub enum EnvironmentCommands {
|
||||||
/// List environments in a workspace
|
/// List environments in a workspace
|
||||||
List {
|
List {
|
||||||
/// Workspace ID (optional when exactly one workspace exists)
|
/// Workspace ID
|
||||||
workspace_id: Option<String>,
|
workspace_id: String,
|
||||||
},
|
|
||||||
|
|
||||||
/// Output JSON schema for environment create/update payloads
|
|
||||||
Schema {
|
|
||||||
/// Pretty-print schema JSON output
|
|
||||||
#[arg(long)]
|
|
||||||
pretty: bool,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Show an environment as JSON
|
/// Show an environment as JSON
|
||||||
@@ -365,22 +271,15 @@ pub enum EnvironmentCommands {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Create an environment
|
/// Create an environment
|
||||||
#[command(after_help = r#"Modes (choose one):
|
|
||||||
1) yaak environment create <workspace_id> --name <name>
|
|
||||||
2) yaak environment create --json '{"workspaceId":"wk_abc","name":"Production"}'
|
|
||||||
3) yaak environment create '{"workspaceId":"wk_abc","name":"Production"}'
|
|
||||||
4) yaak environment create <workspace_id> --json '{"name":"Production"}'
|
|
||||||
"#)]
|
|
||||||
Create {
|
Create {
|
||||||
/// Workspace ID for flag-based mode, or positional JSON payload shorthand
|
/// Workspace ID (or positional JSON payload shorthand)
|
||||||
#[arg(value_name = "WORKSPACE_ID_OR_JSON")]
|
|
||||||
workspace_id: Option<String>,
|
workspace_id: Option<String>,
|
||||||
|
|
||||||
/// Environment name
|
/// Environment name
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
|
||||||
/// JSON payload (use instead of WORKSPACE_ID/--name)
|
/// JSON payload
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: Option<String>,
|
json: Option<String>,
|
||||||
},
|
},
|
||||||
@@ -406,70 +305,3 @@ pub enum EnvironmentCommands {
|
|||||||
yes: bool,
|
yes: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
#[command(disable_help_subcommand = true)]
|
|
||||||
pub struct AuthArgs {
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: AuthCommands,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum AuthCommands {
|
|
||||||
/// Login to Yaak via web browser
|
|
||||||
Login,
|
|
||||||
|
|
||||||
/// Sign out of the Yaak CLI
|
|
||||||
Logout,
|
|
||||||
|
|
||||||
/// Print the current logged-in user's info
|
|
||||||
Whoami,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args)]
|
|
||||||
#[command(disable_help_subcommand = true)]
|
|
||||||
pub struct PluginArgs {
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: PluginCommands,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
pub enum PluginCommands {
|
|
||||||
/// Transpile code into a runnable plugin bundle
|
|
||||||
Build(PluginPathArg),
|
|
||||||
|
|
||||||
/// Build plugin bundle continuously when the filesystem changes
|
|
||||||
Dev(PluginPathArg),
|
|
||||||
|
|
||||||
/// Generate a "Hello World" Yaak plugin
|
|
||||||
Generate(GenerateArgs),
|
|
||||||
|
|
||||||
/// Install a plugin from a local directory or from the registry
|
|
||||||
Install(InstallPluginArgs),
|
|
||||||
|
|
||||||
/// Publish a Yaak plugin version to the plugin registry
|
|
||||||
Publish(PluginPathArg),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args, Clone)]
|
|
||||||
pub struct PluginPathArg {
|
|
||||||
/// Path to plugin directory (defaults to current working directory)
|
|
||||||
pub path: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args, Clone)]
|
|
||||||
pub struct GenerateArgs {
|
|
||||||
/// Plugin name (defaults to a generated name in interactive mode)
|
|
||||||
#[arg(long)]
|
|
||||||
pub name: Option<String>,
|
|
||||||
|
|
||||||
/// Output directory for the generated plugin (defaults to ./<name> in interactive mode)
|
|
||||||
#[arg(long)]
|
|
||||||
pub dir: Option<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args, Clone)]
|
|
||||||
pub struct InstallPluginArgs {
|
|
||||||
/// Local plugin directory path, or registry plugin spec (@org/plugin[@version])
|
|
||||||
pub source: String,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,528 +0,0 @@
|
|||||||
use crate::cli::{AuthArgs, AuthCommands};
|
|
||||||
use crate::ui;
|
|
||||||
use crate::utils::http;
|
|
||||||
use base64::Engine as _;
|
|
||||||
use keyring::Entry;
|
|
||||||
use rand::RngCore;
|
|
||||||
use rand::rngs::OsRng;
|
|
||||||
use reqwest::Url;
|
|
||||||
use serde_json::Value;
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::io::{self, IsTerminal, Write};
|
|
||||||
use std::time::Duration;
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
|
||||||
|
|
||||||
const OAUTH_CLIENT_ID: &str = "a1fe44800c2d7e803cad1b4bf07a291c";
|
|
||||||
const KEYRING_USER: &str = "yaak";
|
|
||||||
const AUTH_TIMEOUT: Duration = Duration::from_secs(300);
|
|
||||||
const MAX_REQUEST_BYTES: usize = 16 * 1024;
|
|
||||||
|
|
||||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
enum Environment {
|
|
||||||
Production,
|
|
||||||
Staging,
|
|
||||||
Development,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Environment {
|
|
||||||
fn app_base_url(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Environment::Production => "https://yaak.app",
|
|
||||||
Environment::Staging => "https://todo.yaak.app",
|
|
||||||
Environment::Development => "http://localhost:9444",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn api_base_url(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Environment::Production => "https://api.yaak.app",
|
|
||||||
Environment::Staging => "https://todo.yaak.app",
|
|
||||||
Environment::Development => "http://localhost:9444",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keyring_service(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Environment::Production => "app.yaak.cli.Token",
|
|
||||||
Environment::Staging => "app.yaak.cli.staging.Token",
|
|
||||||
Environment::Development => "app.yaak.cli.dev.Token",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OAuthFlow {
|
|
||||||
app_base_url: String,
|
|
||||||
auth_url: Url,
|
|
||||||
token_url: String,
|
|
||||||
redirect_url: String,
|
|
||||||
state: String,
|
|
||||||
code_verifier: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run(args: AuthArgs) -> i32 {
|
|
||||||
let result = match args.command {
|
|
||||||
AuthCommands::Login => login().await,
|
|
||||||
AuthCommands::Logout => logout(),
|
|
||||||
AuthCommands::Whoami => whoami().await,
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(()) => 0,
|
|
||||||
Err(error) => {
|
|
||||||
ui::error(&error);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn login() -> CommandResult {
|
|
||||||
let environment = current_environment();
|
|
||||||
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0")
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to start OAuth callback server: {e}"))?;
|
|
||||||
let port = listener
|
|
||||||
.local_addr()
|
|
||||||
.map_err(|e| format!("Failed to determine callback server port: {e}"))?
|
|
||||||
.port();
|
|
||||||
|
|
||||||
let oauth = build_oauth_flow(environment, port)?;
|
|
||||||
|
|
||||||
ui::info(&format!("Initiating login to {}", oauth.auth_url));
|
|
||||||
if !confirm_open_browser()? {
|
|
||||||
ui::info("Login canceled");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = webbrowser::open(oauth.auth_url.as_ref()) {
|
|
||||||
ui::warning(&format!("Failed to open browser: {err}"));
|
|
||||||
ui::info(&format!("Open this URL manually:\n{}", oauth.auth_url));
|
|
||||||
}
|
|
||||||
ui::info("Waiting for authentication...");
|
|
||||||
|
|
||||||
let code = tokio::select! {
|
|
||||||
result = receive_oauth_code(listener, &oauth.state, &oauth.app_base_url) => result?,
|
|
||||||
_ = tokio::signal::ctrl_c() => {
|
|
||||||
return Err("Interrupted by user".to_string());
|
|
||||||
}
|
|
||||||
_ = tokio::time::sleep(AUTH_TIMEOUT) => {
|
|
||||||
return Err("Timeout waiting for authentication".to_string());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let token = exchange_access_token(&oauth, &code).await?;
|
|
||||||
store_auth_token(environment, &token)?;
|
|
||||||
ui::success("Authentication successful!");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn logout() -> CommandResult {
|
|
||||||
delete_auth_token(current_environment())?;
|
|
||||||
ui::success("Signed out of Yaak");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn whoami() -> CommandResult {
|
|
||||||
let environment = current_environment();
|
|
||||||
let token = match get_auth_token(environment)? {
|
|
||||||
Some(token) => token,
|
|
||||||
None => {
|
|
||||||
ui::warning("Not logged in");
|
|
||||||
ui::info("Please run `yaak auth login`");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = format!("{}/api/v1/whoami", environment.api_base_url());
|
|
||||||
let response = http::build_client(Some(&token))?
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to call whoami endpoint: {e}"))?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
let body =
|
|
||||||
response.text().await.map_err(|e| format!("Failed to read whoami response body: {e}"))?;
|
|
||||||
|
|
||||||
if !status.is_success() {
|
|
||||||
if status.as_u16() == 401 {
|
|
||||||
let _ = delete_auth_token(environment);
|
|
||||||
return Err(
|
|
||||||
"Unauthorized to access CLI. Run `yaak auth login` to refresh credentials."
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Err(http::parse_api_error(status.as_u16(), &body));
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{body}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_environment() -> Environment {
|
|
||||||
let value = std::env::var("ENVIRONMENT").ok();
|
|
||||||
parse_environment(value.as_deref())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_environment(value: Option<&str>) -> Environment {
|
|
||||||
match value {
|
|
||||||
Some("staging") => Environment::Staging,
|
|
||||||
Some("development") => Environment::Development,
|
|
||||||
_ => Environment::Production,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_oauth_flow(environment: Environment, callback_port: u16) -> CommandResult<OAuthFlow> {
|
|
||||||
let code_verifier = random_hex(32);
|
|
||||||
let state = random_hex(24);
|
|
||||||
let redirect_url = format!("http://127.0.0.1:{callback_port}/oauth/callback");
|
|
||||||
|
|
||||||
let code_challenge = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
||||||
.encode(Sha256::digest(code_verifier.as_bytes()));
|
|
||||||
|
|
||||||
let mut auth_url = Url::parse(&format!("{}/login/oauth/authorize", environment.app_base_url()))
|
|
||||||
.map_err(|e| format!("Failed to build OAuth authorize URL: {e}"))?;
|
|
||||||
auth_url
|
|
||||||
.query_pairs_mut()
|
|
||||||
.append_pair("response_type", "code")
|
|
||||||
.append_pair("client_id", OAUTH_CLIENT_ID)
|
|
||||||
.append_pair("redirect_uri", &redirect_url)
|
|
||||||
.append_pair("state", &state)
|
|
||||||
.append_pair("code_challenge_method", "S256")
|
|
||||||
.append_pair("code_challenge", &code_challenge);
|
|
||||||
|
|
||||||
Ok(OAuthFlow {
|
|
||||||
app_base_url: environment.app_base_url().to_string(),
|
|
||||||
auth_url,
|
|
||||||
token_url: format!("{}/login/oauth/access_token", environment.app_base_url()),
|
|
||||||
redirect_url,
|
|
||||||
state,
|
|
||||||
code_verifier,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn receive_oauth_code(
|
|
||||||
listener: TcpListener,
|
|
||||||
expected_state: &str,
|
|
||||||
app_base_url: &str,
|
|
||||||
) -> CommandResult<String> {
|
|
||||||
loop {
|
|
||||||
let (mut stream, _) = listener
|
|
||||||
.accept()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("OAuth callback server accept error: {e}"))?;
|
|
||||||
|
|
||||||
match parse_callback_request(&mut stream).await {
|
|
||||||
Ok((state, code)) => {
|
|
||||||
if state != expected_state {
|
|
||||||
let _ = write_bad_request(&mut stream, "Invalid OAuth state").await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let success_redirect = format!("{app_base_url}/login/oauth/success");
|
|
||||||
write_redirect(&mut stream, &success_redirect)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed responding to OAuth callback: {e}"))?;
|
|
||||||
return Ok(code);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
let _ = write_bad_request(&mut stream, &error).await;
|
|
||||||
if error.starts_with("OAuth provider returned error:") {
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn parse_callback_request(stream: &mut TcpStream) -> CommandResult<(String, String)> {
|
|
||||||
let target = read_http_target(stream).await?;
|
|
||||||
if !target.starts_with("/oauth/callback") {
|
|
||||||
return Err("Expected /oauth/callback path".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = Url::parse(&format!("http://127.0.0.1{target}"))
|
|
||||||
.map_err(|e| format!("Failed to parse callback URL: {e}"))?;
|
|
||||||
let mut state: Option<String> = None;
|
|
||||||
let mut code: Option<String> = None;
|
|
||||||
let mut oauth_error: Option<String> = None;
|
|
||||||
let mut oauth_error_description: Option<String> = None;
|
|
||||||
|
|
||||||
for (k, v) in url.query_pairs() {
|
|
||||||
if k == "state" {
|
|
||||||
state = Some(v.into_owned());
|
|
||||||
} else if k == "code" {
|
|
||||||
code = Some(v.into_owned());
|
|
||||||
} else if k == "error" {
|
|
||||||
oauth_error = Some(v.into_owned());
|
|
||||||
} else if k == "error_description" {
|
|
||||||
oauth_error_description = Some(v.into_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(error) = oauth_error {
|
|
||||||
let mut message = format!("OAuth provider returned error: {error}");
|
|
||||||
if let Some(description) = oauth_error_description.filter(|d| !d.is_empty()) {
|
|
||||||
message.push_str(&format!(" ({description})"));
|
|
||||||
}
|
|
||||||
return Err(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = state.ok_or_else(|| "Missing 'state' query parameter".to_string())?;
|
|
||||||
let code = code.ok_or_else(|| "Missing 'code' query parameter".to_string())?;
|
|
||||||
|
|
||||||
if code.is_empty() {
|
|
||||||
return Err("Missing 'code' query parameter".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((state, code))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_http_target(stream: &mut TcpStream) -> CommandResult<String> {
|
|
||||||
let mut buf = vec![0_u8; MAX_REQUEST_BYTES];
|
|
||||||
let mut total_read = 0_usize;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let n = stream
|
|
||||||
.read(&mut buf[total_read..])
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed reading callback request: {e}"))?;
|
|
||||||
if n == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
total_read += n;
|
|
||||||
|
|
||||||
if buf[..total_read].windows(4).any(|w| w == b"\r\n\r\n") {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if total_read == MAX_REQUEST_BYTES {
|
|
||||||
return Err("OAuth callback request too large".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let req = String::from_utf8_lossy(&buf[..total_read]);
|
|
||||||
let request_line =
|
|
||||||
req.lines().next().ok_or_else(|| "Invalid callback request line".to_string())?;
|
|
||||||
let mut parts = request_line.split_whitespace();
|
|
||||||
let method = parts.next().unwrap_or_default();
|
|
||||||
let target = parts.next().unwrap_or_default();
|
|
||||||
|
|
||||||
if method != "GET" {
|
|
||||||
return Err(format!("Expected GET callback request, got '{method}'"));
|
|
||||||
}
|
|
||||||
if target.is_empty() {
|
|
||||||
return Err("Missing callback request target".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(target.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_bad_request(stream: &mut TcpStream, message: &str) -> std::io::Result<()> {
|
|
||||||
let body = format!("Failed to authenticate: {message}");
|
|
||||||
let response = format!(
|
|
||||||
"HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
|
||||||
body.len(),
|
|
||||||
body
|
|
||||||
);
|
|
||||||
stream.write_all(response.as_bytes()).await?;
|
|
||||||
stream.shutdown().await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_redirect(stream: &mut TcpStream, location: &str) -> std::io::Result<()> {
|
|
||||||
let response = format!(
|
|
||||||
"HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
|
||||||
);
|
|
||||||
stream.write_all(response.as_bytes()).await?;
|
|
||||||
stream.shutdown().await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn exchange_access_token(oauth: &OAuthFlow, code: &str) -> CommandResult<String> {
|
|
||||||
let response = http::build_client(None)?
|
|
||||||
.post(&oauth.token_url)
|
|
||||||
.form(&[
|
|
||||||
("grant_type", "authorization_code"),
|
|
||||||
("client_id", OAUTH_CLIENT_ID),
|
|
||||||
("code", code),
|
|
||||||
("redirect_uri", oauth.redirect_url.as_str()),
|
|
||||||
("code_verifier", oauth.code_verifier.as_str()),
|
|
||||||
])
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to exchange OAuth code for access token: {e}"))?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
let body =
|
|
||||||
response.text().await.map_err(|e| format!("Failed to read token response body: {e}"))?;
|
|
||||||
|
|
||||||
if !status.is_success() {
|
|
||||||
return Err(format!(
|
|
||||||
"Failed to fetch access token: status={} body={}",
|
|
||||||
status.as_u16(),
|
|
||||||
body
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed: Value =
|
|
||||||
serde_json::from_str(&body).map_err(|e| format!("Invalid token response JSON: {e}"))?;
|
|
||||||
let token = parsed
|
|
||||||
.get("access_token")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.ok_or_else(|| format!("Token response missing access_token: {body}"))?;
|
|
||||||
|
|
||||||
Ok(token.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keyring_entry(environment: Environment) -> CommandResult<Entry> {
|
|
||||||
Entry::new(environment.keyring_service(), KEYRING_USER)
|
|
||||||
.map_err(|e| format!("Failed to initialize auth keyring entry: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_auth_token(environment: Environment) -> CommandResult<Option<String>> {
|
|
||||||
let entry = keyring_entry(environment)?;
|
|
||||||
match entry.get_password() {
|
|
||||||
Ok(token) => Ok(Some(token)),
|
|
||||||
Err(keyring::Error::NoEntry) => Ok(None),
|
|
||||||
Err(err) => Err(format!("Failed to read auth token: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn store_auth_token(environment: Environment, token: &str) -> CommandResult {
|
|
||||||
let entry = keyring_entry(environment)?;
|
|
||||||
entry.set_password(token).map_err(|e| format!("Failed to store auth token: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delete_auth_token(environment: Environment) -> CommandResult {
|
|
||||||
let entry = keyring_entry(environment)?;
|
|
||||||
match entry.delete_credential() {
|
|
||||||
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
|
|
||||||
Err(err) => Err(format!("Failed to delete auth token: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_hex(bytes: usize) -> String {
|
|
||||||
let mut data = vec![0_u8; bytes];
|
|
||||||
OsRng.fill_bytes(&mut data);
|
|
||||||
hex::encode(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm_open_browser() -> CommandResult<bool> {
|
|
||||||
if !io::stdin().is_terminal() {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
|
||||||
print!("Open default browser? [Y/n]: ");
|
|
||||||
io::stdout().flush().map_err(|e| format!("Failed to flush stdout: {e}"))?;
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin().read_line(&mut input).map_err(|e| format!("Failed to read input: {e}"))?;
|
|
||||||
|
|
||||||
match input.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"" | "y" | "yes" => return Ok(true),
|
|
||||||
"n" | "no" => return Ok(false),
|
|
||||||
_ => ui::warning("Please answer y or n"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn environment_mapping() {
|
|
||||||
assert_eq!(parse_environment(Some("staging")), Environment::Staging);
|
|
||||||
assert_eq!(parse_environment(Some("development")), Environment::Development);
|
|
||||||
assert_eq!(parse_environment(Some("production")), Environment::Production);
|
|
||||||
assert_eq!(parse_environment(None), Environment::Production);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn parses_callback_request() {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
|
||||||
let addr = listener.local_addr().expect("local addr");
|
|
||||||
|
|
||||||
let server = tokio::spawn(async move {
|
|
||||||
let (mut stream, _) = listener.accept().await.expect("accept");
|
|
||||||
parse_callback_request(&mut stream).await
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut client = TcpStream::connect(addr).await.expect("connect");
|
|
||||||
client
|
|
||||||
.write_all(
|
|
||||||
b"GET /oauth/callback?code=abc123&state=xyz HTTP/1.1\r\nHost: localhost\r\n\r\n",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("write");
|
|
||||||
|
|
||||||
let parsed = server.await.expect("join").expect("parse");
|
|
||||||
assert_eq!(parsed.0, "xyz");
|
|
||||||
assert_eq!(parsed.1, "abc123");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn parse_callback_request_oauth_error() {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
|
||||||
let addr = listener.local_addr().expect("local addr");
|
|
||||||
|
|
||||||
let server = tokio::spawn(async move {
|
|
||||||
let (mut stream, _) = listener.accept().await.expect("accept");
|
|
||||||
parse_callback_request(&mut stream).await
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut client = TcpStream::connect(addr).await.expect("connect");
|
|
||||||
client
|
|
||||||
.write_all(
|
|
||||||
b"GET /oauth/callback?error=access_denied&error_description=User%20denied&state=xyz HTTP/1.1\r\nHost: localhost\r\n\r\n",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("write");
|
|
||||||
|
|
||||||
let err = server.await.expect("join").expect_err("should fail");
|
|
||||||
assert!(err.contains("OAuth provider returned error: access_denied"));
|
|
||||||
assert!(err.contains("User denied"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn receive_oauth_code_fails_fast_on_provider_error() {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
|
||||||
let addr = listener.local_addr().expect("local addr");
|
|
||||||
|
|
||||||
let server = tokio::spawn(async move {
|
|
||||||
receive_oauth_code(listener, "expected-state", "http://localhost:9444").await
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut client = TcpStream::connect(addr).await.expect("connect");
|
|
||||||
client
|
|
||||||
.write_all(
|
|
||||||
b"GET /oauth/callback?error=access_denied&state=expected-state HTTP/1.1\r\nHost: localhost\r\n\r\n",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("write");
|
|
||||||
|
|
||||||
let result = tokio::time::timeout(std::time::Duration::from_secs(2), server)
|
|
||||||
.await
|
|
||||||
.expect("should not timeout")
|
|
||||||
.expect("join");
|
|
||||||
let err = result.expect_err("should return oauth error");
|
|
||||||
assert!(err.contains("OAuth provider returned error: access_denied"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn builds_oauth_flow_with_pkce() {
|
|
||||||
let flow = build_oauth_flow(Environment::Development, 8080).expect("flow");
|
|
||||||
assert!(flow.auth_url.as_str().contains("code_challenge_method=S256"));
|
|
||||||
assert!(
|
|
||||||
flow.auth_url
|
|
||||||
.as_str()
|
|
||||||
.contains("redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Foauth%2Fcallback")
|
|
||||||
);
|
|
||||||
assert_eq!(flow.redirect_url, "http://127.0.0.1:8080/oauth/callback");
|
|
||||||
assert_eq!(flow.token_url, "http://localhost:9444/login/oauth/access_token");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
use crate::cli::{CookieJarArgs, CookieJarCommands};
|
|
||||||
use crate::context::CliContext;
|
|
||||||
use crate::utils::workspace::resolve_workspace_id;
|
|
||||||
|
|
||||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
|
||||||
|
|
||||||
pub fn run(ctx: &CliContext, args: CookieJarArgs) -> i32 {
|
|
||||||
let result = match args.command {
|
|
||||||
CookieJarCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(()) => 0,
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Error: {error}");
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
|
||||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "cookie-jar list")?;
|
|
||||||
let cookie_jars = ctx
|
|
||||||
.db()
|
|
||||||
.list_cookie_jars(&workspace_id)
|
|
||||||
.map_err(|e| format!("Failed to list cookie jars: {e}"))?;
|
|
||||||
|
|
||||||
if cookie_jars.is_empty() {
|
|
||||||
println!("No cookie jars found in workspace {}", workspace_id);
|
|
||||||
} else {
|
|
||||||
for cookie_jar in cookie_jars {
|
|
||||||
println!(
|
|
||||||
"{} - {} ({} cookies)",
|
|
||||||
cookie_jar.id,
|
|
||||||
cookie_jar.name,
|
|
||||||
cookie_jar.cookies.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,9 @@ use crate::cli::{EnvironmentArgs, EnvironmentCommands};
|
|||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::utils::confirm::confirm_delete;
|
use crate::utils::confirm::confirm_delete;
|
||||||
use crate::utils::json::{
|
use crate::utils::json::{
|
||||||
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
||||||
parse_required_json, require_id, validate_create_id,
|
validate_create_id,
|
||||||
};
|
};
|
||||||
use crate::utils::schema::append_agent_hints;
|
|
||||||
use crate::utils::workspace::resolve_workspace_id;
|
|
||||||
use schemars::schema_for;
|
|
||||||
use yaak_models::models::Environment;
|
use yaak_models::models::Environment;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -15,8 +12,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
|||||||
|
|
||||||
pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
||||||
let result = match args.command {
|
let result = match args.command {
|
||||||
EnvironmentCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||||
EnvironmentCommands::Schema { pretty } => schema(pretty),
|
|
||||||
EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id),
|
EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id),
|
||||||
EnvironmentCommands::Create { workspace_id, name, json } => {
|
EnvironmentCommands::Create { workspace_id, name, json } => {
|
||||||
create(ctx, workspace_id, name, json)
|
create(ctx, workspace_id, name, json)
|
||||||
@@ -34,23 +30,10 @@ pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schema(pretty: bool) -> CommandResult {
|
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||||
let mut schema = serde_json::to_value(schema_for!(Environment))
|
|
||||||
.map_err(|e| format!("Failed to serialize environment schema: {e}"))?;
|
|
||||||
append_agent_hints(&mut schema);
|
|
||||||
|
|
||||||
let output =
|
|
||||||
if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
|
|
||||||
.map_err(|e| format!("Failed to format environment schema JSON: {e}"))?;
|
|
||||||
println!("{output}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
|
||||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "environment list")?;
|
|
||||||
let environments = ctx
|
let environments = ctx
|
||||||
.db()
|
.db()
|
||||||
.list_environments_ensure_base(&workspace_id)
|
.list_environments_ensure_base(workspace_id)
|
||||||
.map_err(|e| format!("Failed to list environments: {e}"))?;
|
.map_err(|e| format!("Failed to list environments: {e}"))?;
|
||||||
|
|
||||||
if environments.is_empty() {
|
if environments.is_empty() {
|
||||||
@@ -80,11 +63,17 @@ fn create(
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
json: Option<String>,
|
json: Option<String>,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
let json_shorthand =
|
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
||||||
workspace_id.as_deref().filter(|v| is_json_shorthand(v)).map(str::to_owned);
|
return Err(
|
||||||
let workspace_id_arg = workspace_id.filter(|v| !is_json_shorthand(v));
|
"environment create cannot combine workspace_id with --json payload".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let payload = parse_optional_json(json, json_shorthand, "environment create")?;
|
let payload = parse_optional_json(
|
||||||
|
json,
|
||||||
|
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
||||||
|
"environment create",
|
||||||
|
)?;
|
||||||
|
|
||||||
if let Some(payload) = payload {
|
if let Some(payload) = payload {
|
||||||
if name.is_some() {
|
if name.is_some() {
|
||||||
@@ -94,17 +83,10 @@ fn create(
|
|||||||
validate_create_id(&payload, "environment")?;
|
validate_create_id(&payload, "environment")?;
|
||||||
let mut environment: Environment = serde_json::from_value(payload)
|
let mut environment: Environment = serde_json::from_value(payload)
|
||||||
.map_err(|e| format!("Failed to parse environment create JSON: {e}"))?;
|
.map_err(|e| format!("Failed to parse environment create JSON: {e}"))?;
|
||||||
let fallback_workspace_id =
|
|
||||||
if workspace_id_arg.is_none() && environment.workspace_id.is_empty() {
|
if environment.workspace_id.is_empty() {
|
||||||
Some(resolve_workspace_id(ctx, None, "environment create")?)
|
return Err("environment create JSON requires non-empty \"workspaceId\"".to_string());
|
||||||
} else {
|
}
|
||||||
None
|
|
||||||
};
|
|
||||||
merge_workspace_id_arg(
|
|
||||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
|
||||||
&mut environment.workspace_id,
|
|
||||||
"environment create",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if environment.parent_model.is_empty() {
|
if environment.parent_model.is_empty() {
|
||||||
environment.parent_model = "environment".to_string();
|
environment.parent_model = "environment".to_string();
|
||||||
@@ -119,8 +101,9 @@ fn create(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let workspace_id =
|
let workspace_id = workspace_id.ok_or_else(|| {
|
||||||
resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "environment create")?;
|
"environment create requires workspace_id unless JSON payload is provided".to_string()
|
||||||
|
})?;
|
||||||
let name = name.ok_or_else(|| {
|
let name = name.ok_or_else(|| {
|
||||||
"environment create requires --name unless JSON payload is provided".to_string()
|
"environment create requires --name unless JSON payload is provided".to_string()
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ use crate::cli::{FolderArgs, FolderCommands};
|
|||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::utils::confirm::confirm_delete;
|
use crate::utils::confirm::confirm_delete;
|
||||||
use crate::utils::json::{
|
use crate::utils::json::{
|
||||||
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
||||||
parse_required_json, require_id, validate_create_id,
|
validate_create_id,
|
||||||
};
|
};
|
||||||
use crate::utils::workspace::resolve_workspace_id;
|
|
||||||
use yaak_models::models::Folder;
|
use yaak_models::models::Folder;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
|||||||
|
|
||||||
pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
||||||
let result = match args.command {
|
let result = match args.command {
|
||||||
FolderCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
FolderCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||||
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
|
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
|
||||||
FolderCommands::Create { workspace_id, name, json } => {
|
FolderCommands::Create { workspace_id, name, json } => {
|
||||||
create(ctx, workspace_id, name, json)
|
create(ctx, workspace_id, name, json)
|
||||||
@@ -31,10 +30,9 @@ pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "folder list")?;
|
|
||||||
let folders =
|
let folders =
|
||||||
ctx.db().list_folders(&workspace_id).map_err(|e| format!("Failed to list folders: {e}"))?;
|
ctx.db().list_folders(workspace_id).map_err(|e| format!("Failed to list folders: {e}"))?;
|
||||||
if folders.is_empty() {
|
if folders.is_empty() {
|
||||||
println!("No folders found in workspace {}", workspace_id);
|
println!("No folders found in workspace {}", workspace_id);
|
||||||
} else {
|
} else {
|
||||||
@@ -60,11 +58,15 @@ fn create(
|
|||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
json: Option<String>,
|
json: Option<String>,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
let json_shorthand =
|
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
||||||
workspace_id.as_deref().filter(|v| is_json_shorthand(v)).map(str::to_owned);
|
return Err("folder create cannot combine workspace_id with --json payload".to_string());
|
||||||
let workspace_id_arg = workspace_id.filter(|v| !is_json_shorthand(v));
|
}
|
||||||
|
|
||||||
let payload = parse_optional_json(json, json_shorthand, "folder create")?;
|
let payload = parse_optional_json(
|
||||||
|
json,
|
||||||
|
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
||||||
|
"folder create",
|
||||||
|
)?;
|
||||||
|
|
||||||
if let Some(payload) = payload {
|
if let Some(payload) = payload {
|
||||||
if name.is_some() {
|
if name.is_some() {
|
||||||
@@ -72,19 +74,12 @@ fn create(
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_create_id(&payload, "folder")?;
|
validate_create_id(&payload, "folder")?;
|
||||||
let mut folder: Folder = serde_json::from_value(payload)
|
let folder: Folder = serde_json::from_value(payload)
|
||||||
.map_err(|e| format!("Failed to parse folder create JSON: {e}"))?;
|
.map_err(|e| format!("Failed to parse folder create JSON: {e}"))?;
|
||||||
let fallback_workspace_id = if workspace_id_arg.is_none() && folder.workspace_id.is_empty()
|
|
||||||
{
|
if folder.workspace_id.is_empty() {
|
||||||
Some(resolve_workspace_id(ctx, None, "folder create")?)
|
return Err("folder create JSON requires non-empty \"workspaceId\"".to_string());
|
||||||
} else {
|
}
|
||||||
None
|
|
||||||
};
|
|
||||||
merge_workspace_id_arg(
|
|
||||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
|
||||||
&mut folder.workspace_id,
|
|
||||||
"folder create",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let created = ctx
|
let created = ctx
|
||||||
.db()
|
.db()
|
||||||
@@ -95,7 +90,9 @@ fn create(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "folder create")?;
|
let workspace_id = workspace_id.ok_or_else(|| {
|
||||||
|
"folder create requires workspace_id unless JSON payload is provided".to_string()
|
||||||
|
})?;
|
||||||
let name = name.ok_or_else(|| {
|
let name = name.ok_or_else(|| {
|
||||||
"folder create requires --name unless JSON payload is provided".to_string()
|
"folder create requires --name unless JSON payload is provided".to_string()
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
pub mod auth;
|
|
||||||
pub mod cookie_jar;
|
|
||||||
pub mod environment;
|
pub mod environment;
|
||||||
pub mod folder;
|
pub mod folder;
|
||||||
pub mod plugin;
|
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod send;
|
pub mod send;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|||||||
@@ -1,680 +0,0 @@
|
|||||||
use crate::cli::{GenerateArgs, InstallPluginArgs, PluginPathArg};
|
|
||||||
use crate::context::CliContext;
|
|
||||||
use crate::ui;
|
|
||||||
use crate::utils::http;
|
|
||||||
use keyring::Entry;
|
|
||||||
use rand::Rng;
|
|
||||||
use rolldown::{
|
|
||||||
BundleEvent, Bundler, BundlerOptions, ExperimentalOptions, InputItem, LogLevel, OutputFormat,
|
|
||||||
Platform, WatchOption, Watcher, WatcherEvent,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::{self, IsTerminal, Read, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
|
||||||
use yaak_models::models::{Plugin, PluginSource};
|
|
||||||
use yaak_models::util::UpdateSource;
|
|
||||||
use yaak_plugins::events::PluginContext;
|
|
||||||
use yaak_plugins::install::download_and_install;
|
|
||||||
use zip::CompressionMethod;
|
|
||||||
use zip::write::SimpleFileOptions;
|
|
||||||
|
|
||||||
type CommandResult<T = ()> = std::result::Result<T, String>;
|
|
||||||
|
|
||||||
const KEYRING_USER: &str = "yaak";
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
enum Environment {
|
|
||||||
Production,
|
|
||||||
Staging,
|
|
||||||
Development,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Environment {
|
|
||||||
fn api_base_url(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Environment::Production => "https://api.yaak.app",
|
|
||||||
Environment::Staging => "https://todo.yaak.app",
|
|
||||||
Environment::Development => "http://localhost:9444",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keyring_service(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Environment::Production => "app.yaak.cli.Token",
|
|
||||||
Environment::Staging => "app.yaak.cli.staging.Token",
|
|
||||||
Environment::Development => "app.yaak.cli.dev.Token",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_build(args: PluginPathArg) -> i32 {
|
|
||||||
match build(args).await {
|
|
||||||
Ok(()) => 0,
|
|
||||||
Err(error) => {
|
|
||||||
ui::error(&error);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_install(context: &CliContext, args: InstallPluginArgs) -> i32 {
|
|
||||||
match install(context, args).await {
|
|
||||||
Ok(()) => 0,
|
|
||||||
Err(error) => {
|
|
||||||
ui::error(&error);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_dev(args: PluginPathArg) -> i32 {
|
|
||||||
match dev(args).await {
|
|
||||||
Ok(()) => 0,
|
|
||||||
Err(error) => {
|
|
||||||
ui::error(&error);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_generate(args: GenerateArgs) -> i32 {
|
|
||||||
match generate(args) {
|
|
||||||
Ok(()) => 0,
|
|
||||||
Err(error) => {
|
|
||||||
ui::error(&error);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_publish(args: PluginPathArg) -> i32 {
|
|
||||||
match publish(args).await {
|
|
||||||
Ok(()) => 0,
|
|
||||||
Err(error) => {
|
|
||||||
ui::error(&error);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn build(args: PluginPathArg) -> CommandResult {
|
|
||||||
let plugin_dir = resolve_plugin_dir(args.path)?;
|
|
||||||
ensure_plugin_build_inputs(&plugin_dir)?;
|
|
||||||
|
|
||||||
ui::info(&format!("Building plugin {}...", plugin_dir.display()));
|
|
||||||
let warnings = build_plugin_bundle(&plugin_dir).await?;
|
|
||||||
for warning in warnings {
|
|
||||||
ui::warning(&warning);
|
|
||||||
}
|
|
||||||
ui::success(&format!("Built plugin bundle at {}", plugin_dir.join("build/index.js").display()));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn dev(args: PluginPathArg) -> CommandResult {
|
|
||||||
let plugin_dir = resolve_plugin_dir(args.path)?;
|
|
||||||
ensure_plugin_build_inputs(&plugin_dir)?;
|
|
||||||
|
|
||||||
ui::info(&format!("Watching plugin {}...", plugin_dir.display()));
|
|
||||||
|
|
||||||
let bundler = Bundler::new(bundler_options(&plugin_dir, true))
|
|
||||||
.map_err(|err| format!("Failed to initialize Rolldown watcher: {err}"))?;
|
|
||||||
let watcher = Watcher::new(vec![Arc::new(Mutex::new(bundler))], None)
|
|
||||||
.map_err(|err| format!("Failed to start Rolldown watcher: {err}"))?;
|
|
||||||
let emitter = watcher.emitter();
|
|
||||||
let watch_root = plugin_dir.clone();
|
|
||||||
let _event_logger = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let event = {
|
|
||||||
let rx = emitter.rx.lock().await;
|
|
||||||
rx.recv()
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(event) = event else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
match event {
|
|
||||||
WatcherEvent::Change(change) => {
|
|
||||||
let changed_path = Path::new(change.path.as_str());
|
|
||||||
let display_path = changed_path
|
|
||||||
.strip_prefix(&watch_root)
|
|
||||||
.map(|p| p.display().to_string())
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
changed_path
|
|
||||||
.file_name()
|
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
|
||||||
.unwrap_or_else(|| "unknown".to_string())
|
|
||||||
});
|
|
||||||
ui::info(&format!("Rebuilding plugin {display_path}"));
|
|
||||||
}
|
|
||||||
WatcherEvent::Event(BundleEvent::BundleEnd(_)) => {}
|
|
||||||
WatcherEvent::Event(BundleEvent::Error(event)) => {
|
|
||||||
if event.error.diagnostics.is_empty() {
|
|
||||||
ui::error("Plugin build failed");
|
|
||||||
} else {
|
|
||||||
for diagnostic in event.error.diagnostics {
|
|
||||||
ui::error(&diagnostic.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WatcherEvent::Close => break,
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.start().await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate(args: GenerateArgs) -> CommandResult {
|
|
||||||
let default_name = random_name();
|
|
||||||
let name = match args.name {
|
|
||||||
Some(name) => name,
|
|
||||||
None => prompt_with_default("Plugin name", &default_name)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let default_dir = format!("./{name}");
|
|
||||||
let output_dir = match args.dir {
|
|
||||||
Some(dir) => dir,
|
|
||||||
None => PathBuf::from(prompt_with_default("Plugin dir", &default_dir)?),
|
|
||||||
};
|
|
||||||
|
|
||||||
if output_dir.exists() {
|
|
||||||
return Err(format!("Plugin directory already exists: {}", output_dir.display()));
|
|
||||||
}
|
|
||||||
|
|
||||||
ui::info(&format!("Generating plugin in {}", output_dir.display()));
|
|
||||||
fs::create_dir_all(output_dir.join("src"))
|
|
||||||
.map_err(|e| format!("Failed creating plugin directory {}: {e}", output_dir.display()))?;
|
|
||||||
|
|
||||||
write_file(&output_dir.join(".gitignore"), TEMPLATE_GITIGNORE)?;
|
|
||||||
write_file(
|
|
||||||
&output_dir.join("package.json"),
|
|
||||||
&TEMPLATE_PACKAGE_JSON.replace("yaak-plugin-name", &name),
|
|
||||||
)?;
|
|
||||||
write_file(&output_dir.join("tsconfig.json"), TEMPLATE_TSCONFIG)?;
|
|
||||||
write_file(&output_dir.join("README.md"), &TEMPLATE_README.replace("yaak-plugin-name", &name))?;
|
|
||||||
write_file(
|
|
||||||
&output_dir.join("src/index.ts"),
|
|
||||||
&TEMPLATE_INDEX_TS.replace("yaak-plugin-name", &name),
|
|
||||||
)?;
|
|
||||||
write_file(&output_dir.join("src/index.test.ts"), TEMPLATE_INDEX_TEST_TS)?;
|
|
||||||
|
|
||||||
ui::success("Plugin scaffold generated");
|
|
||||||
ui::info("Next steps:");
|
|
||||||
println!(" 1. cd {}", output_dir.display());
|
|
||||||
println!(" 2. npm install");
|
|
||||||
println!(" 3. yaak plugin build");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn publish(args: PluginPathArg) -> CommandResult {
|
|
||||||
let plugin_dir = resolve_plugin_dir(args.path)?;
|
|
||||||
ensure_plugin_build_inputs(&plugin_dir)?;
|
|
||||||
|
|
||||||
let environment = current_environment();
|
|
||||||
let token = get_auth_token(environment)?
|
|
||||||
.ok_or_else(|| "Not logged in. Run `yaak auth login`.".to_string())?;
|
|
||||||
|
|
||||||
ui::info(&format!("Building plugin {}...", plugin_dir.display()));
|
|
||||||
let warnings = build_plugin_bundle(&plugin_dir).await?;
|
|
||||||
for warning in warnings {
|
|
||||||
ui::warning(&warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui::info("Archiving plugin");
|
|
||||||
let archive = create_publish_archive(&plugin_dir)?;
|
|
||||||
|
|
||||||
ui::info("Uploading plugin");
|
|
||||||
let url = format!("{}/api/v1/plugins/publish", environment.api_base_url());
|
|
||||||
let response = http::build_client(Some(&token))?
|
|
||||||
.post(url)
|
|
||||||
.header(reqwest::header::CONTENT_TYPE, "application/zip")
|
|
||||||
.body(archive)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to upload plugin: {e}"))?;
|
|
||||||
|
|
||||||
let status = response.status();
|
|
||||||
let body =
|
|
||||||
response.text().await.map_err(|e| format!("Failed reading publish response body: {e}"))?;
|
|
||||||
|
|
||||||
if !status.is_success() {
|
|
||||||
return Err(http::parse_api_error(status.as_u16(), &body));
|
|
||||||
}
|
|
||||||
|
|
||||||
let published: PublishResponse = serde_json::from_str(&body)
|
|
||||||
.map_err(|e| format!("Failed parsing publish response JSON: {e}\nResponse: {body}"))?;
|
|
||||||
ui::success(&format!("Plugin published {}", published.version));
|
|
||||||
println!(" -> {}", published.url);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn install(context: &CliContext, args: InstallPluginArgs) -> CommandResult {
|
|
||||||
if args.source.starts_with('@') {
|
|
||||||
let (name, version) =
|
|
||||||
parse_registry_install_spec(args.source.as_str()).ok_or_else(|| {
|
|
||||||
"Invalid registry plugin spec. Expected format: @org/plugin or @org/plugin@version"
|
|
||||||
.to_string()
|
|
||||||
})?;
|
|
||||||
return install_from_registry(context, name, version).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
install_from_directory(context, args.source.as_str()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn install_from_registry(
|
|
||||||
context: &CliContext,
|
|
||||||
name: String,
|
|
||||||
version: Option<String>,
|
|
||||||
) -> CommandResult {
|
|
||||||
let current_version = crate::version::cli_version();
|
|
||||||
let http_client = yaak_api_client(ApiClientKind::Cli, current_version)
|
|
||||||
.map_err(|err| format!("Failed to initialize API client: {err}"))?;
|
|
||||||
let installing_version = version.clone().unwrap_or_else(|| "latest".to_string());
|
|
||||||
ui::info(&format!("Installing registry plugin {name}@{installing_version}"));
|
|
||||||
|
|
||||||
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
|
|
||||||
let installed = download_and_install(
|
|
||||||
context.plugin_manager(),
|
|
||||||
context.query_manager(),
|
|
||||||
&http_client,
|
|
||||||
&plugin_context,
|
|
||||||
name.as_str(),
|
|
||||||
version,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|err| format!("Failed to install plugin: {err}"))?;
|
|
||||||
|
|
||||||
ui::success(&format!("Installed plugin {}@{}", installed.name, installed.version));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn install_from_directory(context: &CliContext, source: &str) -> CommandResult {
|
|
||||||
let plugin_dir = resolve_plugin_dir(Some(PathBuf::from(source)))?;
|
|
||||||
let plugin_dir_str = plugin_dir
|
|
||||||
.to_str()
|
|
||||||
.ok_or_else(|| {
|
|
||||||
format!("Plugin directory path is not valid UTF-8: {}", plugin_dir.display())
|
|
||||||
})?
|
|
||||||
.to_string();
|
|
||||||
ui::info(&format!("Installing plugin from directory {}", plugin_dir.display()));
|
|
||||||
|
|
||||||
let plugin = context
|
|
||||||
.db()
|
|
||||||
.upsert_plugin(
|
|
||||||
&Plugin {
|
|
||||||
directory: plugin_dir_str,
|
|
||||||
url: None,
|
|
||||||
enabled: true,
|
|
||||||
source: PluginSource::Filesystem,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&UpdateSource::Background,
|
|
||||||
)
|
|
||||||
.map_err(|err| format!("Failed to save plugin in database: {err}"))?;
|
|
||||||
|
|
||||||
let plugin_context = PluginContext::new(Some("cli".to_string()), None);
|
|
||||||
context
|
|
||||||
.plugin_manager()
|
|
||||||
.add_plugin(&plugin_context, &plugin)
|
|
||||||
.await
|
|
||||||
.map_err(|err| format!("Failed to load plugin runtime: {err}"))?;
|
|
||||||
|
|
||||||
ui::success(&format!("Installed plugin from {}", plugin.directory));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_registry_install_spec(source: &str) -> Option<(String, Option<String>)> {
|
|
||||||
if !source.starts_with('@') || !source.contains('/') {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rest = source.get(1..)?;
|
|
||||||
let version_split = rest.rfind('@').map(|idx| idx + 1);
|
|
||||||
let (name, version) = match version_split {
|
|
||||||
Some(at_idx) => {
|
|
||||||
let (name, version) = source.split_at(at_idx);
|
|
||||||
let version = version.strip_prefix('@').unwrap_or_default();
|
|
||||||
if version.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
(name.to_string(), Some(version.to_string()))
|
|
||||||
}
|
|
||||||
None => (source.to_string(), None),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !name.starts_with('@') {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let without_scope = name.get(1..)?;
|
|
||||||
let (scope, plugin_name) = without_scope.split_once('/')?;
|
|
||||||
if scope.is_empty() || plugin_name.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((name, version))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct PublishResponse {
|
|
||||||
version: String,
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn build_plugin_bundle(plugin_dir: &Path) -> CommandResult<Vec<String>> {
|
|
||||||
prepare_build_output_dir(plugin_dir)?;
|
|
||||||
let mut bundler = Bundler::new(bundler_options(plugin_dir, false))
|
|
||||||
.map_err(|err| format!("Failed to initialize Rolldown: {err}"))?;
|
|
||||||
let output = bundler.write().await.map_err(|err| format!("Plugin build failed:\n{err}"))?;
|
|
||||||
|
|
||||||
Ok(output.warnings.into_iter().map(|w| w.to_string()).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_build_output_dir(plugin_dir: &Path) -> CommandResult {
|
|
||||||
let build_dir = plugin_dir.join("build");
|
|
||||||
if build_dir.exists() {
|
|
||||||
fs::remove_dir_all(&build_dir)
|
|
||||||
.map_err(|e| format!("Failed to clean build directory {}: {e}", build_dir.display()))?;
|
|
||||||
}
|
|
||||||
fs::create_dir_all(&build_dir)
|
|
||||||
.map_err(|e| format!("Failed to create build directory {}: {e}", build_dir.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bundler_options(plugin_dir: &Path, watch: bool) -> BundlerOptions {
|
|
||||||
BundlerOptions {
|
|
||||||
input: Some(vec![InputItem { import: "./src/index.ts".to_string(), ..Default::default() }]),
|
|
||||||
cwd: Some(plugin_dir.to_path_buf()),
|
|
||||||
file: Some("build/index.js".to_string()),
|
|
||||||
format: Some(OutputFormat::Cjs),
|
|
||||||
platform: Some(Platform::Node),
|
|
||||||
log_level: Some(LogLevel::Info),
|
|
||||||
experimental: watch
|
|
||||||
.then_some(ExperimentalOptions { incremental_build: Some(true), ..Default::default() }),
|
|
||||||
watch: watch.then_some(WatchOption::default()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_plugin_dir(path: Option<PathBuf>) -> CommandResult<PathBuf> {
|
|
||||||
let cwd =
|
|
||||||
std::env::current_dir().map_err(|e| format!("Failed to read current directory: {e}"))?;
|
|
||||||
let candidate = match path {
|
|
||||||
Some(path) if path.is_absolute() => path,
|
|
||||||
Some(path) => cwd.join(path),
|
|
||||||
None => cwd,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !candidate.exists() {
|
|
||||||
return Err(format!("Plugin directory does not exist: {}", candidate.display()));
|
|
||||||
}
|
|
||||||
if !candidate.is_dir() {
|
|
||||||
return Err(format!("Plugin path is not a directory: {}", candidate.display()));
|
|
||||||
}
|
|
||||||
|
|
||||||
candidate
|
|
||||||
.canonicalize()
|
|
||||||
.map_err(|e| format!("Failed to resolve plugin directory {}: {e}", candidate.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_plugin_build_inputs(plugin_dir: &Path) -> CommandResult {
|
|
||||||
let package_json = plugin_dir.join("package.json");
|
|
||||||
if !package_json.is_file() {
|
|
||||||
return Err(format!(
|
|
||||||
"{} does not exist. Ensure that you are in a plugin directory.",
|
|
||||||
package_json.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let entry = plugin_dir.join("src/index.ts");
|
|
||||||
if !entry.is_file() {
|
|
||||||
return Err(format!("Required entrypoint missing: {}", entry.display()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_publish_archive(plugin_dir: &Path) -> CommandResult<Vec<u8>> {
|
|
||||||
let required_files = [
|
|
||||||
"README.md",
|
|
||||||
"package.json",
|
|
||||||
"build/index.js",
|
|
||||||
"src/index.ts",
|
|
||||||
];
|
|
||||||
let optional_files = ["package-lock.json"];
|
|
||||||
|
|
||||||
let mut selected = HashSet::new();
|
|
||||||
for required in required_files {
|
|
||||||
let required_path = plugin_dir.join(required);
|
|
||||||
if !required_path.is_file() {
|
|
||||||
return Err(format!("Missing required file: {required}"));
|
|
||||||
}
|
|
||||||
selected.insert(required.to_string());
|
|
||||||
}
|
|
||||||
for optional in optional_files {
|
|
||||||
selected.insert(optional.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let cursor = std::io::Cursor::new(Vec::new());
|
|
||||||
let mut zip = zip::ZipWriter::new(cursor);
|
|
||||||
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Deflated);
|
|
||||||
|
|
||||||
for entry in WalkDir::new(plugin_dir) {
|
|
||||||
let entry = entry.map_err(|e| format!("Failed walking plugin directory: {e}"))?;
|
|
||||||
if !entry.file_type().is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = entry.path();
|
|
||||||
let rel = path
|
|
||||||
.strip_prefix(plugin_dir)
|
|
||||||
.map_err(|e| format!("Failed deriving relative path for {}: {e}", path.display()))?;
|
|
||||||
let rel = rel.to_string_lossy().replace('\\', "/");
|
|
||||||
|
|
||||||
let keep = rel.starts_with("src/") || rel.starts_with("build/") || selected.contains(&rel);
|
|
||||||
if !keep {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
zip.start_file(rel, options).map_err(|e| format!("Failed adding file to archive: {e}"))?;
|
|
||||||
let mut file = fs::File::open(path)
|
|
||||||
.map_err(|e| format!("Failed opening file {}: {e}", path.display()))?;
|
|
||||||
let mut contents = Vec::new();
|
|
||||||
file.read_to_end(&mut contents)
|
|
||||||
.map_err(|e| format!("Failed reading file {}: {e}", path.display()))?;
|
|
||||||
zip.write_all(&contents).map_err(|e| format!("Failed writing archive contents: {e}"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cursor = zip.finish().map_err(|e| format!("Failed finalizing plugin archive: {e}"))?;
|
|
||||||
Ok(cursor.into_inner())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_file(path: &Path, contents: &str) -> CommandResult {
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
fs::create_dir_all(parent)
|
|
||||||
.map_err(|e| format!("Failed creating directory {}: {e}", parent.display()))?;
|
|
||||||
}
|
|
||||||
fs::write(path, contents).map_err(|e| format!("Failed writing file {}: {e}", path.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt_with_default(label: &str, default: &str) -> CommandResult<String> {
|
|
||||||
if !io::stdin().is_terminal() {
|
|
||||||
return Ok(default.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
print!("{label} [{default}]: ");
|
|
||||||
io::stdout().flush().map_err(|e| format!("Failed to flush stdout: {e}"))?;
|
|
||||||
|
|
||||||
let mut input = String::new();
|
|
||||||
io::stdin().read_line(&mut input).map_err(|e| format!("Failed to read input: {e}"))?;
|
|
||||||
let trimmed = input.trim();
|
|
||||||
|
|
||||||
if trimmed.is_empty() { Ok(default.to_string()) } else { Ok(trimmed.to_string()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_environment() -> Environment {
|
|
||||||
match std::env::var("ENVIRONMENT").as_deref() {
|
|
||||||
Ok("staging") => Environment::Staging,
|
|
||||||
Ok("development") => Environment::Development,
|
|
||||||
_ => Environment::Production,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keyring_entry(environment: Environment) -> CommandResult<Entry> {
|
|
||||||
Entry::new(environment.keyring_service(), KEYRING_USER)
|
|
||||||
.map_err(|e| format!("Failed to initialize auth keyring entry: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_auth_token(environment: Environment) -> CommandResult<Option<String>> {
|
|
||||||
let entry = keyring_entry(environment)?;
|
|
||||||
match entry.get_password() {
|
|
||||||
Ok(token) => Ok(Some(token)),
|
|
||||||
Err(keyring::Error::NoEntry) => Ok(None),
|
|
||||||
Err(err) => Err(format!("Failed to read auth token: {err}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_name() -> String {
|
|
||||||
const ADJECTIVES: &[&str] = &[
|
|
||||||
"young", "youthful", "yellow", "yielding", "yappy", "yawning", "yummy", "yucky", "yearly",
|
|
||||||
"yester", "yeasty", "yelling",
|
|
||||||
];
|
|
||||||
const NOUNS: &[&str] = &[
|
|
||||||
"yak", "yarn", "year", "yell", "yoke", "yoga", "yam", "yacht", "yodel",
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
let adjective = ADJECTIVES[rng.gen_range(0..ADJECTIVES.len())];
|
|
||||||
let noun = NOUNS[rng.gen_range(0..NOUNS.len())];
|
|
||||||
format!("{adjective}-{noun}")
|
|
||||||
}
|
|
||||||
|
|
||||||
const TEMPLATE_GITIGNORE: &str = "node_modules\n";
|
|
||||||
|
|
||||||
const TEMPLATE_PACKAGE_JSON: &str = r#"{
|
|
||||||
"name": "yaak-plugin-name",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.1",
|
|
||||||
"scripts": {
|
|
||||||
"build": "yaak plugin build",
|
|
||||||
"dev": "yaak plugin dev"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^24.10.1",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"vitest": "^4.0.14"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@yaakapp/api": "^0.7.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const TEMPLATE_TSCONFIG: &str = r#"{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2021",
|
|
||||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"allowJs": false,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": false,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const TEMPLATE_README: &str = r#"# yaak-plugin-name
|
|
||||||
|
|
||||||
Describe what your plugin does.
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const TEMPLATE_INDEX_TS: &str = r#"import type { PluginDefinition } from "@yaakapp/api";
|
|
||||||
|
|
||||||
export const plugin: PluginDefinition = {
|
|
||||||
httpRequestActions: [
|
|
||||||
{
|
|
||||||
label: "Hello, From Plugin",
|
|
||||||
icon: "info",
|
|
||||||
async onSelect(ctx, args) {
|
|
||||||
await ctx.toast.show({
|
|
||||||
color: "success",
|
|
||||||
message: `You clicked the request ${args.httpRequest.id}`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const TEMPLATE_INDEX_TEST_TS: &str = r#"import { describe, expect, test } from "vitest";
|
|
||||||
import { plugin } from "./index";
|
|
||||||
|
|
||||||
describe("Example Plugin", () => {
|
|
||||||
test("Exports plugin object", () => {
|
|
||||||
expect(plugin).toBeTypeOf("object");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
"#;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::create_publish_archive;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fs;
|
|
||||||
use std::io::Cursor;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
use zip::ZipArchive;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn publish_archive_includes_required_and_optional_files() {
|
|
||||||
let dir = TempDir::new().expect("temp dir");
|
|
||||||
let root = dir.path();
|
|
||||||
|
|
||||||
fs::create_dir_all(root.join("src")).expect("create src");
|
|
||||||
fs::create_dir_all(root.join("build")).expect("create build");
|
|
||||||
fs::create_dir_all(root.join("ignored")).expect("create ignored");
|
|
||||||
|
|
||||||
fs::write(root.join("README.md"), "# Demo\n").expect("write README");
|
|
||||||
fs::write(root.join("package.json"), "{}").expect("write package.json");
|
|
||||||
fs::write(root.join("package-lock.json"), "{}").expect("write package-lock.json");
|
|
||||||
fs::write(root.join("src/index.ts"), "export const plugin = {};\n")
|
|
||||||
.expect("write src/index.ts");
|
|
||||||
fs::write(root.join("build/index.js"), "exports.plugin = {};\n")
|
|
||||||
.expect("write build/index.js");
|
|
||||||
fs::write(root.join("ignored/secret.txt"), "do-not-ship").expect("write ignored file");
|
|
||||||
|
|
||||||
let archive = create_publish_archive(root).expect("create archive");
|
|
||||||
let mut zip = ZipArchive::new(Cursor::new(archive)).expect("open zip");
|
|
||||||
|
|
||||||
let mut names = HashSet::new();
|
|
||||||
for i in 0..zip.len() {
|
|
||||||
let file = zip.by_index(i).expect("zip entry");
|
|
||||||
names.insert(file.name().to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(names.contains("README.md"));
|
|
||||||
assert!(names.contains("package.json"));
|
|
||||||
assert!(names.contains("package-lock.json"));
|
|
||||||
assert!(names.contains("src/index.ts"));
|
|
||||||
assert!(names.contains("build/index.js"));
|
|
||||||
assert!(!names.contains("ignored/secret.txt"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,14 @@ use crate::cli::{RequestArgs, RequestCommands, RequestSchemaType};
|
|||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use crate::utils::confirm::confirm_delete;
|
use crate::utils::confirm::confirm_delete;
|
||||||
use crate::utils::json::{
|
use crate::utils::json::{
|
||||||
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
apply_merge_patch, is_json_shorthand, parse_optional_json, parse_required_json, require_id,
|
||||||
parse_required_json, require_id, validate_create_id,
|
validate_create_id,
|
||||||
};
|
};
|
||||||
use crate::utils::schema::append_agent_hints;
|
|
||||||
use crate::utils::workspace::resolve_workspace_id;
|
|
||||||
use schemars::schema_for;
|
use schemars::schema_for;
|
||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use yaak::send::{SendHttpRequestByIdWithPluginsParams, send_http_request_by_id_with_plugins};
|
use yaak::send::{SendHttpRequestByIdWithPluginsParams, send_http_request_by_id_with_plugins};
|
||||||
use yaak_http::sender::HttpResponseEvent as SenderHttpResponseEvent;
|
|
||||||
use yaak_models::models::{GrpcRequest, HttpRequest, WebsocketRequest};
|
use yaak_models::models::{GrpcRequest, HttpRequest, WebsocketRequest};
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
use yaak_models::queries::any_request::AnyRequest;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
@@ -25,16 +21,13 @@ pub async fn run(
|
|||||||
ctx: &CliContext,
|
ctx: &CliContext,
|
||||||
args: RequestArgs,
|
args: RequestArgs,
|
||||||
environment: Option<&str>,
|
environment: Option<&str>,
|
||||||
cookie_jar_id: Option<&str>,
|
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
let result = match args.command {
|
let result = match args.command {
|
||||||
RequestCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
RequestCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||||
RequestCommands::Show { request_id } => show(ctx, &request_id),
|
RequestCommands::Show { request_id } => show(ctx, &request_id),
|
||||||
RequestCommands::Send { request_id } => {
|
RequestCommands::Send { request_id } => {
|
||||||
return match send_request_by_id(ctx, &request_id, environment, cookie_jar_id, verbose)
|
return match send_request_by_id(ctx, &request_id, environment, verbose).await {
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => 0,
|
Ok(()) => 0,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Error: {error}");
|
eprintln!("Error: {error}");
|
||||||
@@ -42,8 +35,8 @@ pub async fn run(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
RequestCommands::Schema { request_type, pretty } => {
|
RequestCommands::Schema { request_type } => {
|
||||||
return match schema(ctx, request_type, pretty).await {
|
return match schema(ctx, request_type).await {
|
||||||
Ok(()) => 0,
|
Ok(()) => 0,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Error: {error}");
|
eprintln!("Error: {error}");
|
||||||
@@ -67,11 +60,10 @@ pub async fn run(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "request list")?;
|
|
||||||
let requests = ctx
|
let requests = ctx
|
||||||
.db()
|
.db()
|
||||||
.list_http_requests(&workspace_id)
|
.list_http_requests(workspace_id)
|
||||||
.map_err(|e| format!("Failed to list requests: {e}"))?;
|
.map_err(|e| format!("Failed to list requests: {e}"))?;
|
||||||
if requests.is_empty() {
|
if requests.is_empty() {
|
||||||
println!("No requests found in workspace {}", workspace_id);
|
println!("No requests found in workspace {}", workspace_id);
|
||||||
@@ -83,7 +75,7 @@ fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn schema(ctx: &CliContext, request_type: RequestSchemaType, pretty: bool) -> CommandResult {
|
async fn schema(ctx: &CliContext, request_type: RequestSchemaType) -> CommandResult {
|
||||||
let mut schema = match request_type {
|
let mut schema = match request_type {
|
||||||
RequestSchemaType::Http => serde_json::to_value(schema_for!(HttpRequest))
|
RequestSchemaType::Http => serde_json::to_value(schema_for!(HttpRequest))
|
||||||
.map_err(|e| format!("Failed to serialize HTTP request schema: {e}"))?,
|
.map_err(|e| format!("Failed to serialize HTTP request schema: {e}"))?,
|
||||||
@@ -93,51 +85,16 @@ async fn schema(ctx: &CliContext, request_type: RequestSchemaType, pretty: bool)
|
|||||||
.map_err(|e| format!("Failed to serialize WebSocket request schema: {e}"))?,
|
.map_err(|e| format!("Failed to serialize WebSocket request schema: {e}"))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
enrich_schema_guidance(&mut schema, request_type);
|
|
||||||
append_agent_hints(&mut schema);
|
|
||||||
|
|
||||||
if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await {
|
if let Err(error) = merge_auth_schema_from_plugins(ctx, &mut schema).await {
|
||||||
eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}");
|
eprintln!("Warning: Failed to enrich authentication schema from plugins: {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let output =
|
let output = serde_json::to_string_pretty(&schema)
|
||||||
if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
|
.map_err(|e| format!("Failed to format schema JSON: {e}"))?;
|
||||||
.map_err(|e| format!("Failed to format schema JSON: {e}"))?;
|
|
||||||
println!("{output}");
|
println!("{output}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enrich_schema_guidance(schema: &mut Value, request_type: RequestSchemaType) {
|
|
||||||
if !matches!(request_type, RequestSchemaType::Http) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(properties) = schema.get_mut("properties").and_then(Value::as_object_mut) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(url_schema) = properties.get_mut("url").and_then(Value::as_object_mut) {
|
|
||||||
append_description(
|
|
||||||
url_schema,
|
|
||||||
"For path segments like `/foo/:id/comments/:commentId`, put concrete values in `urlParameters` using names without `:` (for example `id`, `commentId`).",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_description(schema: &mut Map<String, Value>, extra: &str) {
|
|
||||||
match schema.get_mut("description") {
|
|
||||||
Some(Value::String(existing)) if !existing.trim().is_empty() => {
|
|
||||||
if !existing.ends_with(' ') {
|
|
||||||
existing.push(' ');
|
|
||||||
}
|
|
||||||
existing.push_str(extra);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
schema.insert("description".to_string(), Value::String(extra.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn merge_auth_schema_from_plugins(
|
async fn merge_auth_schema_from_plugins(
|
||||||
ctx: &CliContext,
|
ctx: &CliContext,
|
||||||
schema: &mut Value,
|
schema: &mut Value,
|
||||||
@@ -341,11 +298,15 @@ fn create(
|
|||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
json: Option<String>,
|
json: Option<String>,
|
||||||
) -> CommandResult {
|
) -> CommandResult {
|
||||||
let json_shorthand =
|
if json.is_some() && workspace_id.as_deref().is_some_and(|v| !is_json_shorthand(v)) {
|
||||||
workspace_id.as_deref().filter(|v| is_json_shorthand(v)).map(str::to_owned);
|
return Err("request create cannot combine workspace_id with --json payload".to_string());
|
||||||
let workspace_id_arg = workspace_id.filter(|v| !is_json_shorthand(v));
|
}
|
||||||
|
|
||||||
let payload = parse_optional_json(json, json_shorthand, "request create")?;
|
let payload = parse_optional_json(
|
||||||
|
json,
|
||||||
|
workspace_id.clone().filter(|v| is_json_shorthand(v)),
|
||||||
|
"request create",
|
||||||
|
)?;
|
||||||
|
|
||||||
if let Some(payload) = payload {
|
if let Some(payload) = payload {
|
||||||
if name.is_some() || method.is_some() || url.is_some() {
|
if name.is_some() || method.is_some() || url.is_some() {
|
||||||
@@ -353,19 +314,12 @@ fn create(
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate_create_id(&payload, "request")?;
|
validate_create_id(&payload, "request")?;
|
||||||
let mut request: HttpRequest = serde_json::from_value(payload)
|
let request: HttpRequest = serde_json::from_value(payload)
|
||||||
.map_err(|e| format!("Failed to parse request create JSON: {e}"))?;
|
.map_err(|e| format!("Failed to parse request create JSON: {e}"))?;
|
||||||
let fallback_workspace_id = if workspace_id_arg.is_none() && request.workspace_id.is_empty()
|
|
||||||
{
|
if request.workspace_id.is_empty() {
|
||||||
Some(resolve_workspace_id(ctx, None, "request create")?)
|
return Err("request create JSON requires non-empty \"workspaceId\"".to_string());
|
||||||
} else {
|
}
|
||||||
None
|
|
||||||
};
|
|
||||||
merge_workspace_id_arg(
|
|
||||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
|
||||||
&mut request.workspace_id,
|
|
||||||
"request create",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let created = ctx
|
let created = ctx
|
||||||
.db()
|
.db()
|
||||||
@@ -376,7 +330,9 @@ fn create(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "request create")?;
|
let workspace_id = workspace_id.ok_or_else(|| {
|
||||||
|
"request create requires workspace_id unless JSON payload is provided".to_string()
|
||||||
|
})?;
|
||||||
let name = name.unwrap_or_default();
|
let name = name.unwrap_or_default();
|
||||||
let url = url.unwrap_or_default();
|
let url = url.unwrap_or_default();
|
||||||
let method = method.unwrap_or_else(|| "GET".to_string());
|
let method = method.unwrap_or_else(|| "GET".to_string());
|
||||||
@@ -445,7 +401,6 @@ pub async fn send_request_by_id(
|
|||||||
ctx: &CliContext,
|
ctx: &CliContext,
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
environment: Option<&str>,
|
environment: Option<&str>,
|
||||||
cookie_jar_id: Option<&str>,
|
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let request =
|
let request =
|
||||||
@@ -457,7 +412,6 @@ pub async fn send_request_by_id(
|
|||||||
&http_request.id,
|
&http_request.id,
|
||||||
&http_request.workspace_id,
|
&http_request.workspace_id,
|
||||||
environment,
|
environment,
|
||||||
cookie_jar_id,
|
|
||||||
verbose,
|
verbose,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -476,32 +430,18 @@ async fn send_http_request_by_id(
|
|||||||
request_id: &str,
|
request_id: &str,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
environment: Option<&str>,
|
environment: Option<&str>,
|
||||||
cookie_jar_id: Option<&str>,
|
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let cookie_jar_id = resolve_cookie_jar_id(ctx, workspace_id, cookie_jar_id)?;
|
let plugin_context = PluginContext::new(None, Some(workspace_id.to_string()));
|
||||||
|
|
||||||
let plugin_context =
|
let (event_tx, mut event_rx) = mpsc::channel(100);
|
||||||
PluginContext::new(Some("cli".to_string()), Some(workspace_id.to_string()));
|
|
||||||
|
|
||||||
let (event_tx, mut event_rx) = mpsc::channel::<SenderHttpResponseEvent>(100);
|
|
||||||
let (body_chunk_tx, mut body_chunk_rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
|
||||||
let event_handle = tokio::spawn(async move {
|
let event_handle = tokio::spawn(async move {
|
||||||
while let Some(event) = event_rx.recv().await {
|
while let Some(event) = event_rx.recv().await {
|
||||||
if verbose && !matches!(event, SenderHttpResponseEvent::ChunkReceived { .. }) {
|
if verbose {
|
||||||
println!("{}", event);
|
println!("{}", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let body_handle = tokio::task::spawn_blocking(move || {
|
|
||||||
let mut stdout = std::io::stdout();
|
|
||||||
while let Some(chunk) = body_chunk_rx.blocking_recv() {
|
|
||||||
if stdout.write_all(&chunk).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let _ = stdout.flush();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let response_dir = ctx.data_dir().join("responses");
|
let response_dir = ctx.data_dir().join("responses");
|
||||||
|
|
||||||
let result = send_http_request_by_id_with_plugins(SendHttpRequestByIdWithPluginsParams {
|
let result = send_http_request_by_id_with_plugins(SendHttpRequestByIdWithPluginsParams {
|
||||||
@@ -510,10 +450,9 @@ async fn send_http_request_by_id(
|
|||||||
request_id,
|
request_id,
|
||||||
environment_id: environment,
|
environment_id: environment,
|
||||||
update_source: UpdateSource::Sync,
|
update_source: UpdateSource::Sync,
|
||||||
cookie_jar_id,
|
cookie_jar_id: None,
|
||||||
response_dir: &response_dir,
|
response_dir: &response_dir,
|
||||||
emit_events_to: Some(event_tx),
|
emit_events_to: Some(event_tx),
|
||||||
emit_response_body_chunks_to: Some(body_chunk_tx),
|
|
||||||
plugin_manager: ctx.plugin_manager(),
|
plugin_manager: ctx.plugin_manager(),
|
||||||
encryption_manager: ctx.encryption_manager.clone(),
|
encryption_manager: ctx.encryption_manager.clone(),
|
||||||
plugin_context: &plugin_context,
|
plugin_context: &plugin_context,
|
||||||
@@ -523,26 +462,24 @@ async fn send_http_request_by_id(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let _ = event_handle.await;
|
let _ = event_handle.await;
|
||||||
let _ = body_handle.await;
|
let result = result.map_err(|e| e.to_string())?;
|
||||||
result.map_err(|e| e.to_string())?;
|
|
||||||
|
if verbose {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"HTTP {} {}",
|
||||||
|
result.response.status,
|
||||||
|
result.response.status_reason.as_deref().unwrap_or("")
|
||||||
|
);
|
||||||
|
if verbose {
|
||||||
|
for header in &result.response.headers {
|
||||||
|
println!("{}: {}", header.name, header.value);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
let body = String::from_utf8(result.response_body)
|
||||||
|
.map_err(|e| format!("Failed to read response body: {e}"))?;
|
||||||
|
println!("{}", body);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_cookie_jar_id(
|
|
||||||
ctx: &CliContext,
|
|
||||||
workspace_id: &str,
|
|
||||||
explicit_cookie_jar_id: Option<&str>,
|
|
||||||
) -> Result<Option<String>, String> {
|
|
||||||
if let Some(cookie_jar_id) = explicit_cookie_jar_id {
|
|
||||||
return Ok(Some(cookie_jar_id.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let default_cookie_jar = ctx
|
|
||||||
.db()
|
|
||||||
.list_cookie_jars(workspace_id)
|
|
||||||
.map_err(|e| format!("Failed to list cookie jars: {e}"))?
|
|
||||||
.into_iter()
|
|
||||||
.min_by_key(|jar| jar.created_at)
|
|
||||||
.map(|jar| jar.id);
|
|
||||||
Ok(default_cookie_jar)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use crate::cli::SendArgs;
|
|||||||
use crate::commands::request;
|
use crate::commands::request;
|
||||||
use crate::context::CliContext;
|
use crate::context::CliContext;
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
|
||||||
|
|
||||||
enum ExecutionMode {
|
enum ExecutionMode {
|
||||||
Sequential,
|
Sequential,
|
||||||
@@ -13,10 +12,9 @@ pub async fn run(
|
|||||||
ctx: &CliContext,
|
ctx: &CliContext,
|
||||||
args: SendArgs,
|
args: SendArgs,
|
||||||
environment: Option<&str>,
|
environment: Option<&str>,
|
||||||
cookie_jar_id: Option<&str>,
|
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
match send_target(ctx, args, environment, cookie_jar_id, verbose).await {
|
match send_target(ctx, args, environment, verbose).await {
|
||||||
Ok(()) => 0,
|
Ok(()) => 0,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("Error: {error}");
|
eprintln!("Error: {error}");
|
||||||
@@ -29,70 +27,30 @@ async fn send_target(
|
|||||||
ctx: &CliContext,
|
ctx: &CliContext,
|
||||||
args: SendArgs,
|
args: SendArgs,
|
||||||
environment: Option<&str>,
|
environment: Option<&str>,
|
||||||
cookie_jar_id: Option<&str>,
|
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mode = if args.parallel { ExecutionMode::Parallel } else { ExecutionMode::Sequential };
|
let mode = if args.parallel { ExecutionMode::Parallel } else { ExecutionMode::Sequential };
|
||||||
|
|
||||||
if let Ok(request) = ctx.db().get_any_request(&args.id) {
|
if ctx.db().get_any_request(&args.id).is_ok() {
|
||||||
let workspace_id = match &request {
|
return request::send_request_by_id(ctx, &args.id, environment, verbose).await;
|
||||||
AnyRequest::HttpRequest(r) => r.workspace_id.clone(),
|
|
||||||
AnyRequest::GrpcRequest(r) => r.workspace_id.clone(),
|
|
||||||
AnyRequest::WebsocketRequest(r) => r.workspace_id.clone(),
|
|
||||||
};
|
|
||||||
let resolved_cookie_jar_id =
|
|
||||||
request::resolve_cookie_jar_id(ctx, &workspace_id, cookie_jar_id)?;
|
|
||||||
|
|
||||||
return request::send_request_by_id(
|
|
||||||
ctx,
|
|
||||||
&args.id,
|
|
||||||
environment,
|
|
||||||
resolved_cookie_jar_id.as_deref(),
|
|
||||||
verbose,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(folder) = ctx.db().get_folder(&args.id) {
|
if ctx.db().get_folder(&args.id).is_ok() {
|
||||||
let resolved_cookie_jar_id =
|
|
||||||
request::resolve_cookie_jar_id(ctx, &folder.workspace_id, cookie_jar_id)?;
|
|
||||||
|
|
||||||
let request_ids = collect_folder_request_ids(ctx, &args.id)?;
|
let request_ids = collect_folder_request_ids(ctx, &args.id)?;
|
||||||
if request_ids.is_empty() {
|
if request_ids.is_empty() {
|
||||||
println!("No requests found in folder {}", args.id);
|
println!("No requests found in folder {}", args.id);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
return send_many(
|
return send_many(ctx, request_ids, mode, args.fail_fast, environment, verbose).await;
|
||||||
ctx,
|
|
||||||
request_ids,
|
|
||||||
mode,
|
|
||||||
args.fail_fast,
|
|
||||||
environment,
|
|
||||||
resolved_cookie_jar_id.as_deref(),
|
|
||||||
verbose,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(workspace) = ctx.db().get_workspace(&args.id) {
|
if ctx.db().get_workspace(&args.id).is_ok() {
|
||||||
let resolved_cookie_jar_id =
|
|
||||||
request::resolve_cookie_jar_id(ctx, &workspace.id, cookie_jar_id)?;
|
|
||||||
|
|
||||||
let request_ids = collect_workspace_request_ids(ctx, &args.id)?;
|
let request_ids = collect_workspace_request_ids(ctx, &args.id)?;
|
||||||
if request_ids.is_empty() {
|
if request_ids.is_empty() {
|
||||||
println!("No requests found in workspace {}", args.id);
|
println!("No requests found in workspace {}", args.id);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
return send_many(
|
return send_many(ctx, request_ids, mode, args.fail_fast, environment, verbose).await;
|
||||||
ctx,
|
|
||||||
request_ids,
|
|
||||||
mode,
|
|
||||||
args.fail_fast,
|
|
||||||
environment,
|
|
||||||
resolved_cookie_jar_id.as_deref(),
|
|
||||||
verbose,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(format!("Could not resolve ID '{}' as request, folder, or workspace", args.id))
|
Err(format!("Could not resolve ID '{}' as request, folder, or workspace", args.id))
|
||||||
@@ -173,7 +131,6 @@ async fn send_many(
|
|||||||
mode: ExecutionMode,
|
mode: ExecutionMode,
|
||||||
fail_fast: bool,
|
fail_fast: bool,
|
||||||
environment: Option<&str>,
|
environment: Option<&str>,
|
||||||
cookie_jar_id: Option<&str>,
|
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut success_count = 0usize;
|
let mut success_count = 0usize;
|
||||||
@@ -182,15 +139,7 @@ async fn send_many(
|
|||||||
match mode {
|
match mode {
|
||||||
ExecutionMode::Sequential => {
|
ExecutionMode::Sequential => {
|
||||||
for request_id in request_ids {
|
for request_id in request_ids {
|
||||||
match request::send_request_by_id(
|
match request::send_request_by_id(ctx, &request_id, environment, verbose).await {
|
||||||
ctx,
|
|
||||||
&request_id,
|
|
||||||
environment,
|
|
||||||
cookie_jar_id,
|
|
||||||
verbose,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => success_count += 1,
|
Ok(()) => success_count += 1,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
failures.push((request_id, error));
|
failures.push((request_id, error));
|
||||||
@@ -207,14 +156,7 @@ async fn send_many(
|
|||||||
.map(|request_id| async move {
|
.map(|request_id| async move {
|
||||||
(
|
(
|
||||||
request_id.clone(),
|
request_id.clone(),
|
||||||
request::send_request_by_id(
|
request::send_request_by_id(ctx, request_id, environment, verbose).await,
|
||||||
ctx,
|
|
||||||
request_id,
|
|
||||||
environment,
|
|
||||||
cookie_jar_id,
|
|
||||||
verbose,
|
|
||||||
)
|
|
||||||
.await,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use crate::utils::confirm::confirm_delete;
|
|||||||
use crate::utils::json::{
|
use crate::utils::json::{
|
||||||
apply_merge_patch, parse_optional_json, parse_required_json, require_id, validate_create_id,
|
apply_merge_patch, parse_optional_json, parse_required_json, require_id, validate_create_id,
|
||||||
};
|
};
|
||||||
use crate::utils::schema::append_agent_hints;
|
|
||||||
use schemars::schema_for;
|
|
||||||
use yaak_models::models::Workspace;
|
use yaak_models::models::Workspace;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -14,7 +12,6 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
|||||||
pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 {
|
pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 {
|
||||||
let result = match args.command {
|
let result = match args.command {
|
||||||
WorkspaceCommands::List => list(ctx),
|
WorkspaceCommands::List => list(ctx),
|
||||||
WorkspaceCommands::Schema { pretty } => schema(pretty),
|
|
||||||
WorkspaceCommands::Show { workspace_id } => show(ctx, &workspace_id),
|
WorkspaceCommands::Show { workspace_id } => show(ctx, &workspace_id),
|
||||||
WorkspaceCommands::Create { name, json, json_input } => create(ctx, name, json, json_input),
|
WorkspaceCommands::Create { name, json, json_input } => create(ctx, name, json, json_input),
|
||||||
WorkspaceCommands::Update { json, json_input } => update(ctx, json, json_input),
|
WorkspaceCommands::Update { json, json_input } => update(ctx, json, json_input),
|
||||||
@@ -30,18 +27,6 @@ pub fn run(ctx: &CliContext, args: WorkspaceArgs) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schema(pretty: bool) -> CommandResult {
|
|
||||||
let mut schema = serde_json::to_value(schema_for!(Workspace))
|
|
||||||
.map_err(|e| format!("Failed to serialize workspace schema: {e}"))?;
|
|
||||||
append_agent_hints(&mut schema);
|
|
||||||
|
|
||||||
let output =
|
|
||||||
if pretty { serde_json::to_string_pretty(&schema) } else { serde_json::to_string(&schema) }
|
|
||||||
.map_err(|e| format!("Failed to format workspace schema JSON: {e}"))?;
|
|
||||||
println!("{output}");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn list(ctx: &CliContext) -> CommandResult {
|
fn list(ctx: &CliContext) -> CommandResult {
|
||||||
let workspaces =
|
let workspaces =
|
||||||
ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?;
|
ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use crate::plugin_events::CliPluginEventBridge;
|
use crate::plugin_events::CliPluginEventBridge;
|
||||||
use include_dir::{Dir, include_dir};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -11,21 +9,6 @@ use yaak_models::query_manager::QueryManager;
|
|||||||
use yaak_plugins::events::PluginContext;
|
use yaak_plugins::events::PluginContext;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
const EMBEDDED_PLUGIN_RUNTIME: &str = include_str!(concat!(
|
|
||||||
env!("CARGO_MANIFEST_DIR"),
|
|
||||||
"/../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs"
|
|
||||||
));
|
|
||||||
static EMBEDDED_VENDORED_PLUGINS: Dir<'_> =
|
|
||||||
include_dir!("$CARGO_MANIFEST_DIR/../../crates-tauri/yaak-app/vendored/plugins");
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct CliExecutionContext {
|
|
||||||
pub request_id: Option<String>,
|
|
||||||
pub workspace_id: Option<String>,
|
|
||||||
pub environment_id: Option<String>,
|
|
||||||
pub cookie_jar_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CliContext {
|
pub struct CliContext {
|
||||||
data_dir: PathBuf,
|
data_dir: PathBuf,
|
||||||
query_manager: QueryManager,
|
query_manager: QueryManager,
|
||||||
@@ -36,71 +19,68 @@ pub struct CliContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CliContext {
|
impl CliContext {
|
||||||
pub fn new(data_dir: PathBuf, app_id: &str) -> Self {
|
pub async fn initialize(data_dir: PathBuf, app_id: &str, with_plugins: bool) -> Self {
|
||||||
let db_path = data_dir.join("db.sqlite");
|
let db_path = data_dir.join("db.sqlite");
|
||||||
let blob_path = data_dir.join("blobs.sqlite");
|
let blob_path = data_dir.join("blobs.sqlite");
|
||||||
let (query_manager, blob_manager, _rx) =
|
|
||||||
match yaak_models::init_standalone(&db_path, &blob_path) {
|
let (query_manager, blob_manager, _rx) = yaak_models::init_standalone(&db_path, &blob_path)
|
||||||
Ok(v) => v,
|
.expect("Failed to initialize database");
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Error: Failed to initialize database: {err}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||||
|
|
||||||
|
let plugin_manager = if with_plugins {
|
||||||
|
let vendored_plugin_dir = data_dir.join("vendored-plugins");
|
||||||
|
let installed_plugin_dir = data_dir.join("installed-plugins");
|
||||||
|
let node_bin_path = PathBuf::from("node");
|
||||||
|
|
||||||
|
let plugin_runtime_main =
|
||||||
|
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../crates-tauri/yaak-app/vendored/plugin-runtime/index.cjs")
|
||||||
|
});
|
||||||
|
|
||||||
|
let plugin_manager = Arc::new(
|
||||||
|
PluginManager::new(
|
||||||
|
vendored_plugin_dir,
|
||||||
|
installed_plugin_dir,
|
||||||
|
node_bin_path,
|
||||||
|
plugin_runtime_main,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
|
|
||||||
|
let plugins = query_manager.connect().list_plugins().unwrap_or_default();
|
||||||
|
if !plugins.is_empty() {
|
||||||
|
let errors = plugin_manager
|
||||||
|
.initialize_all_plugins(plugins, &PluginContext::new_empty())
|
||||||
|
.await;
|
||||||
|
for (plugin_dir, error_msg) in errors {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: Failed to initialize plugin '{}': {}",
|
||||||
|
plugin_dir, error_msg
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(plugin_manager)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let plugin_event_bridge = if let Some(plugin_manager) = &plugin_manager {
|
||||||
|
Some(CliPluginEventBridge::start(plugin_manager.clone(), query_manager.clone()).await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
data_dir,
|
data_dir,
|
||||||
query_manager,
|
query_manager,
|
||||||
blob_manager,
|
blob_manager,
|
||||||
encryption_manager,
|
encryption_manager,
|
||||||
plugin_manager: None,
|
plugin_manager,
|
||||||
plugin_event_bridge: Mutex::new(None),
|
plugin_event_bridge: Mutex::new(plugin_event_bridge),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init_plugins(&mut self, execution_context: CliExecutionContext) {
|
|
||||||
let vendored_plugin_dir = self.data_dir.join("vendored-plugins");
|
|
||||||
let installed_plugin_dir = self.data_dir.join("installed-plugins");
|
|
||||||
let node_bin_path = PathBuf::from("node");
|
|
||||||
|
|
||||||
prepare_embedded_vendored_plugins(&vendored_plugin_dir)
|
|
||||||
.expect("Failed to prepare bundled plugins");
|
|
||||||
|
|
||||||
let plugin_runtime_main =
|
|
||||||
std::env::var("YAAK_PLUGIN_RUNTIME").map(PathBuf::from).unwrap_or_else(|_| {
|
|
||||||
prepare_embedded_plugin_runtime(&self.data_dir)
|
|
||||||
.expect("Failed to prepare embedded plugin runtime")
|
|
||||||
});
|
|
||||||
|
|
||||||
match PluginManager::new(
|
|
||||||
vendored_plugin_dir,
|
|
||||||
installed_plugin_dir,
|
|
||||||
node_bin_path,
|
|
||||||
plugin_runtime_main,
|
|
||||||
&self.query_manager,
|
|
||||||
&PluginContext::new_empty(),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(plugin_manager) => {
|
|
||||||
let plugin_manager = Arc::new(plugin_manager);
|
|
||||||
let plugin_event_bridge = CliPluginEventBridge::start(
|
|
||||||
plugin_manager.clone(),
|
|
||||||
self.query_manager.clone(),
|
|
||||||
self.blob_manager.clone(),
|
|
||||||
self.encryption_manager.clone(),
|
|
||||||
self.data_dir.clone(),
|
|
||||||
execution_context,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
self.plugin_manager = Some(plugin_manager);
|
|
||||||
*self.plugin_event_bridge.lock().await = Some(plugin_event_bridge);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("Warning: Failed to initialize plugins: {err}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,17 +113,3 @@ impl CliContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_embedded_plugin_runtime(data_dir: &Path) -> std::io::Result<PathBuf> {
|
|
||||||
let runtime_dir = data_dir.join("vendored").join("plugin-runtime");
|
|
||||||
fs::create_dir_all(&runtime_dir)?;
|
|
||||||
let runtime_main = runtime_dir.join("index.cjs");
|
|
||||||
fs::write(&runtime_main, EMBEDDED_PLUGIN_RUNTIME)?;
|
|
||||||
Ok(runtime_main)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_embedded_vendored_plugins(vendored_plugin_dir: &Path) -> std::io::Result<()> {
|
|
||||||
fs::create_dir_all(vendored_plugin_dir)?;
|
|
||||||
EMBEDDED_VENDORED_PLUGINS.extract(vendored_plugin_dir)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,282 +2,51 @@ mod cli;
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod context;
|
mod context;
|
||||||
mod plugin_events;
|
mod plugin_events;
|
||||||
mod ui;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
mod version;
|
|
||||||
mod version_check;
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::{Cli, Commands, PluginCommands, RequestCommands};
|
use cli::{Cli, Commands, RequestCommands};
|
||||||
use context::{CliContext, CliExecutionContext};
|
use context::CliContext;
|
||||||
use std::path::PathBuf;
|
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let Cli { data_dir, environment, cookie_jar, verbose, log, command } = Cli::parse();
|
let Cli { data_dir, environment, verbose, command } = Cli::parse();
|
||||||
|
|
||||||
if let Some(log_level) = log {
|
if verbose {
|
||||||
match log_level {
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
Some(level) => {
|
|
||||||
env_logger::Builder::new().filter_level(level.as_filter()).init();
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
|
||||||
.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
let app_id = if cfg!(debug_assertions) { "app.yaak.desktop.dev" } else { "app.yaak.desktop" };
|
||||||
|
|
||||||
let data_dir = data_dir.unwrap_or_else(|| resolve_data_dir(app_id));
|
let data_dir = data_dir.unwrap_or_else(|| {
|
||||||
|
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
||||||
|
});
|
||||||
|
|
||||||
version_check::maybe_check_for_updates().await;
|
let needs_plugins = matches!(
|
||||||
|
&command,
|
||||||
|
Commands::Send(_)
|
||||||
|
| Commands::Request(cli::RequestArgs {
|
||||||
|
command: RequestCommands::Send { .. } | RequestCommands::Schema { .. },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let context = CliContext::initialize(data_dir, app_id, needs_plugins).await;
|
||||||
|
|
||||||
let exit_code = match command {
|
let exit_code = match command {
|
||||||
Commands::Auth(args) => commands::auth::run(args).await,
|
|
||||||
Commands::Plugin(args) => match args.command {
|
|
||||||
PluginCommands::Build(args) => commands::plugin::run_build(args).await,
|
|
||||||
PluginCommands::Dev(args) => commands::plugin::run_dev(args).await,
|
|
||||||
PluginCommands::Generate(args) => commands::plugin::run_generate(args).await,
|
|
||||||
PluginCommands::Publish(args) => commands::plugin::run_publish(args).await,
|
|
||||||
PluginCommands::Install(install_args) => {
|
|
||||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
|
||||||
context.init_plugins(CliExecutionContext::default()).await;
|
|
||||||
let exit_code = commands::plugin::run_install(&context, install_args).await;
|
|
||||||
context.shutdown().await;
|
|
||||||
exit_code
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Commands::Build(args) => commands::plugin::run_build(args).await,
|
|
||||||
Commands::Dev(args) => commands::plugin::run_dev(args).await,
|
|
||||||
Commands::Generate(args) => commands::plugin::run_generate(args).await,
|
|
||||||
Commands::Publish(args) => commands::plugin::run_publish(args).await,
|
|
||||||
Commands::Send(args) => {
|
Commands::Send(args) => {
|
||||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
commands::send::run(&context, args, environment.as_deref(), verbose).await
|
||||||
match resolve_send_execution_context(
|
|
||||||
&context,
|
|
||||||
&args.id,
|
|
||||||
environment.as_deref(),
|
|
||||||
cookie_jar.as_deref(),
|
|
||||||
) {
|
|
||||||
Ok(execution_context) => {
|
|
||||||
context.init_plugins(execution_context).await;
|
|
||||||
let exit_code = commands::send::run(
|
|
||||||
&context,
|
|
||||||
args,
|
|
||||||
environment.as_deref(),
|
|
||||||
cookie_jar.as_deref(),
|
|
||||||
verbose,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
context.shutdown().await;
|
|
||||||
exit_code
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Error: {error}");
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Commands::CookieJar(args) => {
|
|
||||||
let context = CliContext::new(data_dir.clone(), app_id);
|
|
||||||
let exit_code = commands::cookie_jar::run(&context, args);
|
|
||||||
context.shutdown().await;
|
|
||||||
exit_code
|
|
||||||
}
|
|
||||||
Commands::Workspace(args) => {
|
|
||||||
let context = CliContext::new(data_dir.clone(), app_id);
|
|
||||||
let exit_code = commands::workspace::run(&context, args);
|
|
||||||
context.shutdown().await;
|
|
||||||
exit_code
|
|
||||||
}
|
}
|
||||||
|
Commands::Workspace(args) => commands::workspace::run(&context, args),
|
||||||
Commands::Request(args) => {
|
Commands::Request(args) => {
|
||||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
commands::request::run(&context, args, environment.as_deref(), verbose).await
|
||||||
let execution_context_result = match &args.command {
|
|
||||||
RequestCommands::Send { request_id } => resolve_request_execution_context(
|
|
||||||
&context,
|
|
||||||
request_id,
|
|
||||||
environment.as_deref(),
|
|
||||||
cookie_jar.as_deref(),
|
|
||||||
),
|
|
||||||
_ => Ok(CliExecutionContext::default()),
|
|
||||||
};
|
|
||||||
match execution_context_result {
|
|
||||||
Ok(execution_context) => {
|
|
||||||
let with_plugins = matches!(
|
|
||||||
&args.command,
|
|
||||||
RequestCommands::Send { .. } | RequestCommands::Schema { .. }
|
|
||||||
);
|
|
||||||
if with_plugins {
|
|
||||||
context.init_plugins(execution_context).await;
|
|
||||||
}
|
|
||||||
let exit_code = commands::request::run(
|
|
||||||
&context,
|
|
||||||
args,
|
|
||||||
environment.as_deref(),
|
|
||||||
cookie_jar.as_deref(),
|
|
||||||
verbose,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
context.shutdown().await;
|
|
||||||
exit_code
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
eprintln!("Error: {error}");
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Commands::Folder(args) => {
|
|
||||||
let context = CliContext::new(data_dir.clone(), app_id);
|
|
||||||
let exit_code = commands::folder::run(&context, args);
|
|
||||||
context.shutdown().await;
|
|
||||||
exit_code
|
|
||||||
}
|
|
||||||
Commands::Environment(args) => {
|
|
||||||
let context = CliContext::new(data_dir.clone(), app_id);
|
|
||||||
let exit_code = commands::environment::run(&context, args);
|
|
||||||
context.shutdown().await;
|
|
||||||
exit_code
|
|
||||||
}
|
}
|
||||||
|
Commands::Folder(args) => commands::folder::run(&context, args),
|
||||||
|
Commands::Environment(args) => commands::environment::run(&context, args),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
context.shutdown().await;
|
||||||
|
|
||||||
if exit_code != 0 {
|
if exit_code != 0 {
|
||||||
std::process::exit(exit_code);
|
std::process::exit(exit_code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_send_execution_context(
|
|
||||||
context: &CliContext,
|
|
||||||
id: &str,
|
|
||||||
environment: Option<&str>,
|
|
||||||
explicit_cookie_jar_id: Option<&str>,
|
|
||||||
) -> Result<CliExecutionContext, String> {
|
|
||||||
if let Ok(request) = context.db().get_any_request(id) {
|
|
||||||
let (request_id, workspace_id) = match request {
|
|
||||||
AnyRequest::HttpRequest(r) => (Some(r.id), r.workspace_id),
|
|
||||||
AnyRequest::GrpcRequest(r) => (Some(r.id), r.workspace_id),
|
|
||||||
AnyRequest::WebsocketRequest(r) => (Some(r.id), r.workspace_id),
|
|
||||||
};
|
|
||||||
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace_id, explicit_cookie_jar_id)?;
|
|
||||||
return Ok(CliExecutionContext {
|
|
||||||
request_id,
|
|
||||||
workspace_id: Some(workspace_id),
|
|
||||||
environment_id: environment.map(str::to_string),
|
|
||||||
cookie_jar_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(folder) = context.db().get_folder(id) {
|
|
||||||
let cookie_jar_id =
|
|
||||||
resolve_cookie_jar_id(context, &folder.workspace_id, explicit_cookie_jar_id)?;
|
|
||||||
return Ok(CliExecutionContext {
|
|
||||||
request_id: None,
|
|
||||||
workspace_id: Some(folder.workspace_id),
|
|
||||||
environment_id: environment.map(str::to_string),
|
|
||||||
cookie_jar_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(workspace) = context.db().get_workspace(id) {
|
|
||||||
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace.id, explicit_cookie_jar_id)?;
|
|
||||||
return Ok(CliExecutionContext {
|
|
||||||
request_id: None,
|
|
||||||
workspace_id: Some(workspace.id),
|
|
||||||
environment_id: environment.map(str::to_string),
|
|
||||||
cookie_jar_id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(format!("Could not resolve ID '{}' as request, folder, or workspace", id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_request_execution_context(
|
|
||||||
context: &CliContext,
|
|
||||||
request_id: &str,
|
|
||||||
environment: Option<&str>,
|
|
||||||
explicit_cookie_jar_id: Option<&str>,
|
|
||||||
) -> Result<CliExecutionContext, String> {
|
|
||||||
let request = context
|
|
||||||
.db()
|
|
||||||
.get_any_request(request_id)
|
|
||||||
.map_err(|e| format!("Failed to get request: {e}"))?;
|
|
||||||
|
|
||||||
let workspace_id = match request {
|
|
||||||
AnyRequest::HttpRequest(r) => r.workspace_id,
|
|
||||||
AnyRequest::GrpcRequest(r) => r.workspace_id,
|
|
||||||
AnyRequest::WebsocketRequest(r) => r.workspace_id,
|
|
||||||
};
|
|
||||||
let cookie_jar_id = resolve_cookie_jar_id(context, &workspace_id, explicit_cookie_jar_id)?;
|
|
||||||
|
|
||||||
Ok(CliExecutionContext {
|
|
||||||
request_id: Some(request_id.to_string()),
|
|
||||||
workspace_id: Some(workspace_id),
|
|
||||||
environment_id: environment.map(str::to_string),
|
|
||||||
cookie_jar_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_cookie_jar_id(
|
|
||||||
context: &CliContext,
|
|
||||||
workspace_id: &str,
|
|
||||||
explicit_cookie_jar_id: Option<&str>,
|
|
||||||
) -> Result<Option<String>, String> {
|
|
||||||
if let Some(cookie_jar_id) = explicit_cookie_jar_id {
|
|
||||||
return Ok(Some(cookie_jar_id.to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let default_cookie_jar = context
|
|
||||||
.db()
|
|
||||||
.list_cookie_jars(workspace_id)
|
|
||||||
.map_err(|e| format!("Failed to list cookie jars: {e}"))?
|
|
||||||
.into_iter()
|
|
||||||
.min_by_key(|jar| jar.created_at)
|
|
||||||
.map(|jar| jar.id);
|
|
||||||
Ok(default_cookie_jar)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_data_dir(app_id: &str) -> PathBuf {
|
|
||||||
if let Some(dir) = wsl_data_dir(app_id) {
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
dirs::data_dir().expect("Could not determine data directory").join(app_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect WSL and resolve the Windows AppData\Roaming path for the Yaak data directory.
|
|
||||||
fn wsl_data_dir(app_id: &str) -> Option<PathBuf> {
|
|
||||||
if !cfg!(target_os = "linux") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let proc_version = std::fs::read_to_string("/proc/version").ok()?;
|
|
||||||
let is_wsl = proc_version.to_lowercase().contains("microsoft");
|
|
||||||
if !is_wsl {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're in WSL, so try to resolve the Yaak app's data directory in Windows
|
|
||||||
|
|
||||||
// Get the Windows %APPDATA% path via cmd.exe
|
|
||||||
let appdata_output =
|
|
||||||
std::process::Command::new("cmd.exe").args(["/C", "echo", "%APPDATA%"]).output().ok()?;
|
|
||||||
|
|
||||||
let win_path = String::from_utf8(appdata_output.stdout).ok()?.trim().to_string();
|
|
||||||
if win_path.is_empty() || win_path == "%APPDATA%" {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert Windows path to WSL path using wslpath (handles custom mount points)
|
|
||||||
let wslpath_output = std::process::Command::new("wslpath").arg(&win_path).output().ok()?;
|
|
||||||
|
|
||||||
let wsl_appdata = String::from_utf8(wslpath_output.stdout).ok()?.trim().to_string();
|
|
||||||
if wsl_appdata.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let wsl_path = PathBuf::from(wsl_appdata).join(app_id);
|
|
||||||
|
|
||||||
if wsl_path.exists() { Some(wsl_path) } else { None }
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,42 +0,0 @@
|
|||||||
use console::style;
|
|
||||||
use std::io::{self, IsTerminal};
|
|
||||||
|
|
||||||
pub fn info(message: &str) {
|
|
||||||
if io::stdout().is_terminal() {
|
|
||||||
println!("{:<8} {}", style("INFO").cyan().bold(), style(message).cyan());
|
|
||||||
} else {
|
|
||||||
println!("INFO {message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn warning(message: &str) {
|
|
||||||
if io::stdout().is_terminal() {
|
|
||||||
println!("{:<8} {}", style("WARNING").yellow().bold(), style(message).yellow());
|
|
||||||
} else {
|
|
||||||
println!("WARNING {message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn warning_stderr(message: &str) {
|
|
||||||
if io::stderr().is_terminal() {
|
|
||||||
eprintln!("{:<8} {}", style("WARNING").yellow().bold(), style(message).yellow());
|
|
||||||
} else {
|
|
||||||
eprintln!("WARNING {message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn success(message: &str) {
|
|
||||||
if io::stdout().is_terminal() {
|
|
||||||
println!("{:<8} {}", style("SUCCESS").green().bold(), style(message).green());
|
|
||||||
} else {
|
|
||||||
println!("SUCCESS {message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error(message: &str) {
|
|
||||||
if io::stderr().is_terminal() {
|
|
||||||
eprintln!("{:<8} {}", style("ERROR").red().bold(), style(message).red());
|
|
||||||
} else {
|
|
||||||
eprintln!("Error: {message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
use reqwest::Client;
|
|
||||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT};
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
pub fn build_client(session_token: Option<&str>) -> Result<Client, String> {
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
let user_agent = HeaderValue::from_str(&user_agent())
|
|
||||||
.map_err(|e| format!("Failed to build user-agent header: {e}"))?;
|
|
||||||
headers.insert(USER_AGENT, user_agent);
|
|
||||||
|
|
||||||
if let Some(token) = session_token {
|
|
||||||
let token_value = HeaderValue::from_str(token)
|
|
||||||
.map_err(|e| format!("Failed to build session header: {e}"))?;
|
|
||||||
headers.insert(HeaderName::from_static("x-yaak-session"), token_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
Client::builder()
|
|
||||||
.default_headers(headers)
|
|
||||||
.build()
|
|
||||||
.map_err(|e| format!("Failed to initialize HTTP client: {e}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_api_error(status: u16, body: &str) -> String {
|
|
||||||
if let Ok(value) = serde_json::from_str::<Value>(body) {
|
|
||||||
if let Some(message) = value.get("message").and_then(Value::as_str) {
|
|
||||||
return message.to_string();
|
|
||||||
}
|
|
||||||
if let Some(error) = value.get("error").and_then(Value::as_str) {
|
|
||||||
return error.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
format!("API error {status}: {body}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn user_agent() -> String {
|
|
||||||
format!("YaakCli/{} ({})", crate::version::cli_version(), ua_platform())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ua_platform() -> &'static str {
|
|
||||||
match std::env::consts::OS {
|
|
||||||
"windows" => "Win",
|
|
||||||
"darwin" => "Mac",
|
|
||||||
"linux" => "Linux",
|
|
||||||
_ => "Unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,30 +63,6 @@ pub fn validate_create_id(payload: &Value, context: &str) -> JsonResult<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge_workspace_id_arg(
|
|
||||||
workspace_id_from_arg: Option<&str>,
|
|
||||||
payload_workspace_id: &mut String,
|
|
||||||
context: &str,
|
|
||||||
) -> JsonResult<()> {
|
|
||||||
if let Some(workspace_id_arg) = workspace_id_from_arg {
|
|
||||||
if payload_workspace_id.is_empty() {
|
|
||||||
*payload_workspace_id = workspace_id_arg.to_string();
|
|
||||||
} else if payload_workspace_id != workspace_id_arg {
|
|
||||||
return Err(format!(
|
|
||||||
"{context} got conflicting workspace_id values between positional arg and JSON payload"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload_workspace_id.is_empty() {
|
|
||||||
return Err(format!(
|
|
||||||
"{context} requires non-empty \"workspaceId\" in JSON payload or positional workspace_id"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_merge_patch<T>(existing: &T, patch: &Value, id: &str, context: &str) -> JsonResult<T>
|
pub fn apply_merge_patch<T>(existing: &T, patch: &Value, id: &str, context: &str) -> JsonResult<T>
|
||||||
where
|
where
|
||||||
T: Serialize + DeserializeOwned,
|
T: Serialize + DeserializeOwned,
|
||||||
|
|||||||
@@ -1,5 +1,2 @@
|
|||||||
pub mod confirm;
|
pub mod confirm;
|
||||||
pub mod http;
|
|
||||||
pub mod json;
|
pub mod json;
|
||||||
pub mod schema;
|
|
||||||
pub mod workspace;
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
use serde_json::{Value, json};
|
|
||||||
|
|
||||||
pub fn append_agent_hints(schema: &mut Value) {
|
|
||||||
let Some(schema_obj) = schema.as_object_mut() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
schema_obj.insert(
|
|
||||||
"x-yaak-agent-hints".to_string(),
|
|
||||||
json!({
|
|
||||||
"templateVariableSyntax": "${[ my_var ]}",
|
|
||||||
"templateFunctionSyntax": "${[ namespace.my_func(a='aaa',b='bbb') ]}",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
use crate::context::CliContext;
|
|
||||||
|
|
||||||
pub fn resolve_workspace_id(
|
|
||||||
ctx: &CliContext,
|
|
||||||
workspace_id: Option<&str>,
|
|
||||||
command_name: &str,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
if let Some(workspace_id) = workspace_id {
|
|
||||||
return Ok(workspace_id.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let workspaces =
|
|
||||||
ctx.db().list_workspaces().map_err(|e| format!("Failed to list workspaces: {e}"))?;
|
|
||||||
match workspaces.as_slice() {
|
|
||||||
[] => Err(format!("No workspaces found. {command_name} requires a workspace ID.")),
|
|
||||||
[workspace] => Ok(workspace.id.clone()),
|
|
||||||
_ => Err(format!("Multiple workspaces found. {command_name} requires a workspace ID.")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub fn cli_version() -> &'static str {
|
|
||||||
option_env!("YAAK_CLI_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
use crate::ui;
|
|
||||||
use crate::version;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fs;
|
|
||||||
use std::io::IsTerminal;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
||||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
|
||||||
|
|
||||||
const CACHE_FILE_NAME: &str = "cli-version-check.json";
|
|
||||||
const CHECK_INTERVAL_SECS: u64 = 24 * 60 * 60;
|
|
||||||
const REQUEST_TIMEOUT: Duration = Duration::from_millis(800);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
#[serde(default)]
|
|
||||||
struct VersionCheckResponse {
|
|
||||||
outdated: bool,
|
|
||||||
latest_version: Option<String>,
|
|
||||||
upgrade_hint: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(default)]
|
|
||||||
struct CacheRecord {
|
|
||||||
checked_at_epoch_secs: u64,
|
|
||||||
response: VersionCheckResponse,
|
|
||||||
last_warned_at_epoch_secs: Option<u64>,
|
|
||||||
last_warned_version: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CacheRecord {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
checked_at_epoch_secs: 0,
|
|
||||||
response: VersionCheckResponse::default(),
|
|
||||||
last_warned_at_epoch_secs: None,
|
|
||||||
last_warned_version: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
struct VersionCheckRequest<'a> {
|
|
||||||
current_version: &'a str,
|
|
||||||
channel: String,
|
|
||||||
install_source: String,
|
|
||||||
platform: &'a str,
|
|
||||||
arch: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn maybe_check_for_updates() {
|
|
||||||
if should_skip_check() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let now = unix_epoch_secs();
|
|
||||||
let cache_path = cache_path();
|
|
||||||
let cached = read_cache(&cache_path);
|
|
||||||
|
|
||||||
if let Some(cache) = cached.as_ref().filter(|c| !is_expired(c.checked_at_epoch_secs, now)) {
|
|
||||||
let mut record = cache.clone();
|
|
||||||
maybe_warn_outdated(&mut record, now);
|
|
||||||
write_cache(&cache_path, &record);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fresh = fetch_version_check().await;
|
|
||||||
match fresh {
|
|
||||||
Some(response) => {
|
|
||||||
let mut record = CacheRecord {
|
|
||||||
checked_at_epoch_secs: now,
|
|
||||||
response: response.clone(),
|
|
||||||
last_warned_at_epoch_secs: cached
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|c| c.last_warned_at_epoch_secs),
|
|
||||||
last_warned_version: cached.as_ref().and_then(|c| c.last_warned_version.clone()),
|
|
||||||
};
|
|
||||||
maybe_warn_outdated(&mut record, now);
|
|
||||||
write_cache(&cache_path, &record);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let fallback = cached.as_ref().map(|cache| cache.response.clone()).unwrap_or_default();
|
|
||||||
let mut record = CacheRecord {
|
|
||||||
checked_at_epoch_secs: now,
|
|
||||||
response: fallback,
|
|
||||||
last_warned_at_epoch_secs: cached
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|c| c.last_warned_at_epoch_secs),
|
|
||||||
last_warned_version: cached.as_ref().and_then(|c| c.last_warned_version.clone()),
|
|
||||||
};
|
|
||||||
maybe_warn_outdated(&mut record, now);
|
|
||||||
write_cache(&cache_path, &record);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_skip_check() -> bool {
|
|
||||||
if std::env::var("YAAK_CLI_NO_UPDATE_CHECK")
|
|
||||||
.is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if std::env::var("CI").is_ok() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
!std::io::stdout().is_terminal()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_version_check() -> Option<VersionCheckResponse> {
|
|
||||||
let api_url = format!("{}/cli/check", update_base_url());
|
|
||||||
let current_version = version::cli_version();
|
|
||||||
let payload = VersionCheckRequest {
|
|
||||||
current_version,
|
|
||||||
channel: release_channel(current_version),
|
|
||||||
install_source: install_source(),
|
|
||||||
platform: std::env::consts::OS,
|
|
||||||
arch: std::env::consts::ARCH,
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = yaak_api_client(ApiClientKind::Cli, current_version).ok()?;
|
|
||||||
let request = client.post(api_url).json(&payload);
|
|
||||||
|
|
||||||
let response = tokio::time::timeout(REQUEST_TIMEOUT, request.send()).await.ok()?.ok()?;
|
|
||||||
if !response.status().is_success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::timeout(REQUEST_TIMEOUT, response.json::<VersionCheckResponse>()).await.ok()?.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn release_channel(version: &str) -> String {
|
|
||||||
version
|
|
||||||
.split_once('-')
|
|
||||||
.and_then(|(_, suffix)| suffix.split('.').next())
|
|
||||||
.unwrap_or("stable")
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_source() -> String {
|
|
||||||
std::env::var("YAAK_CLI_INSTALL_SOURCE")
|
|
||||||
.ok()
|
|
||||||
.filter(|s| !s.trim().is_empty())
|
|
||||||
.unwrap_or_else(|| "source".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_base_url() -> &'static str {
|
|
||||||
match std::env::var("ENVIRONMENT").ok().as_deref() {
|
|
||||||
Some("development") => "http://localhost:9444",
|
|
||||||
_ => "https://update.yaak.app",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maybe_warn_outdated(record: &mut CacheRecord, now: u64) {
|
|
||||||
if !record.response.outdated {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let latest =
|
|
||||||
record.response.latest_version.clone().unwrap_or_else(|| "a newer release".to_string());
|
|
||||||
let warn_suppressed = record.last_warned_version.as_deref() == Some(latest.as_str())
|
|
||||||
&& record.last_warned_at_epoch_secs.is_some_and(|t| !is_expired(t, now));
|
|
||||||
if warn_suppressed {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let hint = record.response.upgrade_hint.clone().unwrap_or_else(default_upgrade_hint);
|
|
||||||
ui::warning_stderr(&format!("A newer Yaak CLI version is available ({latest}). {hint}"));
|
|
||||||
record.last_warned_version = Some(latest);
|
|
||||||
record.last_warned_at_epoch_secs = Some(now);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_upgrade_hint() -> String {
|
|
||||||
if install_source() == "npm" {
|
|
||||||
let channel = release_channel(version::cli_version());
|
|
||||||
if channel == "stable" {
|
|
||||||
return "Run `npm install -g @yaakapp/cli@latest` to update.".to_string();
|
|
||||||
}
|
|
||||||
return format!("Run `npm install -g @yaakapp/cli@{channel}` to update.");
|
|
||||||
}
|
|
||||||
|
|
||||||
"Update your Yaak CLI installation to the latest release.".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cache_path() -> PathBuf {
|
|
||||||
std::env::temp_dir().join("yaak-cli").join(format!("{}-{CACHE_FILE_NAME}", environment_name()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn environment_name() -> &'static str {
|
|
||||||
match std::env::var("ENVIRONMENT").ok().as_deref() {
|
|
||||||
Some("staging") => "staging",
|
|
||||||
Some("development") => "development",
|
|
||||||
_ => "production",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_cache(path: &Path) -> Option<CacheRecord> {
|
|
||||||
let contents = fs::read_to_string(path).ok()?;
|
|
||||||
serde_json::from_str::<CacheRecord>(&contents).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_cache(path: &Path, record: &CacheRecord) {
|
|
||||||
let Some(parent) = path.parent() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if fs::create_dir_all(parent).is_err() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Ok(json) = serde_json::to_string(record) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let _ = fs::write(path, json);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_expired(checked_at_epoch_secs: u64, now: u64) -> bool {
|
|
||||||
now.saturating_sub(checked_at_epoch_secs) >= CHECK_INTERVAL_SECS
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unix_epoch_secs() -> u64 {
|
|
||||||
SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_else(|_| Duration::from_secs(0))
|
|
||||||
.as_secs()
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::{SocketAddr, TcpListener, TcpStream};
|
use std::net::TcpListener;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub struct TestHttpServer {
|
pub struct TestHttpServer {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
addr: SocketAddr,
|
|
||||||
shutdown: Arc<AtomicBool>,
|
|
||||||
handle: Option<thread::JoinHandle<()>>,
|
handle: Option<thread::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,46 +12,29 @@ impl TestHttpServer {
|
|||||||
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind test HTTP server");
|
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind test HTTP server");
|
||||||
let addr = listener.local_addr().expect("Failed to get local addr");
|
let addr = listener.local_addr().expect("Failed to get local addr");
|
||||||
let url = format!("http://{addr}/test");
|
let url = format!("http://{addr}/test");
|
||||||
listener.set_nonblocking(true).expect("Failed to set test server listener nonblocking");
|
|
||||||
|
|
||||||
let shutdown = Arc::new(AtomicBool::new(false));
|
|
||||||
let shutdown_signal = Arc::clone(&shutdown);
|
|
||||||
let body_bytes = body.as_bytes().to_vec();
|
let body_bytes = body.as_bytes().to_vec();
|
||||||
|
|
||||||
let handle = thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
while !shutdown_signal.load(Ordering::Relaxed) {
|
if let Ok((mut stream, _)) = listener.accept() {
|
||||||
match listener.accept() {
|
let mut request_buf = [0u8; 4096];
|
||||||
Ok((mut stream, _)) => {
|
let _ = stream.read(&mut request_buf);
|
||||||
let _ = stream.set_read_timeout(Some(Duration::from_secs(1)));
|
|
||||||
let mut request_buf = [0u8; 4096];
|
|
||||||
let _ = stream.read(&mut request_buf);
|
|
||||||
|
|
||||||
let response = format!(
|
let response = format!(
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||||
body_bytes.len()
|
body_bytes.len()
|
||||||
);
|
);
|
||||||
let _ = stream.write_all(response.as_bytes());
|
let _ = stream.write_all(response.as_bytes());
|
||||||
let _ = stream.write_all(&body_bytes);
|
let _ = stream.write_all(&body_bytes);
|
||||||
let _ = stream.flush();
|
let _ = stream.flush();
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
|
|
||||||
thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self { url, addr, shutdown, handle: Some(handle) }
|
Self { url, handle: Some(handle) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TestHttpServer {
|
impl Drop for TestHttpServer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.shutdown.store(true, Ordering::Relaxed);
|
|
||||||
let _ = TcpStream::connect(self.addr);
|
|
||||||
|
|
||||||
if let Some(handle) = self.handle.take() {
|
if let Some(handle) = self.handle.take() {
|
||||||
let _ = handle.join();
|
let _ = handle.join();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use yaak_models::query_manager::QueryManager;
|
|||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
pub fn cli_cmd(data_dir: &Path) -> Command {
|
pub fn cli_cmd(data_dir: &Path) -> Command {
|
||||||
let mut cmd = cargo_bin_cmd!("yaak");
|
let mut cmd = cargo_bin_cmd!("yaakcli");
|
||||||
cmd.arg("--data-dir").arg(data_dir);
|
cmd.arg("--data-dir").arg(data_dir);
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,69 +78,3 @@ fn json_create_and_update_merge_patch_round_trip() {
|
|||||||
.stdout(contains("\"name\": \"Json Environment\""))
|
.stdout(contains("\"name\": \"Json Environment\""))
|
||||||
.stdout(contains("\"color\": \"#00ff00\""));
|
.stdout(contains("\"color\": \"#00ff00\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_merges_positional_workspace_id_into_json_payload() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
seed_workspace(data_dir, "wk_test");
|
|
||||||
|
|
||||||
let create_assert = cli_cmd(data_dir)
|
|
||||||
.args([
|
|
||||||
"environment",
|
|
||||||
"create",
|
|
||||||
"wk_test",
|
|
||||||
"--json",
|
|
||||||
r#"{"name":"Merged Environment"}"#,
|
|
||||||
])
|
|
||||||
.assert()
|
|
||||||
.success();
|
|
||||||
let environment_id = parse_created_id(&create_assert.get_output().stdout, "environment create");
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args(["environment", "show", &environment_id])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout(contains("\"workspaceId\": \"wk_test\""))
|
|
||||||
.stdout(contains("\"name\": \"Merged Environment\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_rejects_conflicting_workspace_ids_between_arg_and_json() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
seed_workspace(data_dir, "wk_test");
|
|
||||||
seed_workspace(data_dir, "wk_other");
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args([
|
|
||||||
"environment",
|
|
||||||
"create",
|
|
||||||
"wk_test",
|
|
||||||
"--json",
|
|
||||||
r#"{"workspaceId":"wk_other","name":"Mismatch"}"#,
|
|
||||||
])
|
|
||||||
.assert()
|
|
||||||
.failure()
|
|
||||||
.stderr(contains(
|
|
||||||
"environment create got conflicting workspace_id values between positional arg and JSON payload",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn environment_schema_outputs_json_schema() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args(["environment", "schema"])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout(contains("\"type\":\"object\""))
|
|
||||||
.stdout(contains("\"x-yaak-agent-hints\""))
|
|
||||||
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
|
||||||
.stdout(contains(
|
|
||||||
"\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"",
|
|
||||||
))
|
|
||||||
.stdout(contains("\"workspaceId\""));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -72,51 +72,3 @@ fn json_create_and_update_merge_patch_round_trip() {
|
|||||||
.stdout(contains("\"name\": \"Json Folder\""))
|
.stdout(contains("\"name\": \"Json Folder\""))
|
||||||
.stdout(contains("\"description\": \"Folder Description\""));
|
.stdout(contains("\"description\": \"Folder Description\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_merges_positional_workspace_id_into_json_payload() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
seed_workspace(data_dir, "wk_test");
|
|
||||||
|
|
||||||
let create_assert = cli_cmd(data_dir)
|
|
||||||
.args([
|
|
||||||
"folder",
|
|
||||||
"create",
|
|
||||||
"wk_test",
|
|
||||||
"--json",
|
|
||||||
r#"{"name":"Merged Folder"}"#,
|
|
||||||
])
|
|
||||||
.assert()
|
|
||||||
.success();
|
|
||||||
let folder_id = parse_created_id(&create_assert.get_output().stdout, "folder create");
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args(["folder", "show", &folder_id])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout(contains("\"workspaceId\": \"wk_test\""))
|
|
||||||
.stdout(contains("\"name\": \"Merged Folder\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_rejects_conflicting_workspace_ids_between_arg_and_json() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
seed_workspace(data_dir, "wk_test");
|
|
||||||
seed_workspace(data_dir, "wk_other");
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args([
|
|
||||||
"folder",
|
|
||||||
"create",
|
|
||||||
"wk_test",
|
|
||||||
"--json",
|
|
||||||
r#"{"workspaceId":"wk_other","name":"Mismatch"}"#,
|
|
||||||
])
|
|
||||||
.assert()
|
|
||||||
.failure()
|
|
||||||
.stderr(contains(
|
|
||||||
"folder create got conflicting workspace_id values between positional arg and JSON payload",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -130,54 +130,6 @@ fn create_allows_workspace_only_with_empty_defaults() {
|
|||||||
assert_eq!(request.url, "");
|
assert_eq!(request.url, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_merges_positional_workspace_id_into_json_payload() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
seed_workspace(data_dir, "wk_test");
|
|
||||||
|
|
||||||
let create_assert = cli_cmd(data_dir)
|
|
||||||
.args([
|
|
||||||
"request",
|
|
||||||
"create",
|
|
||||||
"wk_test",
|
|
||||||
"--json",
|
|
||||||
r#"{"name":"Merged Request","url":"https://example.com"}"#,
|
|
||||||
])
|
|
||||||
.assert()
|
|
||||||
.success();
|
|
||||||
let request_id = parse_created_id(&create_assert.get_output().stdout, "request create");
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args(["request", "show", &request_id])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout(contains("\"workspaceId\": \"wk_test\""))
|
|
||||||
.stdout(contains("\"name\": \"Merged Request\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn create_rejects_conflicting_workspace_ids_between_arg_and_json() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
seed_workspace(data_dir, "wk_test");
|
|
||||||
seed_workspace(data_dir, "wk_other");
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args([
|
|
||||||
"request",
|
|
||||||
"create",
|
|
||||||
"wk_test",
|
|
||||||
"--json",
|
|
||||||
r#"{"workspaceId":"wk_other","name":"Mismatch"}"#,
|
|
||||||
])
|
|
||||||
.assert()
|
|
||||||
.failure()
|
|
||||||
.stderr(contains(
|
|
||||||
"request create got conflicting workspace_id values between positional arg and JSON payload",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn request_send_persists_response_body_and_events() {
|
fn request_send_persists_response_body_and_events() {
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||||
@@ -204,6 +156,7 @@ fn request_send_persists_response_body_and_events() {
|
|||||||
.args(["request", "send", &request_id])
|
.args(["request", "send", &request_id])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
|
.stdout(contains("HTTP 200 OK"))
|
||||||
.stdout(contains("hello from integration test"));
|
.stdout(contains("hello from integration test"));
|
||||||
|
|
||||||
let qm = query_manager(data_dir);
|
let qm = query_manager(data_dir);
|
||||||
@@ -236,26 +189,6 @@ fn request_schema_http_outputs_json_schema() {
|
|||||||
.args(["request", "schema", "http"])
|
.args(["request", "schema", "http"])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
.stdout(contains("\"type\":\"object\""))
|
|
||||||
.stdout(contains("\"x-yaak-agent-hints\""))
|
|
||||||
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
|
||||||
.stdout(contains(
|
|
||||||
"\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"",
|
|
||||||
))
|
|
||||||
.stdout(contains("\"authentication\":"))
|
|
||||||
.stdout(contains("/foo/:id/comments/:commentId"))
|
|
||||||
.stdout(contains("put concrete values in `urlParameters`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn request_schema_http_pretty_prints_with_flag() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args(["request", "schema", "http", "--pretty"])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout(contains("\"type\": \"object\""))
|
.stdout(contains("\"type\": \"object\""))
|
||||||
.stdout(contains("\"authentication\""));
|
.stdout(contains("\"authentication\""));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fn top_level_send_workspace_sends_http_requests_and_prints_summary() {
|
|||||||
.args(["send", "wk_test"])
|
.args(["send", "wk_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
|
.stdout(contains("HTTP 200 OK"))
|
||||||
.stdout(contains("workspace bulk send"))
|
.stdout(contains("workspace bulk send"))
|
||||||
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
||||||
}
|
}
|
||||||
@@ -61,6 +62,7 @@ fn top_level_send_folder_sends_http_requests_and_prints_summary() {
|
|||||||
.args(["send", "fl_test"])
|
.args(["send", "fl_test"])
|
||||||
.assert()
|
.assert()
|
||||||
.success()
|
.success()
|
||||||
|
.stdout(contains("HTTP 200 OK"))
|
||||||
.stdout(contains("folder bulk send"))
|
.stdout(contains("folder bulk send"))
|
||||||
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
.stdout(contains("Send summary: 1 succeeded, 0 failed"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,21 +57,3 @@ fn json_create_and_update_merge_patch_round_trip() {
|
|||||||
.stdout(contains("\"name\": \"Json Workspace\""))
|
.stdout(contains("\"name\": \"Json Workspace\""))
|
||||||
.stdout(contains("\"description\": \"Updated via JSON\""));
|
.stdout(contains("\"description\": \"Updated via JSON\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn workspace_schema_outputs_json_schema() {
|
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
|
||||||
let data_dir = temp_dir.path();
|
|
||||||
|
|
||||||
cli_cmd(data_dir)
|
|
||||||
.args(["workspace", "schema"])
|
|
||||||
.assert()
|
|
||||||
.success()
|
|
||||||
.stdout(contains("\"type\":\"object\""))
|
|
||||||
.stdout(contains("\"x-yaak-agent-hints\""))
|
|
||||||
.stdout(contains("\"templateVariableSyntax\":\"${[ my_var ]}\""))
|
|
||||||
.stdout(contains(
|
|
||||||
"\"templateFunctionSyntax\":\"${[ namespace.my_func(a='aaa',b='bbb') ]}\"",
|
|
||||||
))
|
|
||||||
.stdout(contains("\"name\""));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client
|
|||||||
http = { version = "1.2.0", default-features = false }
|
http = { version = "1.2.0", default-features = false }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
md5 = "0.8.0"
|
md5 = "0.8.0"
|
||||||
pretty_graphql = "0.2"
|
|
||||||
r2d2 = "0.8.10"
|
r2d2 = "0.8.10"
|
||||||
r2d2_sqlite = "0.25.0"
|
r2d2_sqlite = "0.25.0"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
|
|||||||
@@ -154,7 +154,6 @@ async fn send_http_request_inner<R: Runtime>(
|
|||||||
cookie_jar_id,
|
cookie_jar_id,
|
||||||
response_dir: &response_dir,
|
response_dir: &response_dir,
|
||||||
emit_events_to: None,
|
emit_events_to: None,
|
||||||
emit_response_body_chunks_to: None,
|
|
||||||
existing_response: Some(response_ctx.response().clone()),
|
existing_response: Some(response_ctx.response().clone()),
|
||||||
plugin_manager,
|
plugin_manager,
|
||||||
encryption_manager,
|
encryption_manager,
|
||||||
|
|||||||
@@ -31,16 +31,14 @@ use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::task::block_in_place;
|
use tokio::task::block_in_place;
|
||||||
use tokio::time;
|
use tokio::time;
|
||||||
use yaak_common::command::new_checked_command;
|
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
|
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
|
||||||
use yaak_templates::strip_json_comments::strip_json_comments;
|
|
||||||
use yaak_grpc::{Code, ServiceDefinition, serialize_message};
|
use yaak_grpc::{Code, ServiceDefinition, serialize_message};
|
||||||
use yaak_mac_window::AppHandleMacWindowExt;
|
use yaak_mac_window::AppHandleMacWindowExt;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
|
||||||
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Workspace,
|
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
|
||||||
WorkspaceMeta,
|
Workspace, WorkspaceMeta,
|
||||||
};
|
};
|
||||||
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
@@ -99,7 +97,6 @@ impl<R: Runtime> PluginContextExt<R> for WebviewWindow<R> {
|
|||||||
struct AppMetaData {
|
struct AppMetaData {
|
||||||
is_dev: bool,
|
is_dev: bool,
|
||||||
version: String,
|
version: String,
|
||||||
cli_version: Option<String>,
|
|
||||||
name: String,
|
name: String,
|
||||||
app_data_dir: String,
|
app_data_dir: String,
|
||||||
app_log_dir: String,
|
app_log_dir: String,
|
||||||
@@ -116,11 +113,9 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
|
|||||||
let vendored_plugin_dir =
|
let vendored_plugin_dir =
|
||||||
app_handle.path().resolve("vendored/plugins", BaseDirectory::Resource)?;
|
app_handle.path().resolve("vendored/plugins", BaseDirectory::Resource)?;
|
||||||
let default_project_dir = app_handle.path().home_dir()?.join("YaakProjects");
|
let default_project_dir = app_handle.path().home_dir()?.join("YaakProjects");
|
||||||
let cli_version = detect_cli_version().await;
|
|
||||||
Ok(AppMetaData {
|
Ok(AppMetaData {
|
||||||
is_dev: is_dev(),
|
is_dev: is_dev(),
|
||||||
version: app_handle.package_info().version.to_string(),
|
version: app_handle.package_info().version.to_string(),
|
||||||
cli_version,
|
|
||||||
name: app_handle.package_info().name.to_string(),
|
name: app_handle.package_info().name.to_string(),
|
||||||
app_data_dir: app_data_dir.to_string_lossy().to_string(),
|
app_data_dir: app_data_dir.to_string_lossy().to_string(),
|
||||||
app_log_dir: app_log_dir.to_string_lossy().to_string(),
|
app_log_dir: app_log_dir.to_string_lossy().to_string(),
|
||||||
@@ -131,24 +126,6 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn detect_cli_version() -> Option<String> {
|
|
||||||
detect_cli_version_for_binary("yaak").await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn detect_cli_version_for_binary(program: &str) -> Option<String> {
|
|
||||||
let mut cmd = new_checked_command(program, "--version").await.ok()?;
|
|
||||||
let out = cmd.arg("--version").output().await.ok()?;
|
|
||||||
if !out.status.success() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = String::from_utf8(out.stdout).ok()?;
|
|
||||||
let line = line.lines().find(|l| !l.trim().is_empty())?.trim();
|
|
||||||
let mut parts = line.split_whitespace();
|
|
||||||
let _name = parts.next();
|
|
||||||
Some(parts.next().unwrap_or(line).to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_template_tokens_to_string<R: Runtime>(
|
async fn cmd_template_tokens_to_string<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
@@ -434,7 +411,6 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
result.expect("Failed to render template")
|
result.expect("Failed to render template")
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let msg = strip_json_comments(&msg);
|
|
||||||
in_msg_tx.try_send(msg.clone()).unwrap();
|
in_msg_tx.try_send(msg.clone()).unwrap();
|
||||||
}
|
}
|
||||||
Ok(IncomingMsg::Commit) => {
|
Ok(IncomingMsg::Commit) => {
|
||||||
@@ -470,7 +446,6 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let msg = strip_json_comments(&msg);
|
|
||||||
|
|
||||||
app_handle.db().upsert_grpc_event(
|
app_handle.db().upsert_grpc_event(
|
||||||
&GrpcEvent {
|
&GrpcEvent {
|
||||||
@@ -872,14 +847,6 @@ async fn cmd_format_json(text: &str) -> YaakResult<String> {
|
|||||||
Ok(format_json(text, " "))
|
Ok(format_json(text, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn cmd_format_graphql(text: &str) -> YaakResult<String> {
|
|
||||||
match pretty_graphql::format_text(text, &Default::default()) {
|
|
||||||
Ok(formatted) => Ok(formatted),
|
|
||||||
Err(_) => Ok(text.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_http_response_body<R: Runtime>(
|
async fn cmd_http_response_body<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
@@ -1378,6 +1345,29 @@ async fn cmd_send_http_request<R: Runtime>(
|
|||||||
Ok(r)
|
Ok(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_install_plugin<R: Runtime>(
|
||||||
|
directory: &str,
|
||||||
|
url: Option<String>,
|
||||||
|
plugin_manager: State<'_, PluginManager>,
|
||||||
|
app_handle: AppHandle<R>,
|
||||||
|
window: WebviewWindow<R>,
|
||||||
|
) -> YaakResult<Plugin> {
|
||||||
|
let plugin = app_handle.db().upsert_plugin(
|
||||||
|
&Plugin { directory: directory.into(), url, enabled: true, ..Default::default() },
|
||||||
|
&UpdateSource::from_window_label(window.label()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
plugin_manager
|
||||||
|
.add_plugin(
|
||||||
|
&PluginContext::new(Some(window.label().to_string()), window.workspace_id()),
|
||||||
|
&plugin,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(plugin)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_reload_plugins<R: Runtime>(
|
async fn cmd_reload_plugins<R: Runtime>(
|
||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
@@ -1649,7 +1639,6 @@ pub fn run() {
|
|||||||
cmd_http_request_body,
|
cmd_http_request_body,
|
||||||
cmd_http_response_body,
|
cmd_http_response_body,
|
||||||
cmd_format_json,
|
cmd_format_json,
|
||||||
cmd_format_graphql,
|
|
||||||
cmd_get_http_authentication_summaries,
|
cmd_get_http_authentication_summaries,
|
||||||
cmd_get_http_authentication_config,
|
cmd_get_http_authentication_config,
|
||||||
cmd_get_sse_events,
|
cmd_get_sse_events,
|
||||||
@@ -1663,6 +1652,7 @@ pub fn run() {
|
|||||||
cmd_workspace_actions,
|
cmd_workspace_actions,
|
||||||
cmd_folder_actions,
|
cmd_folder_actions,
|
||||||
cmd_import_data,
|
cmd_import_data,
|
||||||
|
cmd_install_plugin,
|
||||||
cmd_metadata,
|
cmd_metadata,
|
||||||
cmd_new_child_window,
|
cmd_new_child_window,
|
||||||
cmd_new_main_window,
|
cmd_new_main_window,
|
||||||
@@ -1731,7 +1721,6 @@ pub fn run() {
|
|||||||
git_ext::cmd_git_rm_remote,
|
git_ext::cmd_git_rm_remote,
|
||||||
//
|
//
|
||||||
// Plugin commands
|
// Plugin commands
|
||||||
plugins_ext::cmd_plugins_install_from_directory,
|
|
||||||
plugins_ext::cmd_plugins_search,
|
plugins_ext::cmd_plugins_search,
|
||||||
plugins_ext::cmd_plugins_install,
|
plugins_ext::cmd_plugins_install,
|
||||||
plugins_ext::cmd_plugins_uninstall,
|
plugins_ext::cmd_plugins_uninstall,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use yaak_models::error::Result;
|
|||||||
use yaak_models::models::{AnyModel, GraphQlIntrospection, GrpcEvent, Settings, WebsocketEvent};
|
use yaak_models::models::{AnyModel, GraphQlIntrospection, GrpcEvent, Settings, WebsocketEvent};
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_plugins::manager::PluginManager;
|
|
||||||
|
|
||||||
const MODEL_CHANGES_RETENTION_HOURS: i64 = 1;
|
const MODEL_CHANGES_RETENTION_HOURS: i64 = 1;
|
||||||
const MODEL_CHANGES_POLL_INTERVAL_MS: u64 = 1000;
|
const MODEL_CHANGES_POLL_INTERVAL_MS: u64 = 1000;
|
||||||
@@ -256,32 +255,23 @@ pub(crate) fn models_upsert_graphql_introspection<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub(crate) async fn models_workspace_models<R: Runtime>(
|
pub(crate) fn models_workspace_models<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
workspace_id: Option<&str>,
|
workspace_id: Option<&str>,
|
||||||
plugin_manager: State<'_, PluginManager>,
|
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
let db = window.db();
|
||||||
let mut l: Vec<AnyModel> = Vec::new();
|
let mut l: Vec<AnyModel> = Vec::new();
|
||||||
|
|
||||||
// Add the global models
|
// Add the settings
|
||||||
{
|
l.push(db.get_settings().into());
|
||||||
let db = window.db();
|
|
||||||
l.push(db.get_settings().into());
|
|
||||||
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
|
|
||||||
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
|
|
||||||
}
|
|
||||||
|
|
||||||
let plugins = {
|
// Add global models
|
||||||
let db = window.db();
|
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
|
||||||
db.list_plugins()?
|
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
|
||||||
};
|
l.append(&mut db.list_plugins()?.into_iter().map(Into::into).collect());
|
||||||
|
|
||||||
let plugins = plugin_manager.resolve_plugins_for_runtime_from_db(plugins).await;
|
|
||||||
l.append(&mut plugins.into_iter().map(Into::into).collect());
|
|
||||||
|
|
||||||
// Add the workspace children
|
// Add the workspace children
|
||||||
if let Some(wid) = workspace_id {
|
if let Some(wid) = workspace_id {
|
||||||
let db = window.db();
|
|
||||||
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
|
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
|
||||||
l.append(&mut db.list_environments_ensure_base(wid)?.into_iter().map(Into::into).collect());
|
l.append(&mut db.list_environments_ensure_base(wid)?.into_iter().map(Into::into).collect());
|
||||||
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());
|
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
use yaak_api::yaak_api_client;
|
||||||
use yaak_common::platform::get_os_str;
|
use yaak_common::platform::get_os_str;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ impl YaakNotifier {
|
|||||||
|
|
||||||
let launch_info = get_or_upsert_launch_info(app_handle);
|
let launch_info = get_or_upsert_launch_info(app_handle);
|
||||||
let app_version = app_handle.package_info().version.to_string();
|
let app_version = app_handle.package_info().version.to_string();
|
||||||
let req = yaak_api_client(ApiClientKind::App, &app_version)?
|
let req = yaak_api_client(&app_version)?
|
||||||
.request(Method::GET, "https://notify.yaak.app/notifications")
|
.request(Method::GET, "https://notify.yaak.app/notifications")
|
||||||
.query(&[
|
.query(&[
|
||||||
("version", &launch_info.current_version),
|
("version", &launch_info.current_version),
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ use yaak::plugin_events::{
|
|||||||
GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event,
|
GroupedPluginEvent, HostRequest, SharedPluginEventContext, handle_shared_plugin_event,
|
||||||
};
|
};
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
use yaak_models::models::{HttpResponse, Plugin};
|
use yaak_models::models::{AnyModel, HttpResponse, Plugin};
|
||||||
use yaak_models::queries::any_request::AnyRequest;
|
use yaak_models::queries::any_request::AnyRequest;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_plugins::error::Error::PluginErr;
|
use yaak_plugins::error::Error::PluginErr;
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
Color, EmptyPayload, ErrorResponse, GetCookieValueResponse, Icon, InternalEvent,
|
Color, EmptyPayload, ErrorResponse, FindHttpResponsesResponse, GetCookieValueResponse, Icon,
|
||||||
InternalEventPayload, ListCookieNamesResponse, ListOpenWorkspacesResponse,
|
InternalEvent, InternalEventPayload, ListCookieNamesResponse, ListOpenWorkspacesResponse,
|
||||||
RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse,
|
RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse,
|
||||||
ShowToastRequest, TemplateRenderResponse, WindowInfoResponse, WindowNavigateEvent,
|
ShowToastRequest, TemplateRenderResponse, WindowInfoResponse, WindowNavigateEvent,
|
||||||
WorkspaceInfo,
|
WorkspaceInfo,
|
||||||
@@ -118,7 +118,7 @@ async fn handle_host_plugin_request<R: Runtime>(
|
|||||||
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
|
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
|
||||||
message: format!("Reloaded plugin {}@{}", info.name, info.version),
|
message: format!("Reloaded plugin {}@{}", info.name, info.version),
|
||||||
icon: Some(Icon::Info),
|
icon: Some(Icon::Info),
|
||||||
timeout: Some(5000),
|
timeout: Some(3000),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
@@ -190,6 +190,71 @@ async fn handle_host_plugin_request<R: Runtime>(
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
HostRequest::FindHttpResponses(req) => {
|
||||||
|
let http_responses = app_handle
|
||||||
|
.db()
|
||||||
|
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
|
||||||
|
http_responses,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
HostRequest::UpsertModel(req) => {
|
||||||
|
use AnyModel::*;
|
||||||
|
let model = match &req.model {
|
||||||
|
HttpRequest(m) => {
|
||||||
|
HttpRequest(app_handle.db().upsert_http_request(m, &UpdateSource::Plugin)?)
|
||||||
|
}
|
||||||
|
GrpcRequest(m) => {
|
||||||
|
GrpcRequest(app_handle.db().upsert_grpc_request(m, &UpdateSource::Plugin)?)
|
||||||
|
}
|
||||||
|
WebsocketRequest(m) => WebsocketRequest(
|
||||||
|
app_handle.db().upsert_websocket_request(m, &UpdateSource::Plugin)?,
|
||||||
|
),
|
||||||
|
Folder(m) => Folder(app_handle.db().upsert_folder(m, &UpdateSource::Plugin)?),
|
||||||
|
Environment(m) => {
|
||||||
|
Environment(app_handle.db().upsert_environment(m, &UpdateSource::Plugin)?)
|
||||||
|
}
|
||||||
|
Workspace(m) => {
|
||||||
|
Workspace(app_handle.db().upsert_workspace(m, &UpdateSource::Plugin)?)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(PluginErr("Upsert not supported for this model type".into()).into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(InternalEventPayload::UpsertModelResponse(
|
||||||
|
yaak_plugins::events::UpsertModelResponse { model },
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
HostRequest::DeleteModel(req) => {
|
||||||
|
let model = match req.model.as_str() {
|
||||||
|
"http_request" => AnyModel::HttpRequest(
|
||||||
|
app_handle.db().delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?,
|
||||||
|
),
|
||||||
|
"grpc_request" => AnyModel::GrpcRequest(
|
||||||
|
app_handle.db().delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?,
|
||||||
|
),
|
||||||
|
"websocket_request" => AnyModel::WebsocketRequest(
|
||||||
|
app_handle
|
||||||
|
.db()
|
||||||
|
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)?,
|
||||||
|
),
|
||||||
|
"folder" => AnyModel::Folder(
|
||||||
|
app_handle.db().delete_folder_by_id(&req.id, &UpdateSource::Plugin)?,
|
||||||
|
),
|
||||||
|
"environment" => AnyModel::Environment(
|
||||||
|
app_handle.db().delete_environment_by_id(&req.id, &UpdateSource::Plugin)?,
|
||||||
|
),
|
||||||
|
_ => {
|
||||||
|
return Err(PluginErr("Delete not supported for this model type".into()).into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(InternalEventPayload::DeleteModelResponse(
|
||||||
|
yaak_plugins::events::DeleteModelResponse { model },
|
||||||
|
)))
|
||||||
|
}
|
||||||
HostRequest::RenderGrpcRequest(req) => {
|
HostRequest::RenderGrpcRequest(req) => {
|
||||||
let window = get_window_from_plugin_context(app_handle, plugin_context)?;
|
let window = get_window_from_plugin_context(app_handle, plugin_context)?;
|
||||||
|
|
||||||
@@ -297,7 +362,7 @@ async fn handle_host_plugin_request<R: Runtime>(
|
|||||||
workspace_id: http_request.workspace_id.clone(),
|
workspace_id: http_request.workspace_id.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
&UpdateSource::from_window_label(window.label()),
|
&UpdateSource::Plugin,
|
||||||
&blobs,
|
&blobs,
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ use tauri::{
|
|||||||
};
|
};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
use yaak_api::yaak_api_client;
|
||||||
use yaak_models::models::{Plugin, PluginSource};
|
use yaak_models::models::Plugin;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
use yaak_plugins::api::{
|
use yaak_plugins::api::{
|
||||||
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
|
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
|
||||||
search_plugins,
|
search_plugins,
|
||||||
};
|
};
|
||||||
use yaak_plugins::events::PluginContext;
|
use yaak_plugins::events::{Color, Icon, PluginContext, ShowToastRequest};
|
||||||
use yaak_plugins::install::{delete_and_uninstall, download_and_install};
|
use yaak_plugins::install::{delete_and_uninstall, download_and_install};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::plugin_meta::get_plugin_meta;
|
use yaak_plugins::plugin_meta::get_plugin_meta;
|
||||||
@@ -73,7 +73,7 @@ impl PluginUpdater {
|
|||||||
info!("Checking for plugin updates");
|
info!("Checking for plugin updates");
|
||||||
|
|
||||||
let app_version = window.app_handle().package_info().version.to_string();
|
let app_version = window.app_handle().package_info().version.to_string();
|
||||||
let http_client = yaak_api_client(ApiClientKind::App, &app_version)?;
|
let http_client = yaak_api_client(&app_version)?;
|
||||||
let plugins = window.app_handle().db().list_plugins()?;
|
let plugins = window.app_handle().db().list_plugins()?;
|
||||||
let updates = check_plugin_updates(&http_client, plugins.clone()).await?;
|
let updates = check_plugin_updates(&http_client, plugins.clone()).await?;
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ pub async fn cmd_plugins_search<R: Runtime>(
|
|||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<PluginSearchResponse> {
|
) -> Result<PluginSearchResponse> {
|
||||||
let app_version = app_handle.package_info().version.to_string();
|
let app_version = app_handle.package_info().version.to_string();
|
||||||
let http_client = yaak_api_client(ApiClientKind::App, &app_version)?;
|
let http_client = yaak_api_client(&app_version)?;
|
||||||
Ok(search_plugins(&http_client, query).await?)
|
Ok(search_plugins(&http_client, query).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ pub async fn cmd_plugins_install<R: Runtime>(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
|
||||||
let app_version = window.app_handle().package_info().version.to_string();
|
let app_version = window.app_handle().package_info().version.to_string();
|
||||||
let http_client = yaak_api_client(ApiClientKind::App, &app_version)?;
|
let http_client = yaak_api_client(&app_version)?;
|
||||||
let query_manager = window.state::<yaak_models::query_manager::QueryManager>();
|
let query_manager = window.state::<yaak_models::query_manager::QueryManager>();
|
||||||
let plugin_context = window.plugin_context();
|
let plugin_context = window.plugin_context();
|
||||||
download_and_install(
|
download_and_install(
|
||||||
@@ -165,28 +165,6 @@ pub async fn cmd_plugins_install<R: Runtime>(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
|
||||||
pub async fn cmd_plugins_install_from_directory<R: Runtime>(
|
|
||||||
window: WebviewWindow<R>,
|
|
||||||
directory: &str,
|
|
||||||
) -> Result<Plugin> {
|
|
||||||
let plugin = window.db().upsert_plugin(
|
|
||||||
&Plugin {
|
|
||||||
directory: directory.into(),
|
|
||||||
url: None,
|
|
||||||
enabled: true,
|
|
||||||
source: PluginSource::Filesystem,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&UpdateSource::from_window_label(window.label()),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
|
|
||||||
plugin_manager.add_plugin(&window.plugin_context(), &plugin).await?;
|
|
||||||
|
|
||||||
Ok(plugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn cmd_plugins_uninstall<R: Runtime>(
|
pub async fn cmd_plugins_uninstall<R: Runtime>(
|
||||||
plugin_id: &str,
|
plugin_id: &str,
|
||||||
@@ -203,7 +181,7 @@ pub async fn cmd_plugins_updates<R: Runtime>(
|
|||||||
app_handle: AppHandle<R>,
|
app_handle: AppHandle<R>,
|
||||||
) -> Result<PluginUpdatesResponse> {
|
) -> Result<PluginUpdatesResponse> {
|
||||||
let app_version = app_handle.package_info().version.to_string();
|
let app_version = app_handle.package_info().version.to_string();
|
||||||
let http_client = yaak_api_client(ApiClientKind::App, &app_version)?;
|
let http_client = yaak_api_client(&app_version)?;
|
||||||
let plugins = app_handle.db().list_plugins()?;
|
let plugins = app_handle.db().list_plugins()?;
|
||||||
Ok(check_plugin_updates(&http_client, plugins).await?)
|
Ok(check_plugin_updates(&http_client, plugins).await?)
|
||||||
}
|
}
|
||||||
@@ -213,7 +191,7 @@ pub async fn cmd_plugins_update_all<R: Runtime>(
|
|||||||
window: WebviewWindow<R>,
|
window: WebviewWindow<R>,
|
||||||
) -> Result<Vec<PluginNameVersion>> {
|
) -> Result<Vec<PluginNameVersion>> {
|
||||||
let app_version = window.app_handle().package_info().version.to_string();
|
let app_version = window.app_handle().package_info().version.to_string();
|
||||||
let http_client = yaak_api_client(ApiClientKind::App, &app_version)?;
|
let http_client = yaak_api_client(&app_version)?;
|
||||||
let plugins = window.db().list_plugins()?;
|
let plugins = window.db().list_plugins()?;
|
||||||
|
|
||||||
// Get list of available updates (already filtered to only registry plugins)
|
// Get list of available updates (already filtered to only registry plugins)
|
||||||
@@ -290,8 +268,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
.join("index.cjs");
|
.join("index.cjs");
|
||||||
|
|
||||||
let dev_mode = is_dev();
|
let dev_mode = is_dev();
|
||||||
let query_manager =
|
|
||||||
app_handle.state::<yaak_models::query_manager::QueryManager>().inner().clone();
|
|
||||||
|
|
||||||
// Create plugin manager asynchronously
|
// Create plugin manager asynchronously
|
||||||
let app_handle_clone = app_handle.clone();
|
let app_handle_clone = app_handle.clone();
|
||||||
@@ -301,12 +277,53 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
installed_plugin_dir,
|
installed_plugin_dir,
|
||||||
node_bin_path,
|
node_bin_path,
|
||||||
plugin_runtime_main,
|
plugin_runtime_main,
|
||||||
&query_manager,
|
|
||||||
&PluginContext::new_empty(),
|
|
||||||
dev_mode,
|
dev_mode,
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.expect("Failed to initialize plugins");
|
|
||||||
|
// Initialize all plugins after manager is created
|
||||||
|
let bundled_dirs = manager
|
||||||
|
.list_bundled_plugin_dirs()
|
||||||
|
.await
|
||||||
|
.expect("Failed to list bundled plugins");
|
||||||
|
|
||||||
|
// Ensure all bundled plugins make it into the database
|
||||||
|
let db = app_handle_clone.db();
|
||||||
|
for dir in &bundled_dirs {
|
||||||
|
if db.get_plugin_by_directory(dir).is_none() {
|
||||||
|
db.upsert_plugin(
|
||||||
|
&Plugin {
|
||||||
|
directory: dir.clone(),
|
||||||
|
enabled: true,
|
||||||
|
url: None,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&UpdateSource::Background,
|
||||||
|
)
|
||||||
|
.expect("Failed to upsert bundled plugin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all plugins from database and initialize
|
||||||
|
let plugins = db.list_plugins().expect("Failed to list plugins from database");
|
||||||
|
drop(db); // Explicitly drop the connection before await
|
||||||
|
|
||||||
|
let errors =
|
||||||
|
manager.initialize_all_plugins(plugins, &PluginContext::new_empty()).await;
|
||||||
|
|
||||||
|
// Show toast for any failed plugins
|
||||||
|
for (plugin_dir, error_msg) in errors {
|
||||||
|
let plugin_name = plugin_dir.split('/').last().unwrap_or(&plugin_dir);
|
||||||
|
let toast = ShowToastRequest {
|
||||||
|
message: format!("Failed to start plugin '{}': {}", plugin_name, error_msg),
|
||||||
|
color: Some(Color::Danger),
|
||||||
|
icon: Some(Icon::AlertTriangle),
|
||||||
|
timeout: Some(10000),
|
||||||
|
};
|
||||||
|
if let Err(emit_err) = app_handle_clone.emit("show_toast", toast) {
|
||||||
|
error!("Failed to emit toast for plugin error: {emit_err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app_handle_clone.manage(manager);
|
app_handle_clone.manage(manager);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
use log::info;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
pub use yaak::render::{render_grpc_request, render_http_request};
|
use std::collections::BTreeMap;
|
||||||
use yaak_models::models::Environment;
|
pub use yaak::render::render_http_request;
|
||||||
|
use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader};
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
@@ -23,3 +25,61 @@ pub async fn render_json_value<T: TemplateCallback>(
|
|||||||
let vars = &make_vars_hashmap(environment_chain);
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
render_json_value_raw(value, vars, cb, opt).await
|
render_json_value_raw(value, vars, cb, opt).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn render_grpc_request<T: TemplateCallback>(
|
||||||
|
r: &GrpcRequest,
|
||||||
|
environment_chain: Vec<Environment>,
|
||||||
|
cb: &T,
|
||||||
|
opt: &RenderOptions,
|
||||||
|
) -> yaak_templates::error::Result<GrpcRequest> {
|
||||||
|
let vars = &make_vars_hashmap(environment_chain);
|
||||||
|
|
||||||
|
let mut metadata = Vec::new();
|
||||||
|
for p in r.metadata.clone() {
|
||||||
|
if !p.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
metadata.push(HttpRequestHeader {
|
||||||
|
enabled: p.enabled,
|
||||||
|
name: parse_and_render(p.name.as_str(), vars, cb, &opt).await?,
|
||||||
|
value: parse_and_render(p.value.as_str(), vars, cb, &opt).await?,
|
||||||
|
id: p.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let authentication = {
|
||||||
|
let mut disabled = false;
|
||||||
|
let mut auth = BTreeMap::new();
|
||||||
|
match r.authentication.get("disabled") {
|
||||||
|
Some(Value::Bool(true)) => {
|
||||||
|
disabled = true;
|
||||||
|
}
|
||||||
|
Some(Value::String(tmpl)) => {
|
||||||
|
disabled = parse_and_render(tmpl.as_str(), vars, cb, &opt)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_empty();
|
||||||
|
info!(
|
||||||
|
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if disabled {
|
||||||
|
auth.insert("disabled".to_string(), Value::Bool(true));
|
||||||
|
} else {
|
||||||
|
for (k, v) in r.authentication.clone() {
|
||||||
|
if k == "disabled" {
|
||||||
|
auth.insert(k, Value::Bool(false));
|
||||||
|
} else {
|
||||||
|
auth.insert(k, render_json_value_raw(v, vars, cb, &opt).await?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auth
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = parse_and_render(r.url.as_str(), vars, cb, &opt).await?;
|
||||||
|
|
||||||
|
Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() })
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use std::fs;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, Url};
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
use yaak_api::yaak_api_client;
|
||||||
use yaak_models::util::generate_id;
|
use yaak_models::util::generate_id;
|
||||||
use yaak_plugins::events::{Color, ShowToastRequest};
|
use yaak_plugins::events::{Color, ShowToastRequest};
|
||||||
use yaak_plugins::install::download_and_install;
|
use yaak_plugins::install::download_and_install;
|
||||||
@@ -47,7 +47,7 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
|||||||
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
|
let plugin_manager = Arc::new((*window.state::<PluginManager>()).clone());
|
||||||
let query_manager = app_handle.db_manager();
|
let query_manager = app_handle.db_manager();
|
||||||
let app_version = app_handle.package_info().version.to_string();
|
let app_version = app_handle.package_info().version.to_string();
|
||||||
let http_client = yaak_api_client(ApiClientKind::App, &app_version)?;
|
let http_client = yaak_api_client(&app_version)?;
|
||||||
let plugin_context = window.plugin_context();
|
let plugin_context = window.plugin_context();
|
||||||
let pv = download_and_install(
|
let pv = download_and_install(
|
||||||
plugin_manager,
|
plugin_manager,
|
||||||
@@ -88,8 +88,7 @@ pub(crate) async fn handle_deep_link<R: Runtime>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let app_version = app_handle.package_info().version.to_string();
|
let app_version = app_handle.package_info().version.to_string();
|
||||||
let resp =
|
let resp = yaak_api_client(&app_version)?.get(file_url).send().await?;
|
||||||
yaak_api_client(ApiClientKind::App, &app_version)?.get(file_url).send().await?;
|
|
||||||
let json = resp.bytes().await?;
|
let json = resp.bytes().await?;
|
||||||
let p = app_handle
|
let p = app_handle
|
||||||
.path()
|
.path()
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ use yaak_models::util::UpdateSource;
|
|||||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose};
|
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose};
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||||
use yaak_templates::strip_json_comments::maybe_strip_json_comments;
|
|
||||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||||
use yaak_tls::find_client_certificate;
|
use yaak_tls::find_client_certificate;
|
||||||
use yaak_ws::{WebsocketManager, render_websocket_request};
|
use yaak_ws::{WebsocketManager, render_websocket_request};
|
||||||
@@ -73,10 +72,8 @@ pub async fn cmd_ws_send<R: Runtime>(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let message = maybe_strip_json_comments(&request.message);
|
|
||||||
|
|
||||||
let mut ws_manager = ws_manager.lock().await;
|
let mut ws_manager = ws_manager.lock().await;
|
||||||
ws_manager.send(&connection.id, Message::Text(message.clone().into())).await?;
|
ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?;
|
||||||
|
|
||||||
app_handle.db().upsert_websocket_event(
|
app_handle.db().upsert_websocket_event(
|
||||||
&WebsocketEvent {
|
&WebsocketEvent {
|
||||||
@@ -85,7 +82,7 @@ pub async fn cmd_ws_send<R: Runtime>(
|
|||||||
workspace_id: connection.workspace_id.clone(),
|
workspace_id: connection.workspace_id.clone(),
|
||||||
is_server: false,
|
is_server: false,
|
||||||
message_type: WebsocketEventType::Text,
|
message_type: WebsocketEventType::Text,
|
||||||
message: message.into(),
|
message: request.message.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
&UpdateSource::from_window_label(window.label()),
|
&UpdateSource::from_window_label(window.label()),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::ops::Add;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow, is_dev};
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_api::{ApiClientKind, yaak_api_client};
|
use yaak_api::yaak_api_client;
|
||||||
use yaak_common::platform::get_os_str;
|
use yaak_common::platform::get_os_str;
|
||||||
use yaak_models::db_context::DbContext;
|
use yaak_models::db_context::DbContext;
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
@@ -119,7 +119,7 @@ pub async fn activate_license<R: Runtime>(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("Activating license {}", license_key);
|
info!("Activating license {}", license_key);
|
||||||
let app_version = window.app_handle().package_info().version.to_string();
|
let app_version = window.app_handle().package_info().version.to_string();
|
||||||
let client = yaak_api_client(ApiClientKind::App, &app_version)?;
|
let client = yaak_api_client(&app_version)?;
|
||||||
let payload = ActivateLicenseRequestPayload {
|
let payload = ActivateLicenseRequestPayload {
|
||||||
license_key: license_key.to_string(),
|
license_key: license_key.to_string(),
|
||||||
app_platform: get_os_str().to_string(),
|
app_platform: get_os_str().to_string(),
|
||||||
@@ -157,7 +157,7 @@ pub async fn deactivate_license<R: Runtime>(window: &WebviewWindow<R>) -> Result
|
|||||||
let activation_id = get_activation_id(app_handle).await;
|
let activation_id = get_activation_id(app_handle).await;
|
||||||
|
|
||||||
let app_version = window.app_handle().package_info().version.to_string();
|
let app_version = window.app_handle().package_info().version.to_string();
|
||||||
let client = yaak_api_client(ApiClientKind::App, &app_version)?;
|
let client = yaak_api_client(&app_version)?;
|
||||||
let path = format!("/licenses/activations/{}/deactivate", activation_id);
|
let path = format!("/licenses/activations/{}/deactivate", activation_id);
|
||||||
let payload =
|
let payload =
|
||||||
DeactivateLicenseRequestPayload { app_platform: get_os_str().to_string(), app_version };
|
DeactivateLicenseRequestPayload { app_platform: get_os_str().to_string(), app_version };
|
||||||
@@ -203,7 +203,7 @@ pub async fn check_license<R: Runtime>(window: &WebviewWindow<R>) -> Result<Lice
|
|||||||
(true, _) => {
|
(true, _) => {
|
||||||
info!("Checking license activation");
|
info!("Checking license activation");
|
||||||
// A license has been activated, so let's check the license server
|
// A license has been activated, so let's check the license server
|
||||||
let client = yaak_api_client(ApiClientKind::App, &payload.app_version)?;
|
let client = yaak_api_client(&payload.app_version)?;
|
||||||
let path = format!("/licenses/activations/{activation_id}/check-v2");
|
let path = format!("/licenses/activations/{activation_id}/check-v2");
|
||||||
let response = client.post(build_url(&path)).json(&payload).send().await?;
|
let response = client.post(build_url(&path)).json(&payload).send().await?;
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ unsafe impl Sync for UnsafeWindowHandle {}
|
|||||||
|
|
||||||
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
|
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
|
||||||
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
|
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
|
||||||
/// Extra pixels to add to the title bar height when the default title bar is
|
|
||||||
/// already as tall as button_height + PAD_Y (i.e. macOS Tahoe 26+, where the
|
|
||||||
/// default is 32px and 14 + 18 = 32). On pre-Tahoe this is unused because the
|
|
||||||
/// default title bar is shorter than button_height + PAD_Y.
|
|
||||||
const TITLEBAR_EXTRA_HEIGHT: f64 = 4.0;
|
|
||||||
const MAIN_WINDOW_PREFIX: &str = "main_";
|
const MAIN_WINDOW_PREFIX: &str = "main_";
|
||||||
|
|
||||||
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
|
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
|
||||||
@@ -100,29 +95,12 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64,
|
|||||||
ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
||||||
let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
||||||
|
|
||||||
|
let title_bar_container_view = close.superview().superview();
|
||||||
|
|
||||||
let close_rect: NSRect = msg_send![close, frame];
|
let close_rect: NSRect = msg_send![close, frame];
|
||||||
let button_height = close_rect.size.height;
|
let button_height = close_rect.size.height;
|
||||||
|
|
||||||
let title_bar_container_view = close.superview().superview();
|
let title_bar_frame_height = button_height + y;
|
||||||
|
|
||||||
// Capture the OS default title bar height on the first call, before
|
|
||||||
// we've modified it. This avoids the height growing on repeated calls.
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
static DEFAULT_TITLEBAR_HEIGHT: OnceLock<f64> = OnceLock::new();
|
|
||||||
let default_height =
|
|
||||||
*DEFAULT_TITLEBAR_HEIGHT.get_or_init(|| NSView::frame(title_bar_container_view).size.height);
|
|
||||||
|
|
||||||
// On pre-Tahoe, button_height + y is larger than the default title bar
|
|
||||||
// height, so the resize works as before. On Tahoe (26+), the default is
|
|
||||||
// already 32px and button_height + y = 32, so nothing changes. In that
|
|
||||||
// case, add TITLEBAR_EXTRA_HEIGHT extra pixels to push the buttons down.
|
|
||||||
let desired = button_height + y;
|
|
||||||
let title_bar_frame_height = if desired > default_height {
|
|
||||||
desired
|
|
||||||
} else {
|
|
||||||
default_height + TITLEBAR_EXTRA_HEIGHT
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
let mut title_bar_rect = NSView::frame(title_bar_container_view);
|
||||||
title_bar_rect.size.height = title_bar_frame_height;
|
title_bar_rect.size.height = title_bar_frame_height;
|
||||||
title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height;
|
title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height;
|
||||||
|
|||||||
@@ -8,24 +8,14 @@ use reqwest::header::{HeaderMap, HeaderValue};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use yaak_common::platform::{get_ua_arch, get_ua_platform};
|
use yaak_common::platform::{get_ua_arch, get_ua_platform};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
||||||
pub enum ApiClientKind {
|
|
||||||
App,
|
|
||||||
Cli,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build a reqwest Client configured for Yaak's own API calls.
|
/// Build a reqwest Client configured for Yaak's own API calls.
|
||||||
///
|
///
|
||||||
/// Includes a custom User-Agent, JSON accept header, 20s timeout, gzip,
|
/// Includes a custom User-Agent, JSON accept header, 20s timeout, gzip,
|
||||||
/// and automatic OS-level proxy detection via sysproxy.
|
/// and automatic OS-level proxy detection via sysproxy.
|
||||||
pub fn yaak_api_client(kind: ApiClientKind, version: &str) -> Result<Client> {
|
pub fn yaak_api_client(version: &str) -> Result<Client> {
|
||||||
let platform = get_ua_platform();
|
let platform = get_ua_platform();
|
||||||
let arch = get_ua_arch();
|
let arch = get_ua_arch();
|
||||||
let product = match kind {
|
let ua = format!("Yaak/{version} ({platform}; {arch})");
|
||||||
ApiClientKind::App => "Yaak",
|
|
||||||
ApiClientKind::Cli => "YaakCli",
|
|
||||||
};
|
|
||||||
let ua = format!("{product}/{version} ({platform}; {arch})");
|
|
||||||
|
|
||||||
let mut default_headers = HeaderMap::new();
|
let mut default_headers = HeaderMap::new();
|
||||||
default_headers.insert("Accept", HeaderValue::from_str("application/json").unwrap());
|
default_headers.insert("Accept", HeaderValue::from_str("application/json").unwrap());
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use std::ffi::{OsStr, OsString};
|
use std::ffi::OsStr;
|
||||||
use std::io::{self, ErrorKind};
|
|
||||||
use std::process::Stdio;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||||
@@ -16,27 +14,3 @@ pub fn new_xplatform_command<S: AsRef<OsStr>>(program: S) -> tokio::process::Com
|
|||||||
}
|
}
|
||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a command only if the binary exists and can be invoked with the given probe argument.
|
|
||||||
pub async fn new_checked_command<S: AsRef<OsStr>>(
|
|
||||||
program: S,
|
|
||||||
probe_arg: &str,
|
|
||||||
) -> io::Result<tokio::process::Command> {
|
|
||||||
let program: OsString = program.as_ref().to_os_string();
|
|
||||||
|
|
||||||
let mut probe = new_xplatform_command(&program);
|
|
||||||
probe.arg(probe_arg).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
|
|
||||||
|
|
||||||
let status = probe.status().await?;
|
|
||||||
if !status.success() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
ErrorKind::NotFound,
|
|
||||||
format!(
|
|
||||||
"'{}' is not available on PATH or failed to execute",
|
|
||||||
program.to_string_lossy()
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(new_xplatform_command(&program))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,10 +21,3 @@ pub fn get_str_map<'a>(v: &'a BTreeMap<String, Value>, key: &str) -> &'a str {
|
|||||||
Some(v) => v.as_str().unwrap_or_default(),
|
Some(v) => v.as_str().unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bool_map(v: &BTreeMap<String, Value>, key: &str, fallback: bool) -> bool {
|
|
||||||
match v.get(key) {
|
|
||||||
None => fallback,
|
|
||||||
Some(v) => v.as_bool().unwrap_or(fallback),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::error::Error::GitNotFound;
|
use crate::error::Error::GitNotFound;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::process::Stdio;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use yaak_common::command::new_checked_command;
|
use yaak_common::command::new_xplatform_command;
|
||||||
|
|
||||||
/// Create a git command that runs in the specified directory
|
/// Create a git command that runs in the specified directory
|
||||||
pub(crate) async fn new_binary_command(dir: &Path) -> Result<Command> {
|
pub(crate) async fn new_binary_command(dir: &Path) -> Result<Command> {
|
||||||
@@ -13,5 +14,17 @@ pub(crate) async fn new_binary_command(dir: &Path) -> Result<Command> {
|
|||||||
|
|
||||||
/// Create a git command without a specific directory (for global operations)
|
/// Create a git command without a specific directory (for global operations)
|
||||||
pub(crate) async fn new_binary_command_global() -> Result<Command> {
|
pub(crate) async fn new_binary_command_global() -> Result<Command> {
|
||||||
new_checked_command("git", "--version").await.map_err(|_| GitNotFound)
|
// 1. Probe that `git` exists and is runnable
|
||||||
|
let mut probe = new_xplatform_command("git");
|
||||||
|
probe.arg("--version").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
|
||||||
|
|
||||||
|
let status = probe.status().await.map_err(|_| GitNotFound)?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(GitNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Build the reusable git command
|
||||||
|
let cmd = new_xplatform_command("git");
|
||||||
|
Ok(cmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ mod tests {
|
|||||||
|
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
super::collect_any_types(json, &mut out);
|
super::collect_any_types(json, &mut out);
|
||||||
out.sort();
|
|
||||||
assert_eq!(out, vec!["foo.bar", "mount_source.MountSourceRBDVolume"]);
|
assert_eq!(out, vec!["foo.bar", "mount_source.MountSourceRBDVolume"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,5 +29,4 @@ tower-service = "0.3.3"
|
|||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
yaak-common = { workspace = true }
|
yaak-common = { workspace = true }
|
||||||
yaak-models = { workspace = true }
|
yaak-models = { workspace = true }
|
||||||
yaak-templates = { workspace = true }
|
|
||||||
yaak-tls = { workspace = true }
|
yaak-tls = { workspace = true }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use crate::client::HttpConnectionOptions;
|
use crate::client::HttpConnectionOptions;
|
||||||
use crate::dns::LocalhostResolver;
|
use crate::dns::LocalhostResolver;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use log::info;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -35,6 +36,7 @@ impl HttpConnectionManager {
|
|||||||
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
connections.retain(|_, (_, last_used)| last_used.elapsed() <= self.ttl);
|
||||||
|
|
||||||
if let Some((cached, last_used)) = connections.get_mut(&id) {
|
if let Some((cached, last_used)) = connections.get_mut(&id) {
|
||||||
|
info!("Re-using HTTP client {id}");
|
||||||
*last_used = Instant::now();
|
*last_used = Instant::now();
|
||||||
return Ok(CachedClient {
|
return Ok(CachedClient {
|
||||||
client: cached.client.clone(),
|
client: cached.client.clone(),
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ pub enum HttpResponseEvent {
|
|||||||
url: String,
|
url: String,
|
||||||
status: u16,
|
status: u16,
|
||||||
behavior: RedirectBehavior,
|
behavior: RedirectBehavior,
|
||||||
dropped_body: bool,
|
|
||||||
dropped_headers: Vec<String>,
|
|
||||||
},
|
},
|
||||||
SendUrl {
|
SendUrl {
|
||||||
method: String,
|
method: String,
|
||||||
@@ -69,28 +67,12 @@ impl Display for HttpResponseEvent {
|
|||||||
match self {
|
match self {
|
||||||
HttpResponseEvent::Setting(name, value) => write!(f, "* Setting {}={}", name, value),
|
HttpResponseEvent::Setting(name, value) => write!(f, "* Setting {}={}", name, value),
|
||||||
HttpResponseEvent::Info(s) => write!(f, "* {}", s),
|
HttpResponseEvent::Info(s) => write!(f, "* {}", s),
|
||||||
HttpResponseEvent::Redirect {
|
HttpResponseEvent::Redirect { url, status, behavior } => {
|
||||||
url,
|
|
||||||
status,
|
|
||||||
behavior,
|
|
||||||
dropped_body,
|
|
||||||
dropped_headers,
|
|
||||||
} => {
|
|
||||||
let behavior_str = match behavior {
|
let behavior_str = match behavior {
|
||||||
RedirectBehavior::Preserve => "preserve",
|
RedirectBehavior::Preserve => "preserve",
|
||||||
RedirectBehavior::DropBody => "drop body",
|
RedirectBehavior::DropBody => "drop body",
|
||||||
};
|
};
|
||||||
let body_str = if *dropped_body { ", body dropped" } else { "" };
|
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
|
||||||
let headers_str = if dropped_headers.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(", headers dropped: {}", dropped_headers.join(", "))
|
|
||||||
};
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"* Redirect {} -> {} ({}{}{})",
|
|
||||||
status, url, behavior_str, body_str, headers_str
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
HttpResponseEvent::SendUrl {
|
HttpResponseEvent::SendUrl {
|
||||||
method,
|
method,
|
||||||
@@ -148,21 +130,13 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
|||||||
match event {
|
match event {
|
||||||
HttpResponseEvent::Setting(name, value) => D::Setting { name, value },
|
HttpResponseEvent::Setting(name, value) => D::Setting { name, value },
|
||||||
HttpResponseEvent::Info(message) => D::Info { message },
|
HttpResponseEvent::Info(message) => D::Info { message },
|
||||||
HttpResponseEvent::Redirect {
|
HttpResponseEvent::Redirect { url, status, behavior } => D::Redirect {
|
||||||
url,
|
|
||||||
status,
|
|
||||||
behavior,
|
|
||||||
dropped_body,
|
|
||||||
dropped_headers,
|
|
||||||
} => D::Redirect {
|
|
||||||
url,
|
url,
|
||||||
status,
|
status,
|
||||||
behavior: match behavior {
|
behavior: match behavior {
|
||||||
RedirectBehavior::Preserve => "preserve".to_string(),
|
RedirectBehavior::Preserve => "preserve".to_string(),
|
||||||
RedirectBehavior::DropBody => "drop_body".to_string(),
|
RedirectBehavior::DropBody => "drop_body".to_string(),
|
||||||
},
|
},
|
||||||
dropped_body,
|
|
||||||
dropped_headers,
|
|
||||||
},
|
},
|
||||||
HttpResponseEvent::SendUrl {
|
HttpResponseEvent::SendUrl {
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::cookies::CookieStore;
|
use crate::cookies::CookieStore;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::sender::{HttpResponse, HttpResponseEvent, HttpSender, RedirectBehavior};
|
use crate::sender::{HttpResponse, HttpResponseEvent, HttpSender, RedirectBehavior};
|
||||||
use crate::types::{SendableBody, SendableHttpRequest};
|
use crate::types::SendableHttpRequest;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::watch::Receiver;
|
use tokio::sync::watch::Receiver;
|
||||||
@@ -87,11 +87,6 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Build request for this iteration
|
// Build request for this iteration
|
||||||
let preserved_body = match ¤t_body {
|
|
||||||
Some(SendableBody::Bytes(b)) => Some(SendableBody::Bytes(b.clone())),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let request_had_body = current_body.is_some();
|
|
||||||
let req = SendableHttpRequest {
|
let req = SendableHttpRequest {
|
||||||
url: current_url.clone(),
|
url: current_url.clone(),
|
||||||
method: current_method.clone(),
|
method: current_method.clone(),
|
||||||
@@ -187,6 +182,8 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
format!("{}/{}", base_path, location)
|
format!("{}/{}", base_path, location)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Self::remove_sensitive_headers(&mut current_headers, &previous_url, ¤t_url);
|
||||||
|
|
||||||
// Determine redirect behavior based on status code and method
|
// Determine redirect behavior based on status code and method
|
||||||
let behavior = if status == 303 {
|
let behavior = if status == 303 {
|
||||||
// 303 See Other always changes to GET
|
// 303 See Other always changes to GET
|
||||||
@@ -200,8 +197,11 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
RedirectBehavior::Preserve
|
RedirectBehavior::Preserve
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut dropped_headers =
|
send_event(HttpResponseEvent::Redirect {
|
||||||
Self::remove_sensitive_headers(&mut current_headers, &previous_url, ¤t_url);
|
url: current_url.clone(),
|
||||||
|
status,
|
||||||
|
behavior: behavior.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
// Handle method changes for certain redirect codes
|
// Handle method changes for certain redirect codes
|
||||||
if matches!(behavior, RedirectBehavior::DropBody) {
|
if matches!(behavior, RedirectBehavior::DropBody) {
|
||||||
@@ -211,40 +211,13 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
// Remove content-related headers
|
// Remove content-related headers
|
||||||
current_headers.retain(|h| {
|
current_headers.retain(|h| {
|
||||||
let name_lower = h.0.to_lowercase();
|
let name_lower = h.0.to_lowercase();
|
||||||
let should_drop =
|
!name_lower.starts_with("content-") && name_lower != "transfer-encoding"
|
||||||
name_lower.starts_with("content-") || name_lower == "transfer-encoding";
|
|
||||||
if should_drop {
|
|
||||||
Self::push_header_if_missing(&mut dropped_headers, &h.0);
|
|
||||||
}
|
|
||||||
!should_drop
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore body for Preserve redirects (307/308), drop for others.
|
// Reset body for next iteration (since it was moved in the send call)
|
||||||
// Stream bodies can't be replayed (same limitation as reqwest).
|
// For redirects that change method to GET or for all redirects since body was consumed
|
||||||
current_body = if matches!(behavior, RedirectBehavior::Preserve) {
|
current_body = None;
|
||||||
if request_had_body && preserved_body.is_none() {
|
|
||||||
// Stream body was consumed and can't be replayed (same as reqwest)
|
|
||||||
return Err(crate::error::Error::RequestError(
|
|
||||||
"Cannot follow redirect: request body was a stream and cannot be resent"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
preserved_body
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
// Body was dropped if the request had one but we can't resend it
|
|
||||||
let dropped_body = request_had_body && current_body.is_none();
|
|
||||||
|
|
||||||
send_event(HttpResponseEvent::Redirect {
|
|
||||||
url: current_url.clone(),
|
|
||||||
status,
|
|
||||||
behavior: behavior.clone(),
|
|
||||||
dropped_body,
|
|
||||||
dropped_headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
redirect_count += 1;
|
redirect_count += 1;
|
||||||
}
|
}
|
||||||
@@ -258,8 +231,7 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
headers: &mut Vec<(String, String)>,
|
headers: &mut Vec<(String, String)>,
|
||||||
previous_url: &str,
|
previous_url: &str,
|
||||||
next_url: &str,
|
next_url: &str,
|
||||||
) -> Vec<String> {
|
) {
|
||||||
let mut dropped_headers = Vec::new();
|
|
||||||
let previous_host = Url::parse(previous_url).ok().and_then(|u| {
|
let previous_host = Url::parse(previous_url).ok().and_then(|u| {
|
||||||
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
|
u.host_str().map(|h| format!("{}:{}", h, u.port_or_known_default().unwrap_or(0)))
|
||||||
});
|
});
|
||||||
@@ -269,24 +241,13 @@ impl<S: HttpSender> HttpTransaction<S> {
|
|||||||
if previous_host != next_host {
|
if previous_host != next_host {
|
||||||
headers.retain(|h| {
|
headers.retain(|h| {
|
||||||
let name_lower = h.0.to_lowercase();
|
let name_lower = h.0.to_lowercase();
|
||||||
let should_drop = name_lower == "authorization"
|
name_lower != "authorization"
|
||||||
|| name_lower == "cookie"
|
&& name_lower != "cookie"
|
||||||
|| name_lower == "cookie2"
|
&& name_lower != "cookie2"
|
||||||
|| name_lower == "proxy-authorization"
|
&& name_lower != "proxy-authorization"
|
||||||
|| name_lower == "www-authenticate";
|
&& name_lower != "www-authenticate"
|
||||||
if should_drop {
|
|
||||||
Self::push_header_if_missing(&mut dropped_headers, &h.0);
|
|
||||||
}
|
|
||||||
!should_drop
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dropped_headers
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_header_if_missing(headers: &mut Vec<String>, name: &str) {
|
|
||||||
if !headers.iter().any(|h| h.eq_ignore_ascii_case(name)) {
|
|
||||||
headers.push(name.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a status code indicates a redirect
|
/// Check if a status code indicates a redirect
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ use std::collections::BTreeMap;
|
|||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::io::AsyncRead;
|
use tokio::io::AsyncRead;
|
||||||
use yaak_common::serde::{get_bool, get_bool_map, get_str, get_str_map};
|
use yaak_common::serde::{get_bool, get_str, get_str_map};
|
||||||
use yaak_models::models::HttpRequest;
|
use yaak_models::models::HttpRequest;
|
||||||
use yaak_templates::strip_json_comments::{maybe_strip_json_comments, strip_json_comments};
|
|
||||||
|
|
||||||
pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary";
|
pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary";
|
||||||
|
|
||||||
@@ -135,69 +134,16 @@ pub fn append_query_params(url: &str, params: Vec<(String, String)>) -> String {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn strip_query_params(url: &str, names: &[&str]) -> String {
|
|
||||||
// Split off fragment
|
|
||||||
let (base_and_query, fragment) = if let Some(hash_pos) = url.find('#') {
|
|
||||||
(&url[..hash_pos], Some(&url[hash_pos..]))
|
|
||||||
} else {
|
|
||||||
(url, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = if let Some(q_pos) = base_and_query.find('?') {
|
|
||||||
let base = &base_and_query[..q_pos];
|
|
||||||
let query = &base_and_query[q_pos + 1..];
|
|
||||||
let filtered: Vec<&str> = query
|
|
||||||
.split('&')
|
|
||||||
.filter(|pair| {
|
|
||||||
let key = pair.split('=').next().unwrap_or("");
|
|
||||||
let decoded = urlencoding::decode(key).unwrap_or_default();
|
|
||||||
!names.contains(&decoded.as_ref())
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if filtered.is_empty() {
|
|
||||||
base.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{}?{}", base, filtered.join("&"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
base_and_query.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
match fragment {
|
|
||||||
Some(f) => format!("{}{}", result, f),
|
|
||||||
None => result,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_url(r: &HttpRequest) -> String {
|
fn build_url(r: &HttpRequest) -> String {
|
||||||
let (url_string, params) = apply_path_placeholders(&ensure_proto(&r.url), &r.url_parameters);
|
let (url_string, params) = apply_path_placeholders(&ensure_proto(&r.url), &r.url_parameters);
|
||||||
let mut url = append_query_params(
|
append_query_params(
|
||||||
&url_string,
|
&url_string,
|
||||||
params
|
params
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|p| p.enabled && !p.name.is_empty())
|
.filter(|p| p.enabled && !p.name.is_empty())
|
||||||
.map(|p| (p.name.clone(), p.value.clone()))
|
.map(|p| (p.name.clone(), p.value.clone()))
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
)
|
||||||
|
|
||||||
// GraphQL GET requests encode query/variables as URL query parameters
|
|
||||||
if r.method.to_lowercase() == "get" && r.body_type.as_deref() == Some("graphql") {
|
|
||||||
url = append_graphql_query_params(&url, &r.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
url
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append_graphql_query_params(url: &str, body: &BTreeMap<String, serde_json::Value>) -> String {
|
|
||||||
let query = get_str_map(body, "query").to_string();
|
|
||||||
let variables = strip_json_comments(&get_str_map(body, "variables"));
|
|
||||||
let mut params = vec![("query".to_string(), query)];
|
|
||||||
if !variables.trim().is_empty() {
|
|
||||||
params.push(("variables".to_string(), variables));
|
|
||||||
}
|
|
||||||
// Strip existing query/variables params to avoid duplicates
|
|
||||||
let url = strip_query_params(url, &["query", "variables"]);
|
|
||||||
append_query_params(&url, params)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_headers(r: &HttpRequest) -> Vec<(String, String)> {
|
fn build_headers(r: &HttpRequest) -> Vec<(String, String)> {
|
||||||
@@ -231,7 +177,7 @@ async fn build_body(
|
|||||||
(build_form_body(&body), Some("application/x-www-form-urlencoded".to_string()))
|
(build_form_body(&body), Some("application/x-www-form-urlencoded".to_string()))
|
||||||
}
|
}
|
||||||
"multipart/form-data" => build_multipart_body(&body, &headers).await?,
|
"multipart/form-data" => build_multipart_body(&body, &headers).await?,
|
||||||
_ if body.contains_key("text") => (build_text_body(&body, body_type), None),
|
_ if body.contains_key("text") => (build_text_body(&body), None),
|
||||||
t => {
|
t => {
|
||||||
warn!("Unsupported body type: {}", t);
|
warn!("Unsupported body type: {}", t);
|
||||||
(None, None)
|
(None, None)
|
||||||
@@ -306,20 +252,13 @@ async fn build_binary_body(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_text_body(body: &BTreeMap<String, serde_json::Value>, body_type: &str) -> Option<SendableBodyWithMeta> {
|
fn build_text_body(body: &BTreeMap<String, serde_json::Value>) -> Option<SendableBodyWithMeta> {
|
||||||
let text = get_str_map(body, "text");
|
let text = get_str_map(body, "text");
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
return None;
|
None
|
||||||
}
|
|
||||||
|
|
||||||
let send_comments = get_bool_map(body, "sendJsonComments", false);
|
|
||||||
let text = if !send_comments && body_type == "application/json" {
|
|
||||||
maybe_strip_json_comments(text)
|
|
||||||
} else {
|
} else {
|
||||||
text.to_string()
|
Some(SendableBodyWithMeta::Bytes(Bytes::from(text.to_string())))
|
||||||
};
|
}
|
||||||
|
|
||||||
Some(SendableBodyWithMeta::Bytes(Bytes::from(text)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_graphql_body(
|
fn build_graphql_body(
|
||||||
@@ -327,7 +266,7 @@ fn build_graphql_body(
|
|||||||
body: &BTreeMap<String, serde_json::Value>,
|
body: &BTreeMap<String, serde_json::Value>,
|
||||||
) -> Option<SendableBodyWithMeta> {
|
) -> Option<SendableBodyWithMeta> {
|
||||||
let query = get_str_map(body, "query");
|
let query = get_str_map(body, "query");
|
||||||
let variables = strip_json_comments(&get_str_map(body, "variables"));
|
let variables = get_str_map(body, "variables");
|
||||||
|
|
||||||
if method.to_lowercase() == "get" {
|
if method.to_lowercase() == "get" {
|
||||||
// GraphQL GET requests use query parameters, not a body
|
// GraphQL GET requests use query parameters, not a body
|
||||||
@@ -745,7 +684,7 @@ mod tests {
|
|||||||
let mut body = BTreeMap::new();
|
let mut body = BTreeMap::new();
|
||||||
body.insert("text".to_string(), json!("Hello, World!"));
|
body.insert("text".to_string(), json!("Hello, World!"));
|
||||||
|
|
||||||
let result = build_text_body(&body, "application/json");
|
let result = build_text_body(&body);
|
||||||
match result {
|
match result {
|
||||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||||
assert_eq!(bytes, Bytes::from("Hello, World!"))
|
assert_eq!(bytes, Bytes::from("Hello, World!"))
|
||||||
@@ -759,7 +698,7 @@ mod tests {
|
|||||||
let mut body = BTreeMap::new();
|
let mut body = BTreeMap::new();
|
||||||
body.insert("text".to_string(), json!(""));
|
body.insert("text".to_string(), json!(""));
|
||||||
|
|
||||||
let result = build_text_body(&body, "application/json");
|
let result = build_text_body(&body);
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,57 +706,10 @@ mod tests {
|
|||||||
async fn test_text_body_missing() {
|
async fn test_text_body_missing() {
|
||||||
let body = BTreeMap::new();
|
let body = BTreeMap::new();
|
||||||
|
|
||||||
let result = build_text_body(&body, "application/json");
|
let result = build_text_body(&body);
|
||||||
assert!(result.is_none());
|
assert!(result.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_text_body_strips_json_comments_by_default() {
|
|
||||||
let mut body = BTreeMap::new();
|
|
||||||
body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}"));
|
|
||||||
|
|
||||||
let result = build_text_body(&body, "application/json");
|
|
||||||
match result {
|
|
||||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
|
||||||
let text = String::from_utf8_lossy(&bytes);
|
|
||||||
assert!(!text.contains("// comment"));
|
|
||||||
assert!(text.contains("\"foo\": \"bar\""));
|
|
||||||
}
|
|
||||||
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_text_body_send_json_comments_when_opted_in() {
|
|
||||||
let mut body = BTreeMap::new();
|
|
||||||
body.insert("text".to_string(), json!("{\n // comment\n \"foo\": \"bar\"\n}"));
|
|
||||||
body.insert("sendJsonComments".to_string(), json!(true));
|
|
||||||
|
|
||||||
let result = build_text_body(&body, "application/json");
|
|
||||||
match result {
|
|
||||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
|
||||||
let text = String::from_utf8_lossy(&bytes);
|
|
||||||
assert!(text.contains("// comment"));
|
|
||||||
}
|
|
||||||
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_text_body_no_strip_for_non_json() {
|
|
||||||
let mut body = BTreeMap::new();
|
|
||||||
body.insert("text".to_string(), json!("// not json\nsome text"));
|
|
||||||
|
|
||||||
let result = build_text_body(&body, "text/plain");
|
|
||||||
match result {
|
|
||||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
|
||||||
let text = String::from_utf8_lossy(&bytes);
|
|
||||||
assert!(text.contains("// not json"));
|
|
||||||
}
|
|
||||||
_ => panic!("Expected Some(SendableBody::Bytes)"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_form_urlencoded_body() -> Result<()> {
|
async fn test_form_urlencoded_body() -> Result<()> {
|
||||||
let mut body = BTreeMap::new();
|
let mut body = BTreeMap::new();
|
||||||
|
|||||||
6
crates/yaak-models/bindings/gen_models.ts
generated
6
crates/yaak-models/bindings/gen_models.ts
generated
@@ -49,7 +49,7 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
@@ -67,9 +67,7 @@ export type ParentAuthentication = { authentication: Record<string, any>, authen
|
|||||||
|
|
||||||
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
|
export type ParentHeaders = { headers: Array<HttpRequestHeader>, };
|
||||||
|
|
||||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
|
||||||
|
|
||||||
export type PluginSource = "bundled" | "filesystem" | "registry";
|
|
||||||
|
|
||||||
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
ALTER TABLE plugins
|
|
||||||
ADD COLUMN source TEXT DEFAULT 'filesystem' NOT NULL;
|
|
||||||
|
|
||||||
-- Existing registry installs have a URL; classify them first.
|
|
||||||
UPDATE plugins
|
|
||||||
SET source = 'registry'
|
|
||||||
WHERE url IS NOT NULL;
|
|
||||||
|
|
||||||
-- Best-effort bundled backfill for legacy rows.
|
|
||||||
UPDATE plugins
|
|
||||||
SET source = 'bundled'
|
|
||||||
WHERE source = 'filesystem'
|
|
||||||
AND (
|
|
||||||
-- Normalize separators so this also works for Windows paths.
|
|
||||||
replace(directory, '\', '/') LIKE '%/vendored/plugins/%'
|
|
||||||
OR replace(directory, '\', '/') LIKE '%/vendored-plugins/%'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Keep one row per exact directory before adding uniqueness.
|
|
||||||
-- Tie-break by recency.
|
|
||||||
WITH ranked AS (SELECT id,
|
|
||||||
ROW_NUMBER() OVER (
|
|
||||||
PARTITION BY directory
|
|
||||||
ORDER BY updated_at DESC,
|
|
||||||
created_at DESC
|
|
||||||
) AS row_num
|
|
||||||
FROM plugins)
|
|
||||||
DELETE
|
|
||||||
FROM plugins
|
|
||||||
WHERE id IN (SELECT id FROM ranked WHERE row_num > 1);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_plugins_directory_unique
|
|
||||||
ON plugins (directory);
|
|
||||||
@@ -74,7 +74,7 @@ pub struct ClientCertificate {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
pub struct DnsOverride {
|
pub struct DnsOverride {
|
||||||
@@ -293,7 +293,7 @@ impl UpsertModelInfo for Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
#[enum_def(table_name = "workspaces")]
|
#[enum_def(table_name = "workspaces")]
|
||||||
@@ -590,7 +590,7 @@ impl UpsertModelInfo for CookieJar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
#[enum_def(table_name = "environments")]
|
#[enum_def(table_name = "environments")]
|
||||||
@@ -611,8 +611,6 @@ pub struct Environment {
|
|||||||
pub base: bool,
|
pub base: bool,
|
||||||
pub parent_model: String,
|
pub parent_model: String,
|
||||||
pub parent_id: Option<String>,
|
pub parent_id: Option<String>,
|
||||||
/// Variables defined in this environment scope.
|
|
||||||
/// Child environments override parent variables by name.
|
|
||||||
pub variables: Vec<EnvironmentVariable>,
|
pub variables: Vec<EnvironmentVariable>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
@@ -700,7 +698,7 @@ impl UpsertModelInfo for Environment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, JsonSchema, TS)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
#[ts(export, export_to = "gen_models.ts")]
|
||||||
pub struct EnvironmentVariable {
|
pub struct EnvironmentVariable {
|
||||||
@@ -847,8 +845,6 @@ pub struct HttpUrlParameter {
|
|||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
#[ts(optional, as = "Option<bool>")]
|
#[ts(optional, as = "Option<bool>")]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
/// Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
|
||||||
/// Other entries are appended as query parameters
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
#[ts(optional, as = "Option<String>")]
|
#[ts(optional, as = "Option<String>")]
|
||||||
@@ -881,7 +877,6 @@ pub struct HttpRequest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
|
||||||
pub url_parameters: Vec<HttpUrlParameter>,
|
pub url_parameters: Vec<HttpUrlParameter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1123,7 +1118,6 @@ pub struct WebsocketRequest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
pub url: String,
|
pub url: String,
|
||||||
/// URL parameters used for both path placeholders (`:id`) and query string entries.
|
|
||||||
pub url_parameters: Vec<HttpUrlParameter>,
|
pub url_parameters: Vec<HttpUrlParameter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1499,10 +1493,6 @@ pub enum HttpResponseEventData {
|
|||||||
url: String,
|
url: String,
|
||||||
status: u16,
|
status: u16,
|
||||||
behavior: String,
|
behavior: String,
|
||||||
#[serde(default)]
|
|
||||||
dropped_body: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
dropped_headers: Vec<String>,
|
|
||||||
},
|
},
|
||||||
SendUrl {
|
SendUrl {
|
||||||
method: String,
|
method: String,
|
||||||
@@ -1738,7 +1728,6 @@ pub struct GrpcRequest {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub service: Option<String>,
|
pub service: Option<String>,
|
||||||
pub sort_priority: f64,
|
pub sort_priority: f64,
|
||||||
/// Server URL (http for plaintext or https for secure)
|
|
||||||
pub url: String,
|
pub url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2078,46 +2067,6 @@ pub struct Plugin {
|
|||||||
pub directory: String,
|
pub directory: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub source: PluginSource,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
#[ts(export, export_to = "gen_models.ts")]
|
|
||||||
pub enum PluginSource {
|
|
||||||
Bundled,
|
|
||||||
Filesystem,
|
|
||||||
Registry,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for PluginSource {
|
|
||||||
type Err = crate::error::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"bundled" => Ok(Self::Bundled),
|
|
||||||
"filesystem" => Ok(Self::Filesystem),
|
|
||||||
"registry" => Ok(Self::Registry),
|
|
||||||
_ => Ok(Self::default()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for PluginSource {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
let str = match self {
|
|
||||||
PluginSource::Bundled => "bundled".to_string(),
|
|
||||||
PluginSource::Filesystem => "filesystem".to_string(),
|
|
||||||
PluginSource::Registry => "registry".to_string(),
|
|
||||||
};
|
|
||||||
write!(f, "{}", str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PluginSource {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Filesystem
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Plugin {
|
impl UpsertModelInfo for Plugin {
|
||||||
@@ -2153,7 +2102,6 @@ impl UpsertModelInfo for Plugin {
|
|||||||
(Directory, self.directory.into()),
|
(Directory, self.directory.into()),
|
||||||
(Url, self.url.into()),
|
(Url, self.url.into()),
|
||||||
(Enabled, self.enabled.into()),
|
(Enabled, self.enabled.into()),
|
||||||
(Source, self.source.to_string().into()),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2164,7 +2112,6 @@ impl UpsertModelInfo for Plugin {
|
|||||||
PluginIden::Directory,
|
PluginIden::Directory,
|
||||||
PluginIden::Url,
|
PluginIden::Url,
|
||||||
PluginIden::Enabled,
|
PluginIden::Enabled,
|
||||||
PluginIden::Source,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2181,7 +2128,6 @@ impl UpsertModelInfo for Plugin {
|
|||||||
url: row.get("url")?,
|
url: row.get("url")?,
|
||||||
directory: row.get("directory")?,
|
directory: row.get("directory")?,
|
||||||
enabled: row.get("enabled")?,
|
enabled: row.get("enabled")?,
|
||||||
source: PluginSource::from_str(row.get::<_, String>("source")?.as_str()).unwrap(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,6 @@ impl<'a> DbContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
|
pub fn upsert_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
|
||||||
let mut plugin_to_upsert = plugin.clone();
|
self.upsert(plugin, source)
|
||||||
if let Some(existing) = self.get_plugin_by_directory(&plugin.directory) {
|
|
||||||
plugin_to_upsert.id = existing.id;
|
|
||||||
}
|
|
||||||
self.upsert(&plugin_to_upsert, source)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ log = { workspace = true }
|
|||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
path-slash = "0.2.1"
|
path-slash = "0.2.1"
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
|
regex = "1.10.6"
|
||||||
reqwest = { workspace = true, features = ["json"] }
|
reqwest = { workspace = true, features = ["json"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
160
crates/yaak-plugins/bindings/gen_events.ts
generated
160
crates/yaak-plugins/bindings/gen_events.ts
generated
@@ -18,12 +18,12 @@ export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId:
|
|||||||
|
|
||||||
export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
|
export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
|
||||||
|
|
||||||
export type CallHttpAuthenticationResponse = {
|
export type CallHttpAuthenticationResponse = {
|
||||||
/**
|
/**
|
||||||
* HTTP headers to add to the request. Existing headers will be replaced, while
|
* HTTP headers to add to the request. Existing headers will be replaced, while
|
||||||
* new headers will be added.
|
* new headers will be added.
|
||||||
*/
|
*/
|
||||||
setHeaders?: Array<HttpHeader>,
|
setHeaders?: Array<HttpHeader>,
|
||||||
/**
|
/**
|
||||||
* Query parameters to add to the request. Existing params will be replaced, while
|
* Query parameters to add to the request. Existing params will be replaced, while
|
||||||
* new params will be added.
|
* new params will be added.
|
||||||
@@ -78,7 +78,7 @@ export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
|
|||||||
|
|
||||||
export type ExportHttpRequestResponse = { content: string, };
|
export type ExportHttpRequestResponse = { content: string, };
|
||||||
|
|
||||||
export type FileFilter = { name: string,
|
export type FileFilter = { name: string,
|
||||||
/**
|
/**
|
||||||
* File extensions to require
|
* File extensions to require
|
||||||
*/
|
*/
|
||||||
@@ -100,149 +100,149 @@ export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hid
|
|||||||
|
|
||||||
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
|
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
|
||||||
|
|
||||||
export type FormInputBase = {
|
export type FormInputBase = {
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
/**
|
/**
|
||||||
* Whether this input is visible for the given configuration. Use this to
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
* make branching forms.
|
* make branching forms.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether the user must fill in the argument
|
* Whether the user must fill in the argument
|
||||||
*/
|
*/
|
||||||
optional?: boolean,
|
optional?: boolean,
|
||||||
/**
|
/**
|
||||||
* The label of the input
|
* The label of the input
|
||||||
*/
|
*/
|
||||||
label?: string,
|
label?: string,
|
||||||
/**
|
/**
|
||||||
* Visually hide the label of the input
|
* Visually hide the label of the input
|
||||||
*/
|
*/
|
||||||
hideLabel?: boolean,
|
hideLabel?: boolean,
|
||||||
/**
|
/**
|
||||||
* The default value
|
* The default value
|
||||||
*/
|
*/
|
||||||
defaultValue?: string, disabled?: boolean,
|
defaultValue?: string, disabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* Longer description of the input, likely shown in a tooltip
|
* Longer description of the input, likely shown in a tooltip
|
||||||
*/
|
*/
|
||||||
description?: string, };
|
description?: string, };
|
||||||
|
|
||||||
export type FormInputCheckbox = {
|
export type FormInputCheckbox = {
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
/**
|
/**
|
||||||
* Whether this input is visible for the given configuration. Use this to
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
* make branching forms.
|
* make branching forms.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether the user must fill in the argument
|
* Whether the user must fill in the argument
|
||||||
*/
|
*/
|
||||||
optional?: boolean,
|
optional?: boolean,
|
||||||
/**
|
/**
|
||||||
* The label of the input
|
* The label of the input
|
||||||
*/
|
*/
|
||||||
label?: string,
|
label?: string,
|
||||||
/**
|
/**
|
||||||
* Visually hide the label of the input
|
* Visually hide the label of the input
|
||||||
*/
|
*/
|
||||||
hideLabel?: boolean,
|
hideLabel?: boolean,
|
||||||
/**
|
/**
|
||||||
* The default value
|
* The default value
|
||||||
*/
|
*/
|
||||||
defaultValue?: string, disabled?: boolean,
|
defaultValue?: string, disabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* Longer description of the input, likely shown in a tooltip
|
* Longer description of the input, likely shown in a tooltip
|
||||||
*/
|
*/
|
||||||
description?: string, };
|
description?: string, };
|
||||||
|
|
||||||
export type FormInputEditor = {
|
export type FormInputEditor = {
|
||||||
/**
|
/**
|
||||||
* Placeholder for the text input
|
* Placeholder for the text input
|
||||||
*/
|
*/
|
||||||
placeholder?: string | null,
|
placeholder?: string | null,
|
||||||
/**
|
/**
|
||||||
* Don't show the editor gutter (line numbers, folds, etc.)
|
* Don't show the editor gutter (line numbers, folds, etc.)
|
||||||
*/
|
*/
|
||||||
hideGutter?: boolean,
|
hideGutter?: boolean,
|
||||||
/**
|
/**
|
||||||
* Language for syntax highlighting
|
* Language for syntax highlighting
|
||||||
*/
|
*/
|
||||||
language?: EditorLanguage, readOnly?: boolean,
|
language?: EditorLanguage, readOnly?: boolean,
|
||||||
/**
|
/**
|
||||||
* Fixed number of visible rows
|
* Fixed number of visible rows
|
||||||
*/
|
*/
|
||||||
rows?: number, completionOptions?: Array<GenericCompletionOption>,
|
rows?: number, completionOptions?: Array<GenericCompletionOption>,
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
/**
|
/**
|
||||||
* Whether this input is visible for the given configuration. Use this to
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
* make branching forms.
|
* make branching forms.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether the user must fill in the argument
|
* Whether the user must fill in the argument
|
||||||
*/
|
*/
|
||||||
optional?: boolean,
|
optional?: boolean,
|
||||||
/**
|
/**
|
||||||
* The label of the input
|
* The label of the input
|
||||||
*/
|
*/
|
||||||
label?: string,
|
label?: string,
|
||||||
/**
|
/**
|
||||||
* Visually hide the label of the input
|
* Visually hide the label of the input
|
||||||
*/
|
*/
|
||||||
hideLabel?: boolean,
|
hideLabel?: boolean,
|
||||||
/**
|
/**
|
||||||
* The default value
|
* The default value
|
||||||
*/
|
*/
|
||||||
defaultValue?: string, disabled?: boolean,
|
defaultValue?: string, disabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* Longer description of the input, likely shown in a tooltip
|
* Longer description of the input, likely shown in a tooltip
|
||||||
*/
|
*/
|
||||||
description?: string, };
|
description?: string, };
|
||||||
|
|
||||||
export type FormInputFile = {
|
export type FormInputFile = {
|
||||||
/**
|
/**
|
||||||
* The title of the file selection window
|
* The title of the file selection window
|
||||||
*/
|
*/
|
||||||
title: string,
|
title: string,
|
||||||
/**
|
/**
|
||||||
* Allow selecting multiple files
|
* Allow selecting multiple files
|
||||||
*/
|
*/
|
||||||
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
|
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
/**
|
/**
|
||||||
* Whether this input is visible for the given configuration. Use this to
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
* make branching forms.
|
* make branching forms.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether the user must fill in the argument
|
* Whether the user must fill in the argument
|
||||||
*/
|
*/
|
||||||
optional?: boolean,
|
optional?: boolean,
|
||||||
/**
|
/**
|
||||||
* The label of the input
|
* The label of the input
|
||||||
*/
|
*/
|
||||||
label?: string,
|
label?: string,
|
||||||
/**
|
/**
|
||||||
* Visually hide the label of the input
|
* Visually hide the label of the input
|
||||||
*/
|
*/
|
||||||
hideLabel?: boolean,
|
hideLabel?: boolean,
|
||||||
/**
|
/**
|
||||||
* The default value
|
* The default value
|
||||||
*/
|
*/
|
||||||
defaultValue?: string, disabled?: boolean,
|
defaultValue?: string, disabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* Longer description of the input, likely shown in a tooltip
|
* Longer description of the input, likely shown in a tooltip
|
||||||
*/
|
*/
|
||||||
@@ -250,63 +250,63 @@ description?: string, };
|
|||||||
|
|
||||||
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, };
|
export type FormInputHStack = { inputs?: Array<FormInput>, hidden?: boolean, };
|
||||||
|
|
||||||
export type FormInputHttpRequest = {
|
export type FormInputHttpRequest = {
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
/**
|
/**
|
||||||
* Whether this input is visible for the given configuration. Use this to
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
* make branching forms.
|
* make branching forms.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether the user must fill in the argument
|
* Whether the user must fill in the argument
|
||||||
*/
|
*/
|
||||||
optional?: boolean,
|
optional?: boolean,
|
||||||
/**
|
/**
|
||||||
* The label of the input
|
* The label of the input
|
||||||
*/
|
*/
|
||||||
label?: string,
|
label?: string,
|
||||||
/**
|
/**
|
||||||
* Visually hide the label of the input
|
* Visually hide the label of the input
|
||||||
*/
|
*/
|
||||||
hideLabel?: boolean,
|
hideLabel?: boolean,
|
||||||
/**
|
/**
|
||||||
* The default value
|
* The default value
|
||||||
*/
|
*/
|
||||||
defaultValue?: string, disabled?: boolean,
|
defaultValue?: string, disabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* Longer description of the input, likely shown in a tooltip
|
* Longer description of the input, likely shown in a tooltip
|
||||||
*/
|
*/
|
||||||
description?: string, };
|
description?: string, };
|
||||||
|
|
||||||
export type FormInputKeyValue = {
|
export type FormInputKeyValue = {
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
/**
|
/**
|
||||||
* Whether this input is visible for the given configuration. Use this to
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
* make branching forms.
|
* make branching forms.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether the user must fill in the argument
|
* Whether the user must fill in the argument
|
||||||
*/
|
*/
|
||||||
optional?: boolean,
|
optional?: boolean,
|
||||||
/**
|
/**
|
||||||
* The label of the input
|
* The label of the input
|
||||||
*/
|
*/
|
||||||
label?: string,
|
label?: string,
|
||||||
/**
|
/**
|
||||||
* Visually hide the label of the input
|
* Visually hide the label of the input
|
||||||
*/
|
*/
|
||||||
hideLabel?: boolean,
|
hideLabel?: boolean,
|
||||||
/**
|
/**
|
||||||
* The default value
|
* The default value
|
||||||
*/
|
*/
|
||||||
defaultValue?: string, disabled?: boolean,
|
defaultValue?: string, disabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* Longer description of the input, likely shown in a tooltip
|
* Longer description of the input, likely shown in a tooltip
|
||||||
*/
|
*/
|
||||||
@@ -314,36 +314,36 @@ description?: string, };
|
|||||||
|
|
||||||
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
export type FormInputMarkdown = { content: string, hidden?: boolean, };
|
||||||
|
|
||||||
export type FormInputSelect = {
|
export type FormInputSelect = {
|
||||||
/**
|
/**
|
||||||
* The options that will be available in the select input
|
* The options that will be available in the select input
|
||||||
*/
|
*/
|
||||||
options: Array<FormInputSelectOption>,
|
options: Array<FormInputSelectOption>,
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
/**
|
/**
|
||||||
* Whether this input is visible for the given configuration. Use this to
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
* make branching forms.
|
* make branching forms.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether the user must fill in the argument
|
* Whether the user must fill in the argument
|
||||||
*/
|
*/
|
||||||
optional?: boolean,
|
optional?: boolean,
|
||||||
/**
|
/**
|
||||||
* The label of the input
|
* The label of the input
|
||||||
*/
|
*/
|
||||||
label?: string,
|
label?: string,
|
||||||
/**
|
/**
|
||||||
* Visually hide the label of the input
|
* Visually hide the label of the input
|
||||||
*/
|
*/
|
||||||
hideLabel?: boolean,
|
hideLabel?: boolean,
|
||||||
/**
|
/**
|
||||||
* The default value
|
* The default value
|
||||||
*/
|
*/
|
||||||
defaultValue?: string, disabled?: boolean,
|
defaultValue?: string, disabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* Longer description of the input, likely shown in a tooltip
|
* Longer description of the input, likely shown in a tooltip
|
||||||
*/
|
*/
|
||||||
@@ -351,44 +351,44 @@ description?: string, };
|
|||||||
|
|
||||||
export type FormInputSelectOption = { label: string, value: string, };
|
export type FormInputSelectOption = { label: string, value: string, };
|
||||||
|
|
||||||
export type FormInputText = {
|
export type FormInputText = {
|
||||||
/**
|
/**
|
||||||
* Placeholder for the text input
|
* Placeholder for the text input
|
||||||
*/
|
*/
|
||||||
placeholder?: string | null,
|
placeholder?: string | null,
|
||||||
/**
|
/**
|
||||||
* Placeholder for the text input
|
* Placeholder for the text input
|
||||||
*/
|
*/
|
||||||
password?: boolean,
|
password?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether to allow newlines in the input, like a <textarea/>
|
* Whether to allow newlines in the input, like a <textarea/>
|
||||||
*/
|
*/
|
||||||
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
|
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
|
||||||
/**
|
/**
|
||||||
* The name of the input. The value will be stored at this object attribute in the resulting data
|
* The name of the input. The value will be stored at this object attribute in the resulting data
|
||||||
*/
|
*/
|
||||||
name: string,
|
name: string,
|
||||||
/**
|
/**
|
||||||
* Whether this input is visible for the given configuration. Use this to
|
* Whether this input is visible for the given configuration. Use this to
|
||||||
* make branching forms.
|
* make branching forms.
|
||||||
*/
|
*/
|
||||||
hidden?: boolean,
|
hidden?: boolean,
|
||||||
/**
|
/**
|
||||||
* Whether the user must fill in the argument
|
* Whether the user must fill in the argument
|
||||||
*/
|
*/
|
||||||
optional?: boolean,
|
optional?: boolean,
|
||||||
/**
|
/**
|
||||||
* The label of the input
|
* The label of the input
|
||||||
*/
|
*/
|
||||||
label?: string,
|
label?: string,
|
||||||
/**
|
/**
|
||||||
* Visually hide the label of the input
|
* Visually hide the label of the input
|
||||||
*/
|
*/
|
||||||
hideLabel?: boolean,
|
hideLabel?: boolean,
|
||||||
/**
|
/**
|
||||||
* The default value
|
* The default value
|
||||||
*/
|
*/
|
||||||
defaultValue?: string, disabled?: boolean,
|
defaultValue?: string, disabled?: boolean,
|
||||||
/**
|
/**
|
||||||
* Longer description of the input, likely shown in a tooltip
|
* Longer description of the input, likely shown in a tooltip
|
||||||
*/
|
*/
|
||||||
@@ -474,7 +474,7 @@ export type ListOpenWorkspacesResponse = { workspaces: Array<WorkspaceInfo>, };
|
|||||||
|
|
||||||
export type OpenExternalUrlRequest = { url: string, };
|
export type OpenExternalUrlRequest = { url: string, };
|
||||||
|
|
||||||
export type OpenWindowRequest = { url: string,
|
export type OpenWindowRequest = { url: string,
|
||||||
/**
|
/**
|
||||||
* Label for the window. If not provided, a random one will be generated.
|
* Label for the window. If not provided, a random one will be generated.
|
||||||
*/
|
*/
|
||||||
@@ -486,15 +486,15 @@ export type PromptFormRequest = { id: string, title: string, description?: strin
|
|||||||
|
|
||||||
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, done?: boolean, };
|
export type PromptFormResponse = { values: { [key in string]?: JsonPrimitive } | null, done?: boolean, };
|
||||||
|
|
||||||
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
|
||||||
/**
|
/**
|
||||||
* Text to add to the confirmation button
|
* Text to add to the confirmation button
|
||||||
*/
|
*/
|
||||||
confirmText?: string, password?: boolean,
|
confirmText?: string, password?: boolean,
|
||||||
/**
|
/**
|
||||||
* Text to add to the cancel button
|
* Text to add to the cancel button
|
||||||
*/
|
*/
|
||||||
cancelText?: string,
|
cancelText?: string,
|
||||||
/**
|
/**
|
||||||
* Require the user to enter a non-empty value
|
* Require the user to enter a non-empty value
|
||||||
*/
|
*/
|
||||||
@@ -524,12 +524,12 @@ export type SetKeyValueResponse = {};
|
|||||||
|
|
||||||
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, };
|
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, timeout?: number, };
|
||||||
|
|
||||||
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
|
export type TemplateFunction = { name: string, previewType?: TemplateFunctionPreviewType, description?: string,
|
||||||
/**
|
/**
|
||||||
* Also support alternative names. This is useful for not breaking existing
|
* Also support alternative names. This is useful for not breaking existing
|
||||||
* tags when changing the `name` property
|
* tags when changing the `name` property
|
||||||
*/
|
*/
|
||||||
aliases?: Array<string>, args: Array<TemplateFunctionArg>,
|
aliases?: Array<string>, args: Array<TemplateFunctionArg>,
|
||||||
/**
|
/**
|
||||||
* A list of arg names to show in the inline preview. If not provided, none will be shown (for privacy reasons).
|
* A list of arg names to show in the inline preview. If not provided, none will be shown (for privacy reasons).
|
||||||
*/
|
*/
|
||||||
@@ -546,23 +546,23 @@ export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, }
|
|||||||
|
|
||||||
export type TemplateRenderResponse = { data: JsonValue, };
|
export type TemplateRenderResponse = { data: JsonValue, };
|
||||||
|
|
||||||
export type Theme = {
|
export type Theme = {
|
||||||
/**
|
/**
|
||||||
* How the theme is identified. This should never be changed
|
* How the theme is identified. This should never be changed
|
||||||
*/
|
*/
|
||||||
id: string,
|
id: string,
|
||||||
/**
|
/**
|
||||||
* The friendly name of the theme to be displayed to the user
|
* The friendly name of the theme to be displayed to the user
|
||||||
*/
|
*/
|
||||||
label: string,
|
label: string,
|
||||||
/**
|
/**
|
||||||
* Whether the theme will be used for dark or light appearance
|
* Whether the theme will be used for dark or light appearance
|
||||||
*/
|
*/
|
||||||
dark: boolean,
|
dark: boolean,
|
||||||
/**
|
/**
|
||||||
* The default top-level colors for the theme
|
* The default top-level colors for the theme
|
||||||
*/
|
*/
|
||||||
base: ThemeComponentColors,
|
base: ThemeComponentColors,
|
||||||
/**
|
/**
|
||||||
* Optionally override theme for individual UI components for more control
|
* Optionally override theme for individual UI components for more control
|
||||||
*/
|
*/
|
||||||
|
|||||||
38
crates/yaak-plugins/bindings/gen_models.ts
generated
38
crates/yaak-plugins/bindings/gen_models.ts
generated
@@ -18,12 +18,7 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
|||||||
|
|
||||||
export type EncryptedKey = { encryptedKey: string, };
|
export type EncryptedKey = { encryptedKey: string, };
|
||||||
|
|
||||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||||
/**
|
|
||||||
* Variables defined in this environment scope.
|
|
||||||
* Child environments override parent variables by name.
|
|
||||||
*/
|
|
||||||
variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
|
||||||
|
|
||||||
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
@@ -39,17 +34,9 @@ export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, up
|
|||||||
|
|
||||||
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end";
|
||||||
|
|
||||||
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number,
|
export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record<string, any>, description: string, message: string, metadata: Array<HttpRequestHeader>, method: string | null, name: string, service: string | null, sortPriority: number, url: string, };
|
||||||
/**
|
|
||||||
* Server URL (http for plaintext or https for secure)
|
|
||||||
*/
|
|
||||||
url: string, };
|
|
||||||
|
|
||||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
/**
|
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
|
||||||
*/
|
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
|
||||||
|
|
||||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
|
|
||||||
@@ -62,24 +49,17 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
|||||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||||
*/
|
*/
|
||||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||||
|
|
||||||
export type HttpResponseHeader = { name: string, value: string, };
|
export type HttpResponseHeader = { name: string, value: string, };
|
||||||
|
|
||||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||||
|
|
||||||
export type HttpUrlParameter = { enabled?: boolean,
|
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||||
/**
|
|
||||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
|
||||||
* Other entries are appended as query parameters
|
|
||||||
*/
|
|
||||||
name: string, value: string, id?: string, };
|
|
||||||
|
|
||||||
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, };
|
||||||
|
|
||||||
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, source: PluginSource, };
|
export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
|
||||||
|
|
||||||
export type PluginSource = "bundled" | "filesystem" | "registry";
|
|
||||||
|
|
||||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||||
|
|
||||||
@@ -97,11 +77,7 @@ export type WebsocketEvent = { model: "websocket_event", id: string, createdAt:
|
|||||||
|
|
||||||
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text";
|
||||||
|
|
||||||
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string,
|
export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, message: string, name: string, sortPriority: number, url: string, urlParameters: Array<HttpUrlParameter>, };
|
||||||
/**
|
|
||||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
|
||||||
*/
|
|
||||||
urlParameters: Array<HttpUrlParameter>, };
|
|
||||||
|
|
||||||
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record<string, any>, authenticationType: string | null, description: string, headers: Array<HttpRequestHeader>, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, settingDnsOverrides: Array<DnsOverride>, };
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,3 @@ export async function checkPluginUpdates() {
|
|||||||
export async function updateAllPlugins() {
|
export async function updateAllPlugins() {
|
||||||
return invoke<PluginNameVersion[]>('cmd_plugins_update_all', {});
|
return invoke<PluginNameVersion[]>('cmd_plugins_update_all', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installPluginFromDirectory(directory: string) {
|
|
||||||
return invoke<void>('cmd_plugins_install_from_directory', { directory });
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use ts_rs::TS;
|
use ts_rs::TS;
|
||||||
use yaak_models::models::{Plugin, PluginSource};
|
use yaak_models::models::Plugin;
|
||||||
|
|
||||||
/// Get plugin info from the registry.
|
/// Get plugin info from the registry.
|
||||||
pub async fn get_plugin(
|
pub async fn get_plugin(
|
||||||
@@ -58,7 +58,7 @@ pub async fn check_plugin_updates(
|
|||||||
) -> Result<PluginUpdatesResponse> {
|
) -> Result<PluginUpdatesResponse> {
|
||||||
let name_versions: Vec<PluginNameVersion> = plugins
|
let name_versions: Vec<PluginNameVersion> = plugins
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|p| matches!(p.source, PluginSource::Registry)) // Only check registry-installed plugins
|
.filter(|p| p.url.is_some()) // Only check plugins with URLs (from registry)
|
||||||
.filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) {
|
.filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) {
|
||||||
Ok(m) => Some(PluginNameVersion { name: m.name, version: m.version }),
|
Ok(m) => Some(PluginNameVersion { name: m.name, version: m.version }),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use log::info;
|
|||||||
use std::fs::{create_dir_all, remove_dir_all};
|
use std::fs::{create_dir_all, remove_dir_all};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use yaak_models::models::{Plugin, PluginSource};
|
use yaak_models::models::Plugin;
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -78,7 +78,6 @@ pub async fn download_and_install(
|
|||||||
directory: plugin_dir_str.clone(),
|
directory: plugin_dir_str.clone(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
url: Some(plugin_version.url.clone()),
|
url: Some(plugin_version.url.clone()),
|
||||||
source: PluginSource::Registry,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
&UpdateSource::Background,
|
&UpdateSource::Background,
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ use crate::events::{
|
|||||||
use crate::native_template_functions::{template_function_keyring, template_function_secure};
|
use crate::native_template_functions::{template_function_keyring, template_function_secure};
|
||||||
use crate::nodejs::start_nodejs_plugin_runtime;
|
use crate::nodejs::start_nodejs_plugin_runtime;
|
||||||
use crate::plugin_handle::PluginHandle;
|
use crate::plugin_handle::PluginHandle;
|
||||||
use crate::plugin_meta::get_plugin_meta;
|
|
||||||
use crate::server_ws::PluginRuntimeServerWebsocket;
|
use crate::server_ws::PluginRuntimeServerWebsocket;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -34,9 +33,8 @@ use tokio::net::TcpListener;
|
|||||||
use tokio::sync::mpsc::error::TrySendError;
|
use tokio::sync::mpsc::error::TrySendError;
|
||||||
use tokio::sync::{Mutex, mpsc, oneshot};
|
use tokio::sync::{Mutex, mpsc, oneshot};
|
||||||
use tokio::time::{Instant, timeout};
|
use tokio::time::{Instant, timeout};
|
||||||
use yaak_models::models::{Plugin, PluginSource};
|
use yaak_models::models::Plugin;
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::util::generate_id;
|
||||||
use yaak_models::util::{UpdateSource, generate_id};
|
|
||||||
use yaak_templates::error::Error::RenderError;
|
use yaak_templates::error::Error::RenderError;
|
||||||
use yaak_templates::error::Result as TemplateResult;
|
use yaak_templates::error::Result as TemplateResult;
|
||||||
|
|
||||||
@@ -63,18 +61,14 @@ impl PluginManager {
|
|||||||
/// * `installed_plugin_dir` - Path to installed plugins directory
|
/// * `installed_plugin_dir` - Path to installed plugins directory
|
||||||
/// * `node_bin_path` - Path to the yaaknode binary
|
/// * `node_bin_path` - Path to the yaaknode binary
|
||||||
/// * `plugin_runtime_main` - Path to the plugin runtime index.cjs
|
/// * `plugin_runtime_main` - Path to the plugin runtime index.cjs
|
||||||
/// * `query_manager` - Query manager for bundled plugin registration and loading
|
|
||||||
/// * `plugin_context` - Context to use while initializing plugins
|
|
||||||
/// * `dev_mode` - Whether the app is in dev mode (affects plugin loading)
|
/// * `dev_mode` - Whether the app is in dev mode (affects plugin loading)
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
vendored_plugin_dir: PathBuf,
|
vendored_plugin_dir: PathBuf,
|
||||||
installed_plugin_dir: PathBuf,
|
installed_plugin_dir: PathBuf,
|
||||||
node_bin_path: PathBuf,
|
node_bin_path: PathBuf,
|
||||||
plugin_runtime_main: PathBuf,
|
plugin_runtime_main: PathBuf,
|
||||||
query_manager: &QueryManager,
|
|
||||||
plugin_context: &PluginContext,
|
|
||||||
dev_mode: bool,
|
dev_mode: bool,
|
||||||
) -> Result<PluginManager> {
|
) -> PluginManager {
|
||||||
let (events_tx, mut events_rx) = mpsc::channel(2048);
|
let (events_tx, mut events_rx) = mpsc::channel(2048);
|
||||||
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
let (kill_server_tx, kill_server_rx) = tokio::sync::watch::channel(false);
|
||||||
let (killed_tx, killed_rx) = oneshot::channel();
|
let (killed_tx, killed_rx) = oneshot::channel();
|
||||||
@@ -157,41 +151,12 @@ impl PluginManager {
|
|||||||
&kill_server_rx,
|
&kill_server_rx,
|
||||||
killed_tx,
|
killed_tx,
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.unwrap();
|
||||||
info!("Waiting for plugins to initialize");
|
info!("Waiting for plugins to initialize");
|
||||||
init_plugins_task.await.map_err(|e| PluginErr(e.to_string()))?;
|
init_plugins_task.await.unwrap();
|
||||||
|
|
||||||
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
|
plugin_manager
|
||||||
let db = query_manager.connect();
|
|
||||||
for dir in &bundled_dirs {
|
|
||||||
if db.get_plugin_by_directory(dir).is_none() {
|
|
||||||
db.upsert_plugin(
|
|
||||||
&Plugin {
|
|
||||||
directory: dir.clone(),
|
|
||||||
enabled: true,
|
|
||||||
url: None,
|
|
||||||
source: PluginSource::Bundled,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
&UpdateSource::Background,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let plugins = db.list_plugins()?;
|
|
||||||
drop(db);
|
|
||||||
|
|
||||||
let init_errors = plugin_manager.initialize_all_plugins(plugins, plugin_context).await;
|
|
||||||
if !init_errors.is_empty() {
|
|
||||||
let joined = init_errors
|
|
||||||
.into_iter()
|
|
||||||
.map(|(dir, err)| format!("{dir}: {err}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("; ");
|
|
||||||
return Err(PluginErr(format!("Failed to initialize plugin(s): {joined}")));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(plugin_manager)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
|
/// Get the vendored plugin directory path (resolves dev mode path if applicable)
|
||||||
@@ -215,57 +180,6 @@ impl PluginManager {
|
|||||||
read_plugins_dir(&plugins_dir).await
|
read_plugins_dir(&plugins_dir).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve_plugins_for_runtime_from_db(&self, plugins: Vec<Plugin>) -> Vec<Plugin> {
|
|
||||||
let bundled_dirs = match self.list_bundled_plugin_dirs().await {
|
|
||||||
Ok(dirs) => dirs,
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to read bundled plugin dirs for resolution: {err:?}");
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.resolve_plugins_for_runtime(plugins, bundled_dirs)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve the plugin set for the current runtime instance.
|
|
||||||
///
|
|
||||||
/// Rules:
|
|
||||||
/// - Drop bundled rows that are not present in this instance's bundled directory list.
|
|
||||||
/// - Deduplicate by plugin metadata name (fallback to directory key when metadata is unreadable).
|
|
||||||
/// - Prefer sources in this order: filesystem > registry > bundled.
|
|
||||||
/// - For same-source conflicts, prefer the most recently installed row (`created_at`).
|
|
||||||
fn resolve_plugins_for_runtime(
|
|
||||||
&self,
|
|
||||||
plugins: Vec<Plugin>,
|
|
||||||
bundled_dirs: Vec<String>,
|
|
||||||
) -> Vec<Plugin> {
|
|
||||||
let bundled_dir_set: HashSet<String> = bundled_dirs.into_iter().collect();
|
|
||||||
let mut selected: HashMap<String, Plugin> = HashMap::new();
|
|
||||||
|
|
||||||
for plugin in plugins {
|
|
||||||
if matches!(plugin.source, PluginSource::Bundled)
|
|
||||||
&& !bundled_dir_set.contains(&plugin.directory)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = match get_plugin_meta(Path::new(&plugin.directory)) {
|
|
||||||
Ok(meta) => meta.name,
|
|
||||||
Err(_) => format!("__dir__{}", plugin.directory),
|
|
||||||
};
|
|
||||||
|
|
||||||
match selected.get(&key) {
|
|
||||||
Some(existing) if !prefer_plugin(&plugin, existing) => {}
|
|
||||||
_ => {
|
|
||||||
selected.insert(key, plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut resolved = selected.into_values().collect::<Vec<_>>();
|
|
||||||
resolved.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
|
||||||
resolved
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
|
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
|
||||||
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
|
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
|
||||||
self.remove_plugin(plugin_context, &plugin).await
|
self.remove_plugin(plugin_context, &plugin).await
|
||||||
@@ -340,8 +254,7 @@ impl PluginManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize all plugins from the provided DB list.
|
/// Initialize all plugins from the provided list.
|
||||||
/// Plugin candidates are resolved for this runtime instance before initialization.
|
|
||||||
/// Returns a list of (plugin_directory, error_message) for any plugins that failed to initialize.
|
/// Returns a list of (plugin_directory, error_message) for any plugins that failed to initialize.
|
||||||
pub async fn initialize_all_plugins(
|
pub async fn initialize_all_plugins(
|
||||||
&self,
|
&self,
|
||||||
@@ -351,18 +264,15 @@ impl PluginManager {
|
|||||||
info!("Initializing all plugins");
|
info!("Initializing all plugins");
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
let plugins = self.resolve_plugins_for_runtime_from_db(plugins).await;
|
|
||||||
|
|
||||||
// Rebuild runtime handles from scratch to avoid stale/duplicate handles.
|
|
||||||
let existing_handles = { self.plugin_handles.lock().await.clone() };
|
|
||||||
for plugin_handle in existing_handles {
|
|
||||||
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
|
|
||||||
error!("Failed to remove plugin {} {e:?}", plugin_handle.dir);
|
|
||||||
errors.push((plugin_handle.dir.clone(), e.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for plugin in plugins {
|
for plugin in plugins {
|
||||||
|
// First remove the plugin if it exists and is enabled
|
||||||
|
if let Some(plugin_handle) = self.get_plugin_by_dir(&plugin.directory).await {
|
||||||
|
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
|
||||||
|
error!("Failed to remove plugin {} {e:?}", plugin.directory);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Err(e) = self.add_plugin(plugin_context, &plugin).await {
|
if let Err(e) = self.add_plugin(plugin_context, &plugin).await {
|
||||||
warn!("Failed to add plugin {} {e:?}", plugin.directory);
|
warn!("Failed to add plugin {} {e:?}", plugin.directory);
|
||||||
errors.push((plugin.directory.clone(), e.to_string()));
|
errors.push((plugin.directory.clone(), e.to_string()));
|
||||||
@@ -1120,24 +1030,6 @@ impl PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source_priority(source: &PluginSource) -> i32 {
|
|
||||||
match source {
|
|
||||||
PluginSource::Filesystem => 3,
|
|
||||||
PluginSource::Registry => 2,
|
|
||||||
PluginSource::Bundled => 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prefer_plugin(candidate: &Plugin, existing: &Plugin) -> bool {
|
|
||||||
let candidate_priority = source_priority(&candidate.source);
|
|
||||||
let existing_priority = source_priority(&existing.source);
|
|
||||||
if candidate_priority != existing_priority {
|
|
||||||
return candidate_priority > existing_priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
candidate.created_at > existing.created_at
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
|
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
|
||||||
let mut result = read_dir(dir).await?;
|
let mut result = read_dir(dir).await?;
|
||||||
let mut dirs: Vec<String> = vec![];
|
let mut dirs: Vec<String> = vec![];
|
||||||
@@ -1156,10 +1048,16 @@ async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
|
|||||||
fn fix_windows_paths(p: &PathBuf) -> String {
|
fn fix_windows_paths(p: &PathBuf) -> String {
|
||||||
use dunce;
|
use dunce;
|
||||||
use path_slash::PathBufExt;
|
use path_slash::PathBufExt;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
// 1. Remove UNC prefix for Windows paths
|
// 1. Remove UNC prefix for Windows paths to pass to sidecar
|
||||||
let safe_path = dunce::simplified(p.as_path());
|
let safe_path = dunce::simplified(p.as_path()).to_string_lossy().to_string();
|
||||||
|
|
||||||
// 2. Convert backslashes to forward slashes for Node.js compatibility
|
// 2. Remove the drive letter
|
||||||
PathBuf::from(safe_path).to_slash_lossy().to_string()
|
let safe_path = Regex::new("^[a-zA-Z]:").unwrap().replace(safe_path.as_str(), "");
|
||||||
|
|
||||||
|
// 3. Convert backslashes to forward
|
||||||
|
let safe_path = PathBuf::from(safe_path.to_string()).to_slash_lossy().to_string();
|
||||||
|
|
||||||
|
safe_path
|
||||||
}
|
}
|
||||||
|
|||||||
4
crates/yaak-templates/pkg/yaak_templates.d.ts
generated
vendored
4
crates/yaak-templates/pkg/yaak_templates.d.ts
generated
vendored
@@ -1,5 +1,5 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export function escape_template(template: string): any;
|
|
||||||
export function parse_template(template: string): any;
|
|
||||||
export function unescape_template(template: string): any;
|
export function unescape_template(template: string): any;
|
||||||
|
export function parse_template(template: string): any;
|
||||||
|
export function escape_template(template: string): any;
|
||||||
|
|||||||
8
crates/yaak-templates/pkg/yaak_templates_bg.js
generated
8
crates/yaak-templates/pkg/yaak_templates_bg.js
generated
@@ -165,10 +165,10 @@ function takeFromExternrefTable0(idx) {
|
|||||||
* @param {string} template
|
* @param {string} template
|
||||||
* @returns {any}
|
* @returns {any}
|
||||||
*/
|
*/
|
||||||
export function escape_template(template) {
|
export function unescape_template(template) {
|
||||||
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
const len0 = WASM_VECTOR_LEN;
|
const len0 = WASM_VECTOR_LEN;
|
||||||
const ret = wasm.escape_template(ptr0, len0);
|
const ret = wasm.unescape_template(ptr0, len0);
|
||||||
if (ret[2]) {
|
if (ret[2]) {
|
||||||
throw takeFromExternrefTable0(ret[1]);
|
throw takeFromExternrefTable0(ret[1]);
|
||||||
}
|
}
|
||||||
@@ -193,10 +193,10 @@ export function parse_template(template) {
|
|||||||
* @param {string} template
|
* @param {string} template
|
||||||
* @returns {any}
|
* @returns {any}
|
||||||
*/
|
*/
|
||||||
export function unescape_template(template) {
|
export function escape_template(template) {
|
||||||
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
const len0 = WASM_VECTOR_LEN;
|
const len0 = WASM_VECTOR_LEN;
|
||||||
const ret = wasm.unescape_template(ptr0, len0);
|
const ret = wasm.escape_template(ptr0, len0);
|
||||||
if (ret[2]) {
|
if (ret[2]) {
|
||||||
throw takeFromExternrefTable0(ret[1]);
|
throw takeFromExternrefTable0(ret[1]);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
crates/yaak-templates/pkg/yaak_templates_bg.wasm
generated
BIN
crates/yaak-templates/pkg/yaak_templates_bg.wasm
generated
Binary file not shown.
@@ -11,7 +11,6 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
|||||||
let mut new_json = "".to_string();
|
let mut new_json = "".to_string();
|
||||||
let mut depth = 0;
|
let mut depth = 0;
|
||||||
let mut state = FormatState::None;
|
let mut state = FormatState::None;
|
||||||
let mut saw_newline_in_whitespace = false;
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let rest_of_chars = chars.clone();
|
let rest_of_chars = chars.clone();
|
||||||
@@ -62,62 +61,6 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle line comments (//)
|
|
||||||
if current_char == '/' && chars.peek() == Some(&'/') {
|
|
||||||
chars.next(); // Skip second /
|
|
||||||
// Collect the rest of the comment until newline
|
|
||||||
let mut comment = String::from("//");
|
|
||||||
loop {
|
|
||||||
match chars.peek() {
|
|
||||||
Some(&'\n') | None => break,
|
|
||||||
Some(_) => comment.push(chars.next().unwrap()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check if the comma handler already added \n + indent
|
|
||||||
let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t');
|
|
||||||
if trimmed.ends_with(",\n") && !saw_newline_in_whitespace {
|
|
||||||
// Trailing comment on the same line as comma (e.g. "foo",// comment)
|
|
||||||
new_json.truncate(trimmed.len() - 1);
|
|
||||||
new_json.push(' ');
|
|
||||||
} else if !trimmed.ends_with('\n') && !new_json.is_empty() {
|
|
||||||
// Trailing comment after a value (no newline before us)
|
|
||||||
new_json.push(' ');
|
|
||||||
}
|
|
||||||
new_json.push_str(&comment);
|
|
||||||
new_json.push('\n');
|
|
||||||
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
|
||||||
saw_newline_in_whitespace = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle block comments (/* ... */)
|
|
||||||
if current_char == '/' && chars.peek() == Some(&'*') {
|
|
||||||
chars.next(); // Skip *
|
|
||||||
let mut comment = String::from("/*");
|
|
||||||
loop {
|
|
||||||
match chars.next() {
|
|
||||||
None => break,
|
|
||||||
Some('*') if chars.peek() == Some(&'/') => {
|
|
||||||
chars.next(); // Skip /
|
|
||||||
comment.push_str("*/");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some(c) => comment.push(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we're not already on a fresh line, add newline + indent before comment
|
|
||||||
let trimmed = new_json.trim_end_matches(|c: char| c == ' ' || c == '\t');
|
|
||||||
if !trimmed.is_empty() && !trimmed.ends_with('\n') {
|
|
||||||
new_json.push('\n');
|
|
||||||
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
|
||||||
}
|
|
||||||
new_json.push_str(&comment);
|
|
||||||
// After block comment, add newline + indent for the next content
|
|
||||||
new_json.push('\n');
|
|
||||||
new_json.push_str(tab.to_string().repeat(depth).as_str());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match current_char {
|
match current_char {
|
||||||
',' => {
|
',' => {
|
||||||
new_json.push(current_char);
|
new_json.push(current_char);
|
||||||
@@ -182,37 +125,20 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
|||||||
|| current_char == '\t'
|
|| current_char == '\t'
|
||||||
|| current_char == '\r'
|
|| current_char == '\r'
|
||||||
{
|
{
|
||||||
if current_char == '\n' {
|
|
||||||
saw_newline_in_whitespace = true;
|
|
||||||
}
|
|
||||||
// Don't add these
|
// Don't add these
|
||||||
} else {
|
} else {
|
||||||
saw_newline_in_whitespace = false;
|
|
||||||
new_json.push(current_char);
|
new_json.push(current_char);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out whitespace-only lines, but preserve empty lines inside block comments
|
// Replace only lines containing whitespace with nothing
|
||||||
let mut result_lines: Vec<&str> = Vec::new();
|
new_json
|
||||||
let mut in_block_comment = false;
|
.lines()
|
||||||
for line in new_json.lines() {
|
.filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines
|
||||||
if in_block_comment {
|
.collect::<Vec<&str>>() // Collect the non-empty lines into a vector
|
||||||
result_lines.push(line);
|
.join("\n") // Join the lines back into a single string
|
||||||
if line.contains("*/") {
|
|
||||||
in_block_comment = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if line.contains("/*") && !line.contains("*/") {
|
|
||||||
in_block_comment = true;
|
|
||||||
}
|
|
||||||
if !line.trim().is_empty() {
|
|
||||||
result_lines.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result_lines.iter().map(|line| line.trim_end()).collect::<Vec<&str>>().join("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -371,161 +297,6 @@ mod tests {
|
|||||||
r#"
|
r#"
|
||||||
{}
|
{}
|
||||||
}
|
}
|
||||||
"#
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_line_comment_between_keys() {
|
|
||||||
assert_eq!(
|
|
||||||
format_json(
|
|
||||||
r#"{"foo":"bar",// a comment
|
|
||||||
"baz":"qux"}"#,
|
|
||||||
" "
|
|
||||||
),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"foo": "bar", // a comment
|
|
||||||
"baz": "qux"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_line_comment_at_end() {
|
|
||||||
assert_eq!(
|
|
||||||
format_json(
|
|
||||||
r#"{"foo":"bar" // trailing
|
|
||||||
}"#,
|
|
||||||
" "
|
|
||||||
),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"foo": "bar" // trailing
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_block_comment() {
|
|
||||||
assert_eq!(
|
|
||||||
format_json(r#"{"foo":"bar",/* comment */"baz":"qux"}"#, " "),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"foo": "bar",
|
|
||||||
/* comment */
|
|
||||||
"baz": "qux"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_comment_in_array() {
|
|
||||||
assert_eq!(
|
|
||||||
format_json(
|
|
||||||
r#"[1,// item comment
|
|
||||||
2,3]"#,
|
|
||||||
" "
|
|
||||||
),
|
|
||||||
r#"
|
|
||||||
[
|
|
||||||
1, // item comment
|
|
||||||
2,
|
|
||||||
3
|
|
||||||
]
|
|
||||||
"#
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_comment_only_line() {
|
|
||||||
assert_eq!(
|
|
||||||
format_json(
|
|
||||||
r#"{
|
|
||||||
// this is a standalone comment
|
|
||||||
"foo": "bar"
|
|
||||||
}"#,
|
|
||||||
" "
|
|
||||||
),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
// this is a standalone comment
|
|
||||||
"foo": "bar"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiline_block_comment() {
|
|
||||||
assert_eq!(
|
|
||||||
format_json(
|
|
||||||
r#"{
|
|
||||||
"foo": "bar"
|
|
||||||
/**
|
|
||||||
Hello World!
|
|
||||||
|
|
||||||
Hi there
|
|
||||||
*/
|
|
||||||
}"#,
|
|
||||||
" "
|
|
||||||
),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
/**
|
|
||||||
Hello World!
|
|
||||||
|
|
||||||
Hi there
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: trailing whitespace on output lines is trimmed by the formatter.
|
|
||||||
// We can't easily add a test for this because raw string literals get
|
|
||||||
// trailing whitespace stripped by the editor/linter.
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_comment_inside_string_ignored() {
|
|
||||||
assert_eq!(
|
|
||||||
format_json(r#"{"foo":"// not a comment","bar":"/* also not */"}"#, " "),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"foo": "// not a comment",
|
|
||||||
"bar": "/* also not */"
|
|
||||||
}
|
|
||||||
"#
|
|
||||||
.trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_comment_on_line_after_comma() {
|
|
||||||
assert_eq!(
|
|
||||||
format_json(
|
|
||||||
r#"{
|
|
||||||
"a": "aaa",
|
|
||||||
// "b": "bbb"
|
|
||||||
}"#,
|
|
||||||
" "
|
|
||||||
),
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"a": "aaa",
|
|
||||||
// "b": "bbb"
|
|
||||||
}
|
|
||||||
"#
|
"#
|
||||||
.trim()
|
.trim()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod escape;
|
pub mod escape;
|
||||||
pub mod format_json;
|
pub mod format_json;
|
||||||
pub mod strip_json_comments;
|
|
||||||
pub mod parser;
|
pub mod parser;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod wasm;
|
pub mod wasm;
|
||||||
|
|||||||
@@ -1,318 +0,0 @@
|
|||||||
/// Strips JSON comments only if the result is valid JSON. If stripping comments
|
|
||||||
/// produces invalid JSON, the original text is returned unchanged.
|
|
||||||
pub fn maybe_strip_json_comments(text: &str) -> String {
|
|
||||||
let stripped = strip_json_comments(text);
|
|
||||||
if serde_json::from_str::<serde_json::Value>(&stripped).is_ok() {
|
|
||||||
stripped
|
|
||||||
} else {
|
|
||||||
text.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Strips comments from JSONC, preserving the original formatting as much as possible.
|
|
||||||
///
|
|
||||||
/// - Trailing comments on a line are removed (along with preceding whitespace)
|
|
||||||
/// - Whole-line comments are removed, including the line itself
|
|
||||||
/// - Block comments are removed, including any lines that become empty
|
|
||||||
/// - Comments inside strings and template tags are left alone
|
|
||||||
pub fn strip_json_comments(text: &str) -> String {
|
|
||||||
let mut chars = text.chars().peekable();
|
|
||||||
let mut result = String::with_capacity(text.len());
|
|
||||||
let mut in_string = false;
|
|
||||||
let mut in_template_tag = false;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let current_char = match chars.next() {
|
|
||||||
None => break,
|
|
||||||
Some(c) => c,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle JSON strings
|
|
||||||
if in_string {
|
|
||||||
result.push(current_char);
|
|
||||||
match current_char {
|
|
||||||
'"' => in_string = false,
|
|
||||||
'\\' => {
|
|
||||||
if let Some(c) = chars.next() {
|
|
||||||
result.push(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle template tags
|
|
||||||
if in_template_tag {
|
|
||||||
result.push(current_char);
|
|
||||||
if current_char == ']' && chars.peek() == Some(&'}') {
|
|
||||||
result.push(chars.next().unwrap());
|
|
||||||
in_template_tag = false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for template tag start
|
|
||||||
if current_char == '$' && chars.peek() == Some(&'{') {
|
|
||||||
let mut lookahead = chars.clone();
|
|
||||||
lookahead.next(); // skip {
|
|
||||||
if lookahead.peek() == Some(&'[') {
|
|
||||||
in_template_tag = true;
|
|
||||||
result.push(current_char);
|
|
||||||
result.push(chars.next().unwrap()); // {
|
|
||||||
result.push(chars.next().unwrap()); // [
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for line comment
|
|
||||||
if current_char == '/' && chars.peek() == Some(&'/') {
|
|
||||||
chars.next(); // skip second /
|
|
||||||
// Consume until newline
|
|
||||||
loop {
|
|
||||||
match chars.peek() {
|
|
||||||
Some(&'\n') | None => break,
|
|
||||||
Some(_) => {
|
|
||||||
chars.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Trim trailing whitespace that preceded the comment
|
|
||||||
let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len();
|
|
||||||
result.truncate(trimmed_len);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for block comment
|
|
||||||
if current_char == '/' && chars.peek() == Some(&'*') {
|
|
||||||
chars.next(); // skip *
|
|
||||||
// Consume until */
|
|
||||||
loop {
|
|
||||||
match chars.next() {
|
|
||||||
None => break,
|
|
||||||
Some('*') if chars.peek() == Some(&'/') => {
|
|
||||||
chars.next(); // skip /
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Trim trailing whitespace that preceded the comment
|
|
||||||
let trimmed_len = result.trim_end_matches(|c: char| c == ' ' || c == '\t').len();
|
|
||||||
result.truncate(trimmed_len);
|
|
||||||
// Skip whitespace/newline after the block comment if the next line is content
|
|
||||||
// (this handles the case where the block comment is on its own line)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_char == '"' {
|
|
||||||
in_string = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(current_char);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove lines that are now empty (were comment-only lines)
|
|
||||||
let result = result
|
|
||||||
.lines()
|
|
||||||
.filter(|line| !line.trim().is_empty())
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
// Remove trailing commas before } or ]
|
|
||||||
strip_trailing_commas(&result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes trailing commas before closing braces/brackets, respecting strings.
|
|
||||||
fn strip_trailing_commas(text: &str) -> String {
|
|
||||||
let mut result = String::with_capacity(text.len());
|
|
||||||
let chars: Vec<char> = text.chars().collect();
|
|
||||||
let mut i = 0;
|
|
||||||
let mut in_string = false;
|
|
||||||
|
|
||||||
while i < chars.len() {
|
|
||||||
let ch = chars[i];
|
|
||||||
|
|
||||||
if in_string {
|
|
||||||
result.push(ch);
|
|
||||||
match ch {
|
|
||||||
'"' => in_string = false,
|
|
||||||
'\\' => {
|
|
||||||
i += 1;
|
|
||||||
if i < chars.len() {
|
|
||||||
result.push(chars[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ch == '"' {
|
|
||||||
in_string = true;
|
|
||||||
result.push(ch);
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ch == ',' {
|
|
||||||
// Look ahead past whitespace/newlines for } or ]
|
|
||||||
let mut j = i + 1;
|
|
||||||
while j < chars.len() && chars[j].is_whitespace() {
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
if j < chars.len() && (chars[j] == '}' || chars[j] == ']') {
|
|
||||||
// Skip the comma
|
|
||||||
i += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(ch);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::strip_json_comments::strip_json_comments;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_no_comments() {
|
|
||||||
let input = r#"{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": 123
|
|
||||||
}"#;
|
|
||||||
assert_eq!(strip_json_comments(input), input);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_trailing_line_comment() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_json_comments(r#"{
|
|
||||||
"foo": "bar", // this is a comment
|
|
||||||
"baz": 123
|
|
||||||
}"#),
|
|
||||||
r#"{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": 123
|
|
||||||
}"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_whole_line_comment() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_json_comments(r#"{
|
|
||||||
// this is a comment
|
|
||||||
"foo": "bar"
|
|
||||||
}"#),
|
|
||||||
r#"{
|
|
||||||
"foo": "bar"
|
|
||||||
}"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_inline_block_comment() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_json_comments(r#"{
|
|
||||||
"foo": /* a comment */ "bar"
|
|
||||||
}"#),
|
|
||||||
r#"{
|
|
||||||
"foo": "bar"
|
|
||||||
}"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_whole_line_block_comment() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_json_comments(r#"{
|
|
||||||
/* a comment */
|
|
||||||
"foo": "bar"
|
|
||||||
}"#),
|
|
||||||
r#"{
|
|
||||||
"foo": "bar"
|
|
||||||
}"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiline_block_comment() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_json_comments(r#"{
|
|
||||||
/**
|
|
||||||
* Hello World!
|
|
||||||
*/
|
|
||||||
"foo": "bar"
|
|
||||||
}"#),
|
|
||||||
r#"{
|
|
||||||
"foo": "bar"
|
|
||||||
}"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_comment_inside_string_preserved() {
|
|
||||||
let input = r#"{
|
|
||||||
"foo": "// not a comment",
|
|
||||||
"bar": "/* also not */"
|
|
||||||
}"#;
|
|
||||||
assert_eq!(strip_json_comments(input), input);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_comment_inside_template_tag_preserved() {
|
|
||||||
let input = r#"{
|
|
||||||
"foo": ${[ fn("// hi", "/* hey */") ]}
|
|
||||||
}"#;
|
|
||||||
assert_eq!(strip_json_comments(input), input);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_multiple_comments() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_json_comments(r#"{
|
|
||||||
// first comment
|
|
||||||
"foo": "bar", // trailing
|
|
||||||
/* block */
|
|
||||||
"baz": 123
|
|
||||||
}"#),
|
|
||||||
r#"{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": 123
|
|
||||||
}"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_trailing_comma_after_comment_removed() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_json_comments(r#"{
|
|
||||||
"a": "aaa",
|
|
||||||
// "b": "bbb"
|
|
||||||
}"#),
|
|
||||||
r#"{
|
|
||||||
"a": "aaa"
|
|
||||||
}"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_trailing_comma_in_array() {
|
|
||||||
assert_eq!(
|
|
||||||
strip_json_comments(r#"[1, 2, /* 3 */]"#),
|
|
||||||
r#"[1, 2]"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_comma_inside_string_preserved() {
|
|
||||||
let input = r#"{"a": "hello,}"#;
|
|
||||||
assert_eq!(strip_json_comments(input), input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -273,5 +273,6 @@ pub fn find_client_certificate(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("No matching client certificate found for {}", url_string);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
use yaak_models::models::AnyModel;
|
|
||||||
use yaak_models::query_manager::QueryManager;
|
use yaak_models::query_manager::QueryManager;
|
||||||
use yaak_models::util::UpdateSource;
|
|
||||||
use yaak_plugins::events::{
|
use yaak_plugins::events::{
|
||||||
CloseWindowRequest, CopyTextRequest, DeleteKeyValueRequest, DeleteKeyValueResponse,
|
CloseWindowRequest, CopyTextRequest, DeleteKeyValueRequest, DeleteKeyValueResponse,
|
||||||
DeleteModelRequest, DeleteModelResponse, ErrorResponse, FindHttpResponsesRequest,
|
DeleteModelRequest, ErrorResponse, FindHttpResponsesRequest, GetCookieValueRequest,
|
||||||
FindHttpResponsesResponse, GetCookieValueRequest, GetHttpRequestByIdRequest,
|
GetHttpRequestByIdRequest, GetHttpRequestByIdResponse, GetKeyValueRequest, GetKeyValueResponse,
|
||||||
GetHttpRequestByIdResponse, GetKeyValueRequest, GetKeyValueResponse, InternalEventPayload,
|
InternalEventPayload, ListCookieNamesRequest, ListFoldersRequest, ListFoldersResponse,
|
||||||
ListCookieNamesRequest, ListFoldersRequest, ListFoldersResponse, ListHttpRequestsRequest,
|
ListHttpRequestsRequest, ListHttpRequestsResponse, ListOpenWorkspacesRequest,
|
||||||
ListHttpRequestsResponse, ListOpenWorkspacesRequest, OpenExternalUrlRequest, OpenWindowRequest,
|
OpenExternalUrlRequest, OpenWindowRequest, PromptFormRequest, PromptTextRequest,
|
||||||
PromptFormRequest, PromptTextRequest, ReloadResponse, RenderGrpcRequestRequest,
|
ReloadResponse, RenderGrpcRequestRequest, RenderHttpRequestRequest, SendHttpRequestRequest,
|
||||||
RenderHttpRequestRequest, SendHttpRequestRequest, SetKeyValueRequest, ShowToastRequest,
|
SetKeyValueRequest, ShowToastRequest, TemplateRenderRequest, UpsertModelRequest,
|
||||||
TemplateRenderRequest, UpsertModelRequest, UpsertModelResponse, WindowInfoRequest,
|
WindowInfoRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SharedPluginEventContext<'a> {
|
pub struct SharedPluginEventContext<'a> {
|
||||||
@@ -39,9 +37,6 @@ pub enum SharedRequest<'a> {
|
|||||||
GetHttpRequestById(&'a GetHttpRequestByIdRequest),
|
GetHttpRequestById(&'a GetHttpRequestByIdRequest),
|
||||||
ListFolders(&'a ListFoldersRequest),
|
ListFolders(&'a ListFoldersRequest),
|
||||||
ListHttpRequests(&'a ListHttpRequestsRequest),
|
ListHttpRequests(&'a ListHttpRequestsRequest),
|
||||||
FindHttpResponses(&'a FindHttpResponsesRequest),
|
|
||||||
UpsertModel(&'a UpsertModelRequest),
|
|
||||||
DeleteModel(&'a DeleteModelRequest),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -50,6 +45,9 @@ pub enum HostRequest<'a> {
|
|||||||
CopyText(&'a CopyTextRequest),
|
CopyText(&'a CopyTextRequest),
|
||||||
PromptText(&'a PromptTextRequest),
|
PromptText(&'a PromptTextRequest),
|
||||||
PromptForm(&'a PromptFormRequest),
|
PromptForm(&'a PromptFormRequest),
|
||||||
|
FindHttpResponses(&'a FindHttpResponsesRequest),
|
||||||
|
UpsertModel(&'a UpsertModelRequest),
|
||||||
|
DeleteModel(&'a DeleteModelRequest),
|
||||||
RenderGrpcRequest(&'a RenderGrpcRequestRequest),
|
RenderGrpcRequest(&'a RenderGrpcRequestRequest),
|
||||||
RenderHttpRequest(&'a RenderHttpRequestRequest),
|
RenderHttpRequest(&'a RenderHttpRequestRequest),
|
||||||
TemplateRender(&'a TemplateRenderRequest),
|
TemplateRender(&'a TemplateRenderRequest),
|
||||||
@@ -73,6 +71,9 @@ impl HostRequest<'_> {
|
|||||||
HostRequest::CopyText(_) => "copy_text_request".to_string(),
|
HostRequest::CopyText(_) => "copy_text_request".to_string(),
|
||||||
HostRequest::PromptText(_) => "prompt_text_request".to_string(),
|
HostRequest::PromptText(_) => "prompt_text_request".to_string(),
|
||||||
HostRequest::PromptForm(_) => "prompt_form_request".to_string(),
|
HostRequest::PromptForm(_) => "prompt_form_request".to_string(),
|
||||||
|
HostRequest::FindHttpResponses(_) => "find_http_responses_request".to_string(),
|
||||||
|
HostRequest::UpsertModel(_) => "upsert_model_request".to_string(),
|
||||||
|
HostRequest::DeleteModel(_) => "delete_model_request".to_string(),
|
||||||
HostRequest::RenderGrpcRequest(_) => "render_grpc_request_request".to_string(),
|
HostRequest::RenderGrpcRequest(_) => "render_grpc_request_request".to_string(),
|
||||||
HostRequest::RenderHttpRequest(_) => "render_http_request_request".to_string(),
|
HostRequest::RenderHttpRequest(_) => "render_http_request_request".to_string(),
|
||||||
HostRequest::TemplateRender(_) => "template_render_request".to_string(),
|
HostRequest::TemplateRender(_) => "template_render_request".to_string(),
|
||||||
@@ -134,13 +135,13 @@ impl<'a> From<&'a InternalEventPayload> for GroupedPluginRequest<'a> {
|
|||||||
GroupedPluginRequest::Host(HostRequest::PromptForm(req))
|
GroupedPluginRequest::Host(HostRequest::PromptForm(req))
|
||||||
}
|
}
|
||||||
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
||||||
GroupedPluginRequest::Shared(SharedRequest::FindHttpResponses(req))
|
GroupedPluginRequest::Host(HostRequest::FindHttpResponses(req))
|
||||||
}
|
}
|
||||||
InternalEventPayload::UpsertModelRequest(req) => {
|
InternalEventPayload::UpsertModelRequest(req) => {
|
||||||
GroupedPluginRequest::Shared(SharedRequest::UpsertModel(req))
|
GroupedPluginRequest::Host(HostRequest::UpsertModel(req))
|
||||||
}
|
}
|
||||||
InternalEventPayload::DeleteModelRequest(req) => {
|
InternalEventPayload::DeleteModelRequest(req) => {
|
||||||
GroupedPluginRequest::Shared(SharedRequest::DeleteModel(req))
|
GroupedPluginRequest::Host(HostRequest::DeleteModel(req))
|
||||||
}
|
}
|
||||||
InternalEventPayload::RenderGrpcRequestRequest(req) => {
|
InternalEventPayload::RenderGrpcRequestRequest(req) => {
|
||||||
GroupedPluginRequest::Host(HostRequest::RenderGrpcRequest(req))
|
GroupedPluginRequest::Host(HostRequest::RenderGrpcRequest(req))
|
||||||
@@ -274,175 +275,17 @@ fn build_shared_reply(
|
|||||||
http_requests,
|
http_requests,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SharedRequest::FindHttpResponses(req) => {
|
|
||||||
let http_responses = query_manager
|
|
||||||
.connect()
|
|
||||||
.list_http_responses_for_request(&req.request_id, req.limit.map(|l| l as u64))
|
|
||||||
.unwrap_or_default();
|
|
||||||
InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
|
|
||||||
http_responses,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
SharedRequest::UpsertModel(req) => {
|
|
||||||
use AnyModel::*;
|
|
||||||
|
|
||||||
let model = match &req.model {
|
|
||||||
HttpRequest(m) => {
|
|
||||||
match query_manager.connect().upsert_http_request(m, &UpdateSource::Plugin) {
|
|
||||||
Ok(model) => HttpRequest(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to upsert HTTP request: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GrpcRequest(m) => {
|
|
||||||
match query_manager.connect().upsert_grpc_request(m, &UpdateSource::Plugin) {
|
|
||||||
Ok(model) => GrpcRequest(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to upsert gRPC request: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WebsocketRequest(m) => {
|
|
||||||
match query_manager.connect().upsert_websocket_request(m, &UpdateSource::Plugin)
|
|
||||||
{
|
|
||||||
Ok(model) => WebsocketRequest(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to upsert WebSocket request: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Folder(m) => {
|
|
||||||
match query_manager.connect().upsert_folder(m, &UpdateSource::Plugin) {
|
|
||||||
Ok(model) => Folder(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to upsert folder: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Environment(m) => {
|
|
||||||
match query_manager.connect().upsert_environment(m, &UpdateSource::Plugin) {
|
|
||||||
Ok(model) => Environment(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to upsert environment: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Workspace(m) => {
|
|
||||||
match query_manager.connect().upsert_workspace(m, &UpdateSource::Plugin) {
|
|
||||||
Ok(model) => Workspace(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to upsert workspace: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: "Upsert not supported for this model type".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
InternalEventPayload::UpsertModelResponse(UpsertModelResponse { model })
|
|
||||||
}
|
|
||||||
SharedRequest::DeleteModel(req) => {
|
|
||||||
let model = match req.model.as_str() {
|
|
||||||
"http_request" => {
|
|
||||||
match query_manager
|
|
||||||
.connect()
|
|
||||||
.delete_http_request_by_id(&req.id, &UpdateSource::Plugin)
|
|
||||||
{
|
|
||||||
Ok(model) => AnyModel::HttpRequest(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to delete HTTP request: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"grpc_request" => {
|
|
||||||
match query_manager
|
|
||||||
.connect()
|
|
||||||
.delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)
|
|
||||||
{
|
|
||||||
Ok(model) => AnyModel::GrpcRequest(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to delete gRPC request: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"websocket_request" => {
|
|
||||||
match query_manager
|
|
||||||
.connect()
|
|
||||||
.delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)
|
|
||||||
{
|
|
||||||
Ok(model) => AnyModel::WebsocketRequest(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to delete WebSocket request: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"folder" => match query_manager
|
|
||||||
.connect()
|
|
||||||
.delete_folder_by_id(&req.id, &UpdateSource::Plugin)
|
|
||||||
{
|
|
||||||
Ok(model) => AnyModel::Folder(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to delete folder: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"environment" => {
|
|
||||||
match query_manager
|
|
||||||
.connect()
|
|
||||||
.delete_environment_by_id(&req.id, &UpdateSource::Plugin)
|
|
||||||
{
|
|
||||||
Ok(model) => AnyModel::Environment(model),
|
|
||||||
Err(err) => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: format!("Failed to delete environment: {err}"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return InternalEventPayload::ErrorResponse(ErrorResponse {
|
|
||||||
error: "Delete not supported for this model type".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
InternalEventPayload::DeleteModelResponse(DeleteModelResponse { model })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use tempfile::TempDir;
|
use yaak_models::models::{Folder, HttpRequest, Workspace};
|
||||||
use yaak_models::models::{AnyModel, Folder, HttpRequest, Workspace};
|
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
fn seed_query_manager() -> (QueryManager, TempDir) {
|
fn seed_query_manager() -> QueryManager {
|
||||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
let temp_dir = tempfile::TempDir::new().expect("Failed to create temp dir");
|
||||||
let db_path = temp_dir.path().join("db.sqlite");
|
let db_path = temp_dir.path().join("db.sqlite");
|
||||||
let blob_path = temp_dir.path().join("blobs.sqlite");
|
let blob_path = temp_dir.path().join("blobs.sqlite");
|
||||||
let (query_manager, _blob_manager, _rx) =
|
let (query_manager, _blob_manager, _rx) =
|
||||||
@@ -489,12 +332,12 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect("Failed to seed request");
|
.expect("Failed to seed request");
|
||||||
|
|
||||||
(query_manager, temp_dir)
|
query_manager
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_requests_requires_workspace_when_folder_missing() {
|
fn list_requests_requires_workspace_when_folder_missing() {
|
||||||
let (query_manager, _temp_dir) = seed_query_manager();
|
let query_manager = seed_query_manager();
|
||||||
let payload = InternalEventPayload::ListHttpRequestsRequest(
|
let payload = InternalEventPayload::ListHttpRequestsRequest(
|
||||||
yaak_plugins::events::ListHttpRequestsRequest { folder_id: None },
|
yaak_plugins::events::ListHttpRequestsRequest { folder_id: None },
|
||||||
);
|
);
|
||||||
@@ -512,7 +355,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn list_requests_by_workspace_and_folder() {
|
fn list_requests_by_workspace_and_folder() {
|
||||||
let (query_manager, _temp_dir) = seed_query_manager();
|
let query_manager = seed_query_manager();
|
||||||
|
|
||||||
let by_workspace_payload = InternalEventPayload::ListHttpRequestsRequest(
|
let by_workspace_payload = InternalEventPayload::ListHttpRequestsRequest(
|
||||||
yaak_plugins::events::ListHttpRequestsRequest { folder_id: None },
|
yaak_plugins::events::ListHttpRequestsRequest { folder_id: None },
|
||||||
@@ -551,83 +394,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn find_http_responses_is_shared_handled() {
|
|
||||||
let (query_manager, _temp_dir) = seed_query_manager();
|
|
||||||
let payload = InternalEventPayload::FindHttpResponsesRequest(FindHttpResponsesRequest {
|
|
||||||
request_id: "rq_test".to_string(),
|
|
||||||
limit: Some(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = handle_shared_plugin_event(
|
|
||||||
&query_manager,
|
|
||||||
&payload,
|
|
||||||
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
|
|
||||||
);
|
|
||||||
|
|
||||||
match result {
|
|
||||||
GroupedPluginEvent::Handled(Some(InternalEventPayload::FindHttpResponsesResponse(
|
|
||||||
resp,
|
|
||||||
))) => {
|
|
||||||
assert!(resp.http_responses.is_empty());
|
|
||||||
}
|
|
||||||
other => panic!("unexpected find responses result: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn upsert_and_delete_model_are_shared_handled() {
|
|
||||||
let (query_manager, _temp_dir) = seed_query_manager();
|
|
||||||
|
|
||||||
let existing = query_manager
|
|
||||||
.connect()
|
|
||||||
.get_http_request("rq_test")
|
|
||||||
.expect("Failed to load seeded request");
|
|
||||||
let upsert_payload = InternalEventPayload::UpsertModelRequest(UpsertModelRequest {
|
|
||||||
model: AnyModel::HttpRequest(HttpRequest {
|
|
||||||
name: "Request Updated".to_string(),
|
|
||||||
..existing
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
let upsert_result = handle_shared_plugin_event(
|
|
||||||
&query_manager,
|
|
||||||
&upsert_payload,
|
|
||||||
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
|
|
||||||
);
|
|
||||||
match upsert_result {
|
|
||||||
GroupedPluginEvent::Handled(Some(InternalEventPayload::UpsertModelResponse(resp))) => {
|
|
||||||
match resp.model {
|
|
||||||
AnyModel::HttpRequest(r) => assert_eq!(r.name, "Request Updated"),
|
|
||||||
other => panic!("unexpected upsert model type: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other => panic!("unexpected upsert result: {other:?}"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let delete_payload = InternalEventPayload::DeleteModelRequest(DeleteModelRequest {
|
|
||||||
model: "http_request".to_string(),
|
|
||||||
id: "rq_test".to_string(),
|
|
||||||
});
|
|
||||||
let delete_result = handle_shared_plugin_event(
|
|
||||||
&query_manager,
|
|
||||||
&delete_payload,
|
|
||||||
SharedPluginEventContext { plugin_name: "@yaak/test", workspace_id: Some("wk_test") },
|
|
||||||
);
|
|
||||||
match delete_result {
|
|
||||||
GroupedPluginEvent::Handled(Some(InternalEventPayload::DeleteModelResponse(resp))) => {
|
|
||||||
match resp.model {
|
|
||||||
AnyModel::HttpRequest(r) => assert_eq!(r.id, "rq_test"),
|
|
||||||
other => panic!("unexpected delete model type: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other => panic!("unexpected delete result: {other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn host_request_classification_works() {
|
fn host_request_classification_works() {
|
||||||
let (query_manager, _temp_dir) = seed_query_manager();
|
let query_manager = seed_query_manager();
|
||||||
let payload = InternalEventPayload::WindowInfoRequest(WindowInfoRequest {
|
let payload = InternalEventPayload::WindowInfoRequest(WindowInfoRequest {
|
||||||
label: "main".to_string(),
|
label: "main".to_string(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use log::info;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use yaak_http::path_placeholders::apply_path_placeholders;
|
use yaak_http::path_placeholders::apply_path_placeholders;
|
||||||
use yaak_models::models::{Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
use yaak_models::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
||||||
use yaak_models::render::make_vars_hashmap;
|
use yaak_models::render::make_vars_hashmap;
|
||||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||||
|
|
||||||
@@ -89,64 +89,6 @@ pub async fn render_http_request<T: TemplateCallback>(
|
|||||||
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() })
|
Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..request.to_owned() })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render_grpc_request<T: TemplateCallback>(
|
|
||||||
r: &GrpcRequest,
|
|
||||||
environment_chain: Vec<Environment>,
|
|
||||||
cb: &T,
|
|
||||||
opt: &RenderOptions,
|
|
||||||
) -> yaak_templates::error::Result<GrpcRequest> {
|
|
||||||
let vars = &make_vars_hashmap(environment_chain);
|
|
||||||
|
|
||||||
let mut metadata = Vec::new();
|
|
||||||
for p in r.metadata.clone() {
|
|
||||||
if !p.enabled {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
metadata.push(HttpRequestHeader {
|
|
||||||
enabled: p.enabled,
|
|
||||||
name: parse_and_render(p.name.as_str(), vars, cb, opt).await?,
|
|
||||||
value: parse_and_render(p.value.as_str(), vars, cb, opt).await?,
|
|
||||||
id: p.id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let authentication = {
|
|
||||||
let mut disabled = false;
|
|
||||||
let mut auth = BTreeMap::new();
|
|
||||||
match r.authentication.get("disabled") {
|
|
||||||
Some(Value::Bool(true)) => {
|
|
||||||
disabled = true;
|
|
||||||
}
|
|
||||||
Some(Value::String(tmpl)) => {
|
|
||||||
disabled = parse_and_render(tmpl.as_str(), vars, cb, opt)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.is_empty();
|
|
||||||
info!(
|
|
||||||
"Rendering authentication.disabled as a template: {disabled} from \"{tmpl}\""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if disabled {
|
|
||||||
auth.insert("disabled".to_string(), Value::Bool(true));
|
|
||||||
} else {
|
|
||||||
for (k, v) in r.authentication.clone() {
|
|
||||||
if k == "disabled" {
|
|
||||||
auth.insert(k, Value::Bool(false));
|
|
||||||
} else {
|
|
||||||
auth.insert(k, render_json_value_raw(v, vars, cb, opt).await?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
auth
|
|
||||||
};
|
|
||||||
|
|
||||||
let url = parse_and_render(r.url.as_str(), vars, cb, opt).await?;
|
|
||||||
|
|
||||||
Ok(GrpcRequest { url, metadata, authentication, ..r.to_owned() })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_disabled_form_entries(v: Value) -> Value {
|
fn strip_disabled_form_entries(v: Value) -> Value {
|
||||||
match v {
|
match v {
|
||||||
Value::Array(items) => Value::Array(
|
Value::Array(items) => Value::Array(
|
||||||
|
|||||||
@@ -239,7 +239,6 @@ pub struct SendHttpRequestByIdParams<'a, T: TemplateCallback> {
|
|||||||
pub cookie_jar_id: Option<String>,
|
pub cookie_jar_id: Option<String>,
|
||||||
pub response_dir: &'a Path,
|
pub response_dir: &'a Path,
|
||||||
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
pub emit_response_body_chunks_to: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
|
||||||
pub cancelled_rx: Option<watch::Receiver<bool>>,
|
pub cancelled_rx: Option<watch::Receiver<bool>>,
|
||||||
pub prepare_sendable_request: Option<&'a dyn PrepareSendableRequest>,
|
pub prepare_sendable_request: Option<&'a dyn PrepareSendableRequest>,
|
||||||
pub executor: Option<&'a dyn SendRequestExecutor>,
|
pub executor: Option<&'a dyn SendRequestExecutor>,
|
||||||
@@ -256,7 +255,6 @@ pub struct SendHttpRequestParams<'a, T: TemplateCallback> {
|
|||||||
pub cookie_jar_id: Option<String>,
|
pub cookie_jar_id: Option<String>,
|
||||||
pub response_dir: &'a Path,
|
pub response_dir: &'a Path,
|
||||||
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
pub emit_response_body_chunks_to: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
|
||||||
pub cancelled_rx: Option<watch::Receiver<bool>>,
|
pub cancelled_rx: Option<watch::Receiver<bool>>,
|
||||||
pub auth_context_id: Option<String>,
|
pub auth_context_id: Option<String>,
|
||||||
pub existing_response: Option<HttpResponse>,
|
pub existing_response: Option<HttpResponse>,
|
||||||
@@ -273,7 +271,6 @@ pub struct SendHttpRequestWithPluginsParams<'a> {
|
|||||||
pub cookie_jar_id: Option<String>,
|
pub cookie_jar_id: Option<String>,
|
||||||
pub response_dir: &'a Path,
|
pub response_dir: &'a Path,
|
||||||
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
pub emit_response_body_chunks_to: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
|
||||||
pub existing_response: Option<HttpResponse>,
|
pub existing_response: Option<HttpResponse>,
|
||||||
pub plugin_manager: Arc<PluginManager>,
|
pub plugin_manager: Arc<PluginManager>,
|
||||||
pub encryption_manager: Arc<EncryptionManager>,
|
pub encryption_manager: Arc<EncryptionManager>,
|
||||||
@@ -291,7 +288,6 @@ pub struct SendHttpRequestByIdWithPluginsParams<'a> {
|
|||||||
pub cookie_jar_id: Option<String>,
|
pub cookie_jar_id: Option<String>,
|
||||||
pub response_dir: &'a Path,
|
pub response_dir: &'a Path,
|
||||||
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
pub emit_events_to: Option<mpsc::Sender<SenderHttpResponseEvent>>,
|
||||||
pub emit_response_body_chunks_to: Option<mpsc::UnboundedSender<Vec<u8>>>,
|
|
||||||
pub plugin_manager: Arc<PluginManager>,
|
pub plugin_manager: Arc<PluginManager>,
|
||||||
pub encryption_manager: Arc<EncryptionManager>,
|
pub encryption_manager: Arc<EncryptionManager>,
|
||||||
pub plugin_context: &'a PluginContext,
|
pub plugin_context: &'a PluginContext,
|
||||||
@@ -357,7 +353,6 @@ pub async fn send_http_request_by_id_with_plugins(
|
|||||||
cookie_jar_id: params.cookie_jar_id,
|
cookie_jar_id: params.cookie_jar_id,
|
||||||
response_dir: params.response_dir,
|
response_dir: params.response_dir,
|
||||||
emit_events_to: params.emit_events_to,
|
emit_events_to: params.emit_events_to,
|
||||||
emit_response_body_chunks_to: params.emit_response_body_chunks_to,
|
|
||||||
existing_response: None,
|
existing_response: None,
|
||||||
plugin_manager: params.plugin_manager,
|
plugin_manager: params.plugin_manager,
|
||||||
encryption_manager: params.encryption_manager,
|
encryption_manager: params.encryption_manager,
|
||||||
@@ -402,7 +397,6 @@ pub async fn send_http_request_with_plugins(
|
|||||||
cookie_jar_id: params.cookie_jar_id,
|
cookie_jar_id: params.cookie_jar_id,
|
||||||
response_dir: params.response_dir,
|
response_dir: params.response_dir,
|
||||||
emit_events_to: params.emit_events_to,
|
emit_events_to: params.emit_events_to,
|
||||||
emit_response_body_chunks_to: params.emit_response_body_chunks_to,
|
|
||||||
cancelled_rx: params.cancelled_rx,
|
cancelled_rx: params.cancelled_rx,
|
||||||
auth_context_id: None,
|
auth_context_id: None,
|
||||||
existing_response: params.existing_response,
|
existing_response: params.existing_response,
|
||||||
@@ -433,7 +427,6 @@ pub async fn send_http_request_by_id<T: TemplateCallback>(
|
|||||||
cookie_jar_id: params.cookie_jar_id,
|
cookie_jar_id: params.cookie_jar_id,
|
||||||
response_dir: params.response_dir,
|
response_dir: params.response_dir,
|
||||||
emit_events_to: params.emit_events_to,
|
emit_events_to: params.emit_events_to,
|
||||||
emit_response_body_chunks_to: params.emit_response_body_chunks_to,
|
|
||||||
cancelled_rx: params.cancelled_rx,
|
cancelled_rx: params.cancelled_rx,
|
||||||
existing_response: None,
|
existing_response: None,
|
||||||
prepare_sendable_request: params.prepare_sendable_request,
|
prepare_sendable_request: params.prepare_sendable_request,
|
||||||
@@ -694,17 +687,13 @@ pub async fn send_http_request<T: TemplateCallback>(
|
|||||||
Ok(n) => {
|
Ok(n) => {
|
||||||
written_bytes += n;
|
written_bytes += n;
|
||||||
let start_idx = response_body.len() - n;
|
let start_idx = response_body.len() - n;
|
||||||
let chunk = &response_body[start_idx..];
|
file.write_all(&response_body[start_idx..]).await.map_err(|source| {
|
||||||
file.write_all(chunk).await.map_err(|source| {
|
|
||||||
SendHttpRequestError::WriteResponseBody { path: body_path.clone(), source }
|
SendHttpRequestError::WriteResponseBody { path: body_path.clone(), source }
|
||||||
})?;
|
})?;
|
||||||
file.flush().await.map_err(|source| SendHttpRequestError::WriteResponseBody {
|
file.flush().await.map_err(|source| SendHttpRequestError::WriteResponseBody {
|
||||||
path: body_path.clone(),
|
path: body_path.clone(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
if let Some(tx) = params.emit_response_body_chunks_to.as_ref() {
|
|
||||||
let _ = tx.send(chunk.to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let should_update = now.duration_since(last_progress_update).as_millis()
|
let should_update = now.duration_since(last_progress_update).as_millis()
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@yaakapp/cli-darwin-arm64",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
|
||||||
},
|
|
||||||
"os": ["darwin"],
|
|
||||||
"cpu": ["arm64"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@yaakapp/cli-darwin-x64",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
|
||||||
},
|
|
||||||
"os": ["darwin"],
|
|
||||||
"cpu": ["x64"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@yaakapp/cli-linux-arm64",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
|
||||||
},
|
|
||||||
"os": ["linux"],
|
|
||||||
"cpu": ["arm64"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@yaakapp/cli-linux-x64",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
|
||||||
},
|
|
||||||
"os": ["linux"],
|
|
||||||
"cpu": ["x64"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@yaakapp/cli-win32-arm64",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
|
||||||
},
|
|
||||||
"os": ["win32"],
|
|
||||||
"cpu": ["arm64"]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@yaakapp/cli-win32-x64",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/mountain-loop/yaak.git"
|
|
||||||
},
|
|
||||||
"os": ["win32"],
|
|
||||||
"cpu": ["x64"]
|
|
||||||
}
|
|
||||||
2
npm/cli/.gitignore
vendored
2
npm/cli/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
yaak
|
|
||||||
yaak.exe
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user