mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-06 05:10:05 +01:00
Compare commits
14 Commits
yaak-cli-0
...
v2026.3.0-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b563319bed | ||
|
|
3d577dd7d9 | ||
|
|
591c68c59c | ||
|
|
a0cb7f813f | ||
|
|
cfab62707e | ||
|
|
267508e533 | ||
|
|
242f55b609 | ||
|
|
67a3dd15ac | ||
|
|
543325613b | ||
|
|
88f5f0e045 | ||
|
|
615f3134d2 | ||
|
|
0c7051d59c | ||
|
|
30f006401a | ||
|
|
3c12074db6 |
@@ -37,6 +37,7 @@ 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**: 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
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ Generate formatted markdown release notes for a Yaak tag.
|
||||
- Keep a blank line before and after the code fence.
|
||||
- Output the markdown code block last.
|
||||
- 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
|
||||
|
||||
|
||||
2
.github/workflows/release-app.yml
vendored
2
.github/workflows/release-app.yml
vendored
@@ -95,7 +95,7 @@ jobs:
|
||||
- name: Run JS Tests
|
||||
run: npm test
|
||||
- name: Run Rust Tests
|
||||
run: cargo test --all
|
||||
run: cargo test --all --exclude yaak-cli
|
||||
|
||||
- name: Set version
|
||||
run: npm run replace-version
|
||||
|
||||
242
Cargo.lock
generated
242
Cargo.lock
generated
@@ -173,6 +173,17 @@ version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "apollo-parser"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "947e21ff51879f8a40d7519dfe619268de2afba4042a8a43878276de3cb910f0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"rowan",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
version = "0.1.8"
|
||||
@@ -1200,7 +1211,7 @@ dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -1347,6 +1358,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "countme"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "cow-utils"
|
||||
version = "0.1.3"
|
||||
@@ -1405,6 +1422,31 @@ version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"parking_lot",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
@@ -2294,6 +2336,15 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuzzy-matcher"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
|
||||
dependencies = [
|
||||
"thread_local 1.1.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
@@ -3164,6 +3215,24 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inquire"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"crossterm",
|
||||
"dyn-clone",
|
||||
"fuzzy-matcher",
|
||||
"fxhash",
|
||||
"newline-converter",
|
||||
"once_cell",
|
||||
"tempfile",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "interfaces"
|
||||
version = "0.0.8"
|
||||
@@ -3756,6 +3825,18 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.4"
|
||||
@@ -3851,6 +3932,15 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "newline-converter"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
@@ -3942,7 +4032,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"mio",
|
||||
"mio 1.0.4",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.59.0",
|
||||
@@ -4483,7 +4573,7 @@ checksum = "75b1853bc34cadaa90aa09f95713d8b77ec0c0d3e2d90ccf7a74216f40d20850"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"postcard",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
@@ -4501,7 +4591,7 @@ dependencies = [
|
||||
"textwrap",
|
||||
"thiserror 2.0.17",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4526,7 +4616,7 @@ dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"oxc_data_structures",
|
||||
"oxc_estree",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -4582,7 +4672,7 @@ dependencies = [
|
||||
"oxc_index",
|
||||
"oxc_syntax",
|
||||
"petgraph 0.8.3",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4603,7 +4693,7 @@ dependencies = [
|
||||
"oxc_sourcemap",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4615,7 +4705,7 @@ dependencies = [
|
||||
"cow-utils",
|
||||
"oxc-browserslist",
|
||||
"oxc_syntax",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -4690,7 +4780,7 @@ dependencies = [
|
||||
"oxc_ecmascript",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4707,7 +4797,7 @@ dependencies = [
|
||||
"oxc_semantic",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4732,7 +4822,7 @@ dependencies = [
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"oxc_traverse",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4754,7 +4844,7 @@ dependencies = [
|
||||
"oxc_regular_expression",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
@@ -4770,7 +4860,7 @@ dependencies = [
|
||||
"oxc_diagnostics",
|
||||
"oxc_span",
|
||||
"phf 0.13.1",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"unicode-id-start",
|
||||
]
|
||||
|
||||
@@ -4787,7 +4877,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"papaya",
|
||||
"pnp",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"self_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4817,7 +4907,7 @@ dependencies = [
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"phf 0.13.1",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"self_cell",
|
||||
]
|
||||
|
||||
@@ -4829,7 +4919,7 @@ checksum = "c7f89482522f3cd820817d48ee4ade5b10822060d6e5e4d419f05f6d8bd29d70"
|
||||
dependencies = [
|
||||
"base64-simd",
|
||||
"json-escape-simd",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@@ -4893,7 +4983,7 @@ dependencies = [
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"oxc_traverse",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
@@ -4918,7 +5008,7 @@ dependencies = [
|
||||
"oxc_syntax",
|
||||
"oxc_transformer",
|
||||
"oxc_traverse",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4936,7 +5026,7 @@ dependencies = [
|
||||
"oxc_semantic",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5341,7 +5431,7 @@ dependencies = [
|
||||
"nodejs-built-in-modules",
|
||||
"pathdiff",
|
||||
"radix_trie",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
@@ -5460,6 +5550,18 @@ dependencies = [
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_graphql"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea8c38ecedb3d28a998ea783469a78587f5f984d61226cf071f6979861e9e6a9"
|
||||
dependencies = [
|
||||
"apollo-parser",
|
||||
"memchr",
|
||||
"rowan",
|
||||
"tiny_pretty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
@@ -5640,7 +5742,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.17",
|
||||
@@ -5660,7 +5762,7 @@ dependencies = [
|
||||
"lru-slab",
|
||||
"rand 0.9.1",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
@@ -6154,7 +6256,7 @@ dependencies = [
|
||||
"rolldown_tracing",
|
||||
"rolldown_utils",
|
||||
"rolldown_watcher",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"string_wizard",
|
||||
@@ -6171,7 +6273,7 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77dff57c9de498bb1eb5b1ce682c2e3a0ae956b266fa0933c3e151b87b078967"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
@@ -6196,7 +6298,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log 0.4.29",
|
||||
"mio",
|
||||
"mio 1.0.4",
|
||||
"rolldown-notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -6244,7 +6346,7 @@ dependencies = [
|
||||
"rolldown_sourcemap",
|
||||
"rolldown_std_utils",
|
||||
"rolldown_utils",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simdutf8",
|
||||
@@ -6262,7 +6364,7 @@ dependencies = [
|
||||
"blake3",
|
||||
"dashmap",
|
||||
"rolldown_debug_action",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
@@ -6319,7 +6421,7 @@ dependencies = [
|
||||
"rolldown-ariadne",
|
||||
"rolldown_utils",
|
||||
"ropey",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"sugar_path",
|
||||
]
|
||||
|
||||
@@ -6353,7 +6455,7 @@ dependencies = [
|
||||
"rolldown_resolver",
|
||||
"rolldown_sourcemap",
|
||||
"rolldown_utils",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"string_wizard",
|
||||
@@ -6373,7 +6475,7 @@ dependencies = [
|
||||
"rolldown_common",
|
||||
"rolldown_plugin",
|
||||
"rolldown_utils",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde_json",
|
||||
"xxhash-rust",
|
||||
]
|
||||
@@ -6444,7 +6546,7 @@ dependencies = [
|
||||
"oxc",
|
||||
"oxc_sourcemap",
|
||||
"rolldown_utils",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6496,7 +6598,7 @@ dependencies = [
|
||||
"regex 1.11.1",
|
||||
"regress",
|
||||
"rolldown_std_utils",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde_json",
|
||||
"simdutf8",
|
||||
"sugar_path",
|
||||
@@ -6526,6 +6628,18 @@ dependencies = [
|
||||
"str_indices",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rowan"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21"
|
||||
dependencies = [
|
||||
"countme",
|
||||
"hashbrown 0.14.5",
|
||||
"rustc-hash 1.1.0",
|
||||
"text-size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
@@ -6568,6 +6682,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
@@ -7173,6 +7293,27 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.5"
|
||||
@@ -7359,7 +7500,7 @@ dependencies = [
|
||||
"memchr",
|
||||
"oxc_index",
|
||||
"oxc_sourcemap",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -8060,6 +8201,12 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
@@ -8068,7 +8215,7 @@ checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8182,6 +8329,12 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny_pretty"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650d82e943da333637be9f1567d33d605e76810a26464edfd7ae74f7ef181e95"
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
@@ -8215,7 +8368,7 @@ checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.0.4",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.6.1",
|
||||
@@ -8785,6 +8938,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
@@ -9562,6 +9721,15 @@ dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@@ -10089,6 +10257,7 @@ dependencies = [
|
||||
"md5 0.8.0",
|
||||
"mime_guess",
|
||||
"openssl-sys",
|
||||
"pretty_graphql",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rand 0.9.1",
|
||||
@@ -10141,6 +10310,7 @@ dependencies = [
|
||||
name = "yaak-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"assert_cmd",
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
@@ -10150,6 +10320,7 @@ dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
"include_dir",
|
||||
"inquire",
|
||||
"keyring",
|
||||
"log 0.4.29",
|
||||
"oxc_resolver",
|
||||
@@ -10288,6 +10459,7 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"yaak-common",
|
||||
"yaak-models",
|
||||
"yaak-templates",
|
||||
"yaak-tls",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
@@ -48,7 +48,8 @@
|
||||
"!src-web/routeTree.gen.ts",
|
||||
"!packages/plugin-runtime-types/lib",
|
||||
"!**/bindings",
|
||||
"!flatpak"
|
||||
"!flatpak",
|
||||
"!npm"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,14 @@ name = "yaak"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
arboard = "3"
|
||||
base64 = "0.22"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
console = "0.15"
|
||||
dirs = "6"
|
||||
env_logger = "0.11"
|
||||
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"] }
|
||||
|
||||
@@ -21,6 +21,10 @@ pub struct Cli {
|
||||
#[arg(long, short, global = true)]
|
||||
pub environment: Option<String>,
|
||||
|
||||
/// Cookie jar ID to use when sending requests
|
||||
#[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)]
|
||||
pub verbose: bool,
|
||||
@@ -58,6 +62,9 @@ pub enum Commands {
|
||||
/// Send a request, folder, or workspace by ID
|
||||
Send(SendArgs),
|
||||
|
||||
/// Cookie jar commands
|
||||
CookieJar(CookieJarArgs),
|
||||
|
||||
/// Workspace commands
|
||||
Workspace(WorkspaceArgs),
|
||||
|
||||
@@ -85,6 +92,22 @@ pub struct SendArgs {
|
||||
pub fail_fast: bool,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
@@ -158,8 +181,8 @@ pub struct RequestArgs {
|
||||
pub enum RequestCommands {
|
||||
/// List requests in a workspace
|
||||
List {
|
||||
/// Workspace ID
|
||||
workspace_id: String,
|
||||
/// Workspace ID (optional when exactly one workspace exists)
|
||||
workspace_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Show a request as JSON
|
||||
@@ -267,8 +290,8 @@ pub struct FolderArgs {
|
||||
pub enum FolderCommands {
|
||||
/// List folders in a workspace
|
||||
List {
|
||||
/// Workspace ID
|
||||
workspace_id: String,
|
||||
/// Workspace ID (optional when exactly one workspace exists)
|
||||
workspace_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Show a folder as JSON
|
||||
@@ -324,8 +347,8 @@ pub struct EnvironmentArgs {
|
||||
pub enum EnvironmentCommands {
|
||||
/// List environments in a workspace
|
||||
List {
|
||||
/// Workspace ID
|
||||
workspace_id: String,
|
||||
/// Workspace ID (optional when exactly one workspace exists)
|
||||
workspace_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Output JSON schema for environment create/update payloads
|
||||
@@ -421,6 +444,9 @@ pub enum PluginCommands {
|
||||
/// 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),
|
||||
}
|
||||
@@ -441,3 +467,9 @@ pub struct GenerateArgs {
|
||||
#[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,
|
||||
}
|
||||
|
||||
42
crates-cli/yaak-cli/src/commands/cookie_jar.rs
Normal file
42
crates-cli/yaak-cli/src/commands/cookie_jar.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
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(())
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use crate::utils::json::{
|
||||
parse_required_json, require_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::util::UpdateSource;
|
||||
@@ -14,7 +15,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
|
||||
pub fn run(ctx: &CliContext, args: EnvironmentArgs) -> i32 {
|
||||
let result = match args.command {
|
||||
EnvironmentCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||
EnvironmentCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||
EnvironmentCommands::Schema { pretty } => schema(pretty),
|
||||
EnvironmentCommands::Show { environment_id } => show(ctx, &environment_id),
|
||||
EnvironmentCommands::Create { workspace_id, name, json } => {
|
||||
@@ -45,10 +46,11 @@ fn schema(pretty: bool) -> CommandResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "environment list")?;
|
||||
let environments = ctx
|
||||
.db()
|
||||
.list_environments_ensure_base(workspace_id)
|
||||
.list_environments_ensure_base(&workspace_id)
|
||||
.map_err(|e| format!("Failed to list environments: {e}"))?;
|
||||
|
||||
if environments.is_empty() {
|
||||
@@ -92,8 +94,14 @@ fn create(
|
||||
validate_create_id(&payload, "environment")?;
|
||||
let mut environment: Environment = serde_json::from_value(payload)
|
||||
.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() {
|
||||
Some(resolve_workspace_id(ctx, None, "environment create")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref(),
|
||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
||||
&mut environment.workspace_id,
|
||||
"environment create",
|
||||
)?;
|
||||
@@ -111,9 +119,8 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
"environment create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let workspace_id =
|
||||
resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "environment create")?;
|
||||
let name = name.ok_or_else(|| {
|
||||
"environment create requires --name unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::utils::json::{
|
||||
apply_merge_patch, is_json_shorthand, merge_workspace_id_arg, parse_optional_json,
|
||||
parse_required_json, require_id, validate_create_id,
|
||||
};
|
||||
use crate::utils::workspace::resolve_workspace_id;
|
||||
use yaak_models::models::Folder;
|
||||
use yaak_models::util::UpdateSource;
|
||||
|
||||
@@ -12,7 +13,7 @@ type CommandResult<T = ()> = std::result::Result<T, String>;
|
||||
|
||||
pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
||||
let result = match args.command {
|
||||
FolderCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||
FolderCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||
FolderCommands::Show { folder_id } => show(ctx, &folder_id),
|
||||
FolderCommands::Create { workspace_id, name, json } => {
|
||||
create(ctx, workspace_id, name, json)
|
||||
@@ -30,9 +31,10 @@ pub fn run(ctx: &CliContext, args: FolderArgs) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "folder list")?;
|
||||
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() {
|
||||
println!("No folders found in workspace {}", workspace_id);
|
||||
} else {
|
||||
@@ -72,8 +74,14 @@ fn create(
|
||||
validate_create_id(&payload, "folder")?;
|
||||
let mut folder: Folder = serde_json::from_value(payload)
|
||||
.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()
|
||||
{
|
||||
Some(resolve_workspace_id(ctx, None, "folder create")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref(),
|
||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
||||
&mut folder.workspace_id,
|
||||
"folder create",
|
||||
)?;
|
||||
@@ -87,9 +95,7 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
"folder create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "folder create")?;
|
||||
let name = name.ok_or_else(|| {
|
||||
"folder create requires --name unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod cookie_jar;
|
||||
pub mod environment;
|
||||
pub mod folder;
|
||||
pub mod plugin;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::cli::{GenerateArgs, PluginArgs, PluginCommands, PluginPathArg};
|
||||
use crate::cli::{GenerateArgs, InstallPluginArgs, PluginPathArg};
|
||||
use crate::context::CliContext;
|
||||
use crate::ui;
|
||||
use crate::utils::http;
|
||||
use keyring::Entry;
|
||||
@@ -15,6 +16,11 @@ 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;
|
||||
|
||||
@@ -57,12 +63,13 @@ pub async fn run_build(args: PluginPathArg) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(args: PluginArgs) -> i32 {
|
||||
match args.command {
|
||||
PluginCommands::Build(args) => run_build(args).await,
|
||||
PluginCommands::Dev(args) => run_dev(args).await,
|
||||
PluginCommands::Generate(args) => run_generate(args).await,
|
||||
PluginCommands::Publish(args) => run_publish(args).await,
|
||||
pub async fn run_install(context: &CliContext, args: InstallPluginArgs) -> i32 {
|
||||
match install(context, args).await {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
ui::error(&error);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,6 +257,113 @@ async fn publish(args: PluginPathArg) -> CommandResult {
|
||||
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,
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::utils::json::{
|
||||
parse_required_json, require_id, validate_create_id,
|
||||
};
|
||||
use crate::utils::schema::append_agent_hints;
|
||||
use crate::utils::workspace::resolve_workspace_id;
|
||||
use schemars::schema_for;
|
||||
use serde_json::{Map, Value, json};
|
||||
use std::collections::HashMap;
|
||||
@@ -24,13 +25,16 @@ pub async fn run(
|
||||
ctx: &CliContext,
|
||||
args: RequestArgs,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> i32 {
|
||||
let result = match args.command {
|
||||
RequestCommands::List { workspace_id } => list(ctx, &workspace_id),
|
||||
RequestCommands::List { workspace_id } => list(ctx, workspace_id.as_deref()),
|
||||
RequestCommands::Show { request_id } => show(ctx, &request_id),
|
||||
RequestCommands::Send { request_id } => {
|
||||
return match send_request_by_id(ctx, &request_id, environment, verbose).await {
|
||||
return match send_request_by_id(ctx, &request_id, environment, cookie_jar_id, verbose)
|
||||
.await
|
||||
{
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {error}");
|
||||
@@ -63,10 +67,11 @@ pub async fn run(
|
||||
}
|
||||
}
|
||||
|
||||
fn list(ctx: &CliContext, workspace_id: &str) -> CommandResult {
|
||||
fn list(ctx: &CliContext, workspace_id: Option<&str>) -> CommandResult {
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id, "request list")?;
|
||||
let requests = ctx
|
||||
.db()
|
||||
.list_http_requests(workspace_id)
|
||||
.list_http_requests(&workspace_id)
|
||||
.map_err(|e| format!("Failed to list requests: {e}"))?;
|
||||
if requests.is_empty() {
|
||||
println!("No requests found in workspace {}", workspace_id);
|
||||
@@ -350,8 +355,14 @@ fn create(
|
||||
validate_create_id(&payload, "request")?;
|
||||
let mut request: HttpRequest = serde_json::from_value(payload)
|
||||
.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()
|
||||
{
|
||||
Some(resolve_workspace_id(ctx, None, "request create")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
merge_workspace_id_arg(
|
||||
workspace_id_arg.as_deref(),
|
||||
workspace_id_arg.as_deref().or(fallback_workspace_id.as_deref()),
|
||||
&mut request.workspace_id,
|
||||
"request create",
|
||||
)?;
|
||||
@@ -365,9 +376,7 @@ fn create(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let workspace_id = workspace_id_arg.ok_or_else(|| {
|
||||
"request create requires workspace_id unless JSON payload is provided".to_string()
|
||||
})?;
|
||||
let workspace_id = resolve_workspace_id(ctx, workspace_id_arg.as_deref(), "request create")?;
|
||||
let name = name.unwrap_or_default();
|
||||
let url = url.unwrap_or_default();
|
||||
let method = method.unwrap_or_else(|| "GET".to_string());
|
||||
@@ -436,6 +445,7 @@ pub async fn send_request_by_id(
|
||||
ctx: &CliContext,
|
||||
request_id: &str,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
let request =
|
||||
@@ -447,6 +457,7 @@ pub async fn send_request_by_id(
|
||||
&http_request.id,
|
||||
&http_request.workspace_id,
|
||||
environment,
|
||||
cookie_jar_id,
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
@@ -465,9 +476,13 @@ async fn send_http_request_by_id(
|
||||
request_id: &str,
|
||||
workspace_id: &str,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
let plugin_context = PluginContext::new(None, Some(workspace_id.to_string()));
|
||||
let cookie_jar_id = resolve_cookie_jar_id(ctx, workspace_id, cookie_jar_id)?;
|
||||
|
||||
let plugin_context =
|
||||
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>>();
|
||||
@@ -495,7 +510,7 @@ async fn send_http_request_by_id(
|
||||
request_id,
|
||||
environment_id: environment,
|
||||
update_source: UpdateSource::Sync,
|
||||
cookie_jar_id: None,
|
||||
cookie_jar_id,
|
||||
response_dir: &response_dir,
|
||||
emit_events_to: Some(event_tx),
|
||||
emit_response_body_chunks_to: Some(body_chunk_tx),
|
||||
@@ -512,3 +527,22 @@ async fn send_http_request_by_id(
|
||||
result.map_err(|e| e.to_string())?;
|
||||
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,6 +2,7 @@ use crate::cli::SendArgs;
|
||||
use crate::commands::request;
|
||||
use crate::context::CliContext;
|
||||
use futures::future::join_all;
|
||||
use yaak_models::queries::any_request::AnyRequest;
|
||||
|
||||
enum ExecutionMode {
|
||||
Sequential,
|
||||
@@ -12,9 +13,10 @@ pub async fn run(
|
||||
ctx: &CliContext,
|
||||
args: SendArgs,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> i32 {
|
||||
match send_target(ctx, args, environment, verbose).await {
|
||||
match send_target(ctx, args, environment, cookie_jar_id, verbose).await {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
eprintln!("Error: {error}");
|
||||
@@ -27,30 +29,70 @@ async fn send_target(
|
||||
ctx: &CliContext,
|
||||
args: SendArgs,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
let mode = if args.parallel { ExecutionMode::Parallel } else { ExecutionMode::Sequential };
|
||||
|
||||
if ctx.db().get_any_request(&args.id).is_ok() {
|
||||
return request::send_request_by_id(ctx, &args.id, environment, verbose).await;
|
||||
if let Ok(request) = ctx.db().get_any_request(&args.id) {
|
||||
let workspace_id = match &request {
|
||||
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 ctx.db().get_folder(&args.id).is_ok() {
|
||||
if let Ok(folder) = ctx.db().get_folder(&args.id) {
|
||||
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)?;
|
||||
if request_ids.is_empty() {
|
||||
println!("No requests found in folder {}", args.id);
|
||||
return Ok(());
|
||||
}
|
||||
return send_many(ctx, request_ids, mode, args.fail_fast, environment, verbose).await;
|
||||
return send_many(
|
||||
ctx,
|
||||
request_ids,
|
||||
mode,
|
||||
args.fail_fast,
|
||||
environment,
|
||||
resolved_cookie_jar_id.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if ctx.db().get_workspace(&args.id).is_ok() {
|
||||
if let Ok(workspace) = ctx.db().get_workspace(&args.id) {
|
||||
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)?;
|
||||
if request_ids.is_empty() {
|
||||
println!("No requests found in workspace {}", args.id);
|
||||
return Ok(());
|
||||
}
|
||||
return send_many(ctx, request_ids, mode, args.fail_fast, environment, verbose).await;
|
||||
return send_many(
|
||||
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))
|
||||
@@ -131,6 +173,7 @@ async fn send_many(
|
||||
mode: ExecutionMode,
|
||||
fail_fast: bool,
|
||||
environment: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
let mut success_count = 0usize;
|
||||
@@ -139,7 +182,15 @@ async fn send_many(
|
||||
match mode {
|
||||
ExecutionMode::Sequential => {
|
||||
for request_id in request_ids {
|
||||
match request::send_request_by_id(ctx, &request_id, environment, verbose).await {
|
||||
match request::send_request_by_id(
|
||||
ctx,
|
||||
&request_id,
|
||||
environment,
|
||||
cookie_jar_id,
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => success_count += 1,
|
||||
Err(error) => {
|
||||
failures.push((request_id, error));
|
||||
@@ -156,7 +207,14 @@ async fn send_many(
|
||||
.map(|request_id| async move {
|
||||
(
|
||||
request_id.clone(),
|
||||
request::send_request_by_id(ctx, request_id, environment, verbose).await,
|
||||
request::send_request_by_id(
|
||||
ctx,
|
||||
request_id,
|
||||
environment,
|
||||
cookie_jar_id,
|
||||
verbose,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -18,6 +18,14 @@ const EMBEDDED_PLUGIN_RUNTIME: &str = include_str!(concat!(
|
||||
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 {
|
||||
data_dir: PathBuf,
|
||||
query_manager: QueryManager,
|
||||
@@ -28,63 +36,71 @@ pub struct CliContext {
|
||||
}
|
||||
|
||||
impl CliContext {
|
||||
pub async fn initialize(data_dir: PathBuf, app_id: &str, with_plugins: bool) -> Self {
|
||||
pub fn new(data_dir: PathBuf, app_id: &str) -> Self {
|
||||
let db_path = data_dir.join("db.sqlite");
|
||||
let blob_path = data_dir.join("blobs.sqlite");
|
||||
|
||||
let (query_manager, blob_manager, _rx) = yaak_models::init_standalone(&db_path, &blob_path)
|
||||
.expect("Failed to initialize database");
|
||||
|
||||
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");
|
||||
|
||||
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(&data_dir)
|
||||
.expect("Failed to prepare embedded plugin runtime")
|
||||
});
|
||||
|
||||
match PluginManager::new(
|
||||
vendored_plugin_dir,
|
||||
installed_plugin_dir,
|
||||
node_bin_path,
|
||||
plugin_runtime_main,
|
||||
&query_manager,
|
||||
&PluginContext::new_empty(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(plugin_manager) => Some(Arc::new(plugin_manager)),
|
||||
let (query_manager, blob_manager, _rx) =
|
||||
match yaak_models::init_standalone(&db_path, &blob_path) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("Warning: Failed to initialize plugins: {err}");
|
||||
None
|
||||
eprintln!("Error: Failed to initialize database: {err}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} 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
|
||||
};
|
||||
};
|
||||
let encryption_manager = Arc::new(EncryptionManager::new(query_manager.clone(), app_id));
|
||||
|
||||
Self {
|
||||
data_dir,
|
||||
query_manager,
|
||||
blob_manager,
|
||||
encryption_manager,
|
||||
plugin_manager,
|
||||
plugin_event_bridge: Mutex::new(plugin_event_bridge),
|
||||
plugin_manager: None,
|
||||
plugin_event_bridge: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ mod version;
|
||||
mod version_check;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands, RequestCommands};
|
||||
use context::CliContext;
|
||||
use cli::{Cli, Commands, PluginCommands, RequestCommands};
|
||||
use context::{CliContext, CliExecutionContext};
|
||||
use yaak_models::queries::any_request::AnyRequest;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let Cli { data_dir, environment, verbose, log, command } = Cli::parse();
|
||||
let Cli { data_dir, environment, cookie_jar, verbose, log, command } = Cli::parse();
|
||||
|
||||
if let Some(log_level) = log {
|
||||
match log_level {
|
||||
@@ -35,72 +36,206 @@ async fn main() {
|
||||
|
||||
version_check::maybe_check_for_updates().await;
|
||||
|
||||
let needs_context = matches!(
|
||||
&command,
|
||||
Commands::Send(_)
|
||||
| Commands::Workspace(_)
|
||||
| Commands::Request(_)
|
||||
| Commands::Folder(_)
|
||||
| Commands::Environment(_)
|
||||
);
|
||||
|
||||
let needs_plugins = matches!(
|
||||
&command,
|
||||
Commands::Send(_)
|
||||
| Commands::Request(cli::RequestArgs {
|
||||
command: RequestCommands::Send { .. } | RequestCommands::Schema { .. },
|
||||
})
|
||||
);
|
||||
|
||||
let context = if needs_context {
|
||||
Some(CliContext::initialize(data_dir, app_id, needs_plugins).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let exit_code = match command {
|
||||
Commands::Auth(args) => commands::auth::run(args).await,
|
||||
Commands::Plugin(args) => commands::plugin::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::run(
|
||||
context.as_ref().expect("context initialized for send"),
|
||||
args,
|
||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||
match resolve_send_execution_context(
|
||||
&context,
|
||||
&args.id,
|
||||
environment.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
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.as_ref().expect("context initialized for workspace"),
|
||||
args,
|
||||
),
|
||||
Commands::Request(args) => {
|
||||
commands::request::run(
|
||||
context.as_ref().expect("context initialized for request"),
|
||||
args,
|
||||
environment.as_deref(),
|
||||
verbose,
|
||||
)
|
||||
.await
|
||||
let mut context = CliContext::new(data_dir.clone(), app_id);
|
||||
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) => {
|
||||
commands::folder::run(context.as_ref().expect("context initialized for 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::Environment(args) => commands::environment::run(
|
||||
context.as_ref().expect("context initialized for environment"),
|
||||
args,
|
||||
),
|
||||
};
|
||||
|
||||
if let Some(context) = &context {
|
||||
context.shutdown().await;
|
||||
}
|
||||
|
||||
if exit_code != 0 {
|
||||
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)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,3 +2,4 @@ pub mod confirm;
|
||||
pub mod http;
|
||||
pub mod json;
|
||||
pub mod schema;
|
||||
pub mod workspace;
|
||||
|
||||
19
crates-cli/yaak-cli/src/utils/workspace.rs
Normal file
19
crates-cli/yaak-cli/src/utils/workspace.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
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.")),
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client
|
||||
http = { version = "1.2.0", default-features = false }
|
||||
log = { workspace = true }
|
||||
md5 = "0.8.0"
|
||||
pretty_graphql = "0.2"
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_sqlite = "0.25.0"
|
||||
mime_guess = "2.0.5"
|
||||
|
||||
@@ -34,6 +34,7 @@ use tokio::time;
|
||||
use yaak_common::command::new_checked_command;
|
||||
use yaak_crypto::manager::EncryptionManager;
|
||||
use yaak_grpc::manager::{GrpcConfig, GrpcHandle};
|
||||
use yaak_templates::strip_json_comments::strip_json_comments;
|
||||
use yaak_grpc::{Code, ServiceDefinition, serialize_message};
|
||||
use yaak_mac_window::AppHandleMacWindowExt;
|
||||
use yaak_models::models::{
|
||||
@@ -131,11 +132,7 @@ async fn cmd_metadata(app_handle: AppHandle) -> YaakResult<AppMetaData> {
|
||||
}
|
||||
|
||||
async fn detect_cli_version() -> Option<String> {
|
||||
// Prefer `yaak`, but support the legacy `yaakcli` alias if present.
|
||||
if let Some(version) = detect_cli_version_for_binary("yaak").await {
|
||||
return Some(version);
|
||||
}
|
||||
detect_cli_version_for_binary("yaakcli").await
|
||||
detect_cli_version_for_binary("yaak").await
|
||||
}
|
||||
|
||||
async fn detect_cli_version_for_binary(program: &str) -> Option<String> {
|
||||
@@ -437,6 +434,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
result.expect("Failed to render template")
|
||||
})
|
||||
});
|
||||
let msg = strip_json_comments(&msg);
|
||||
in_msg_tx.try_send(msg.clone()).unwrap();
|
||||
}
|
||||
Ok(IncomingMsg::Commit) => {
|
||||
@@ -472,6 +470,7 @@ async fn cmd_grpc_go<R: Runtime>(
|
||||
&RenderOptions { error_behavior: RenderErrorBehavior::Throw },
|
||||
)
|
||||
.await?;
|
||||
let msg = strip_json_comments(&msg);
|
||||
|
||||
app_handle.db().upsert_grpc_event(
|
||||
&GrpcEvent {
|
||||
@@ -873,6 +872,14 @@ async fn cmd_format_json(text: &str) -> YaakResult<String> {
|
||||
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]
|
||||
async fn cmd_http_response_body<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
@@ -1642,6 +1649,7 @@ pub fn run() {
|
||||
cmd_http_request_body,
|
||||
cmd_http_response_body,
|
||||
cmd_format_json,
|
||||
cmd_format_graphql,
|
||||
cmd_get_http_authentication_summaries,
|
||||
cmd_get_http_authentication_config,
|
||||
cmd_get_sse_events,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use log::info;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
pub use yaak::render::render_http_request;
|
||||
use yaak_models::models::{Environment, GrpcRequest, HttpRequestHeader};
|
||||
pub use yaak::render::{render_grpc_request, render_http_request};
|
||||
use yaak_models::models::Environment;
|
||||
use yaak_models::render::make_vars_hashmap;
|
||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||
|
||||
@@ -25,61 +23,3 @@ pub async fn render_json_value<T: TemplateCallback>(
|
||||
let vars = &make_vars_hashmap(environment_chain);
|
||||
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() })
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use yaak_models::util::UpdateSource;
|
||||
use yaak_plugins::events::{CallHttpAuthenticationRequest, HttpHeader, RenderPurpose};
|
||||
use yaak_plugins::manager::PluginManager;
|
||||
use yaak_plugins::template_callback::PluginTemplateCallback;
|
||||
use yaak_templates::strip_json_comments::maybe_strip_json_comments;
|
||||
use yaak_templates::{RenderErrorBehavior, RenderOptions};
|
||||
use yaak_tls::find_client_certificate;
|
||||
use yaak_ws::{WebsocketManager, render_websocket_request};
|
||||
@@ -72,8 +73,10 @@ pub async fn cmd_ws_send<R: Runtime>(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let message = maybe_strip_json_comments(&request.message);
|
||||
|
||||
let mut ws_manager = ws_manager.lock().await;
|
||||
ws_manager.send(&connection.id, Message::Text(request.message.clone().into())).await?;
|
||||
ws_manager.send(&connection.id, Message::Text(message.clone().into())).await?;
|
||||
|
||||
app_handle.db().upsert_websocket_event(
|
||||
&WebsocketEvent {
|
||||
@@ -82,7 +85,7 @@ pub async fn cmd_ws_send<R: Runtime>(
|
||||
workspace_id: connection.workspace_id.clone(),
|
||||
is_server: false,
|
||||
message_type: WebsocketEventType::Text,
|
||||
message: request.message.into(),
|
||||
message: message.into(),
|
||||
..Default::default()
|
||||
},
|
||||
&UpdateSource::from_window_label(window.label()),
|
||||
|
||||
@@ -12,6 +12,11 @@ unsafe impl Sync for UnsafeWindowHandle {}
|
||||
|
||||
const WINDOW_CONTROL_PAD_X: f64 = 13.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_";
|
||||
|
||||
pub(crate) fn update_window_title<R: Runtime>(window: Window<R>, title: String) {
|
||||
@@ -95,12 +100,29 @@ fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64,
|
||||
ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
|
||||
let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
|
||||
|
||||
let title_bar_container_view = close.superview().superview();
|
||||
|
||||
let close_rect: NSRect = msg_send![close, frame];
|
||||
let button_height = close_rect.size.height;
|
||||
|
||||
let title_bar_frame_height = button_height + y;
|
||||
let title_bar_container_view = close.superview().superview();
|
||||
|
||||
// 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);
|
||||
title_bar_rect.size.height = title_bar_frame_height;
|
||||
title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height;
|
||||
|
||||
@@ -21,3 +21,10 @@ pub fn get_str_map<'a>(v: &'a BTreeMap<String, Value>, key: &str) -> &'a str {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,5 @@ tower-service = "0.3.3"
|
||||
urlencoding = "2.1.3"
|
||||
yaak-common = { workspace = true }
|
||||
yaak-models = { workspace = true }
|
||||
yaak-templates = { workspace = true }
|
||||
yaak-tls = { workspace = true }
|
||||
|
||||
@@ -30,6 +30,8 @@ pub enum HttpResponseEvent {
|
||||
url: String,
|
||||
status: u16,
|
||||
behavior: RedirectBehavior,
|
||||
dropped_body: bool,
|
||||
dropped_headers: Vec<String>,
|
||||
},
|
||||
SendUrl {
|
||||
method: String,
|
||||
@@ -67,12 +69,28 @@ impl Display for HttpResponseEvent {
|
||||
match self {
|
||||
HttpResponseEvent::Setting(name, value) => write!(f, "* Setting {}={}", name, value),
|
||||
HttpResponseEvent::Info(s) => write!(f, "* {}", s),
|
||||
HttpResponseEvent::Redirect { url, status, behavior } => {
|
||||
HttpResponseEvent::Redirect {
|
||||
url,
|
||||
status,
|
||||
behavior,
|
||||
dropped_body,
|
||||
dropped_headers,
|
||||
} => {
|
||||
let behavior_str = match behavior {
|
||||
RedirectBehavior::Preserve => "preserve",
|
||||
RedirectBehavior::DropBody => "drop body",
|
||||
};
|
||||
write!(f, "* Redirect {} -> {} ({})", status, url, behavior_str)
|
||||
let body_str = if *dropped_body { ", body dropped" } else { "" };
|
||||
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 {
|
||||
method,
|
||||
@@ -130,13 +148,21 @@ impl From<HttpResponseEvent> for yaak_models::models::HttpResponseEventData {
|
||||
match event {
|
||||
HttpResponseEvent::Setting(name, value) => D::Setting { name, value },
|
||||
HttpResponseEvent::Info(message) => D::Info { message },
|
||||
HttpResponseEvent::Redirect { url, status, behavior } => D::Redirect {
|
||||
HttpResponseEvent::Redirect {
|
||||
url,
|
||||
status,
|
||||
behavior,
|
||||
dropped_body,
|
||||
dropped_headers,
|
||||
} => D::Redirect {
|
||||
url,
|
||||
status,
|
||||
behavior: match behavior {
|
||||
RedirectBehavior::Preserve => "preserve".to_string(),
|
||||
RedirectBehavior::DropBody => "drop_body".to_string(),
|
||||
},
|
||||
dropped_body,
|
||||
dropped_headers,
|
||||
},
|
||||
HttpResponseEvent::SendUrl {
|
||||
method,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::cookies::CookieStore;
|
||||
use crate::error::Result;
|
||||
use crate::sender::{HttpResponse, HttpResponseEvent, HttpSender, RedirectBehavior};
|
||||
use crate::types::SendableHttpRequest;
|
||||
use crate::types::{SendableBody, SendableHttpRequest};
|
||||
use log::debug;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::watch::Receiver;
|
||||
@@ -87,6 +87,11 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
};
|
||||
|
||||
// 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 {
|
||||
url: current_url.clone(),
|
||||
method: current_method.clone(),
|
||||
@@ -182,8 +187,6 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
format!("{}/{}", base_path, location)
|
||||
};
|
||||
|
||||
Self::remove_sensitive_headers(&mut current_headers, &previous_url, ¤t_url);
|
||||
|
||||
// Determine redirect behavior based on status code and method
|
||||
let behavior = if status == 303 {
|
||||
// 303 See Other always changes to GET
|
||||
@@ -197,11 +200,8 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
RedirectBehavior::Preserve
|
||||
};
|
||||
|
||||
send_event(HttpResponseEvent::Redirect {
|
||||
url: current_url.clone(),
|
||||
status,
|
||||
behavior: behavior.clone(),
|
||||
});
|
||||
let mut dropped_headers =
|
||||
Self::remove_sensitive_headers(&mut current_headers, &previous_url, ¤t_url);
|
||||
|
||||
// Handle method changes for certain redirect codes
|
||||
if matches!(behavior, RedirectBehavior::DropBody) {
|
||||
@@ -211,13 +211,40 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
// Remove content-related headers
|
||||
current_headers.retain(|h| {
|
||||
let name_lower = h.0.to_lowercase();
|
||||
!name_lower.starts_with("content-") && name_lower != "transfer-encoding"
|
||||
let should_drop =
|
||||
name_lower.starts_with("content-") || name_lower == "transfer-encoding";
|
||||
if should_drop {
|
||||
Self::push_header_if_missing(&mut dropped_headers, &h.0);
|
||||
}
|
||||
!should_drop
|
||||
});
|
||||
}
|
||||
|
||||
// Reset body for next iteration (since it was moved in the send call)
|
||||
// For redirects that change method to GET or for all redirects since body was consumed
|
||||
current_body = None;
|
||||
// Restore body for Preserve redirects (307/308), drop for others.
|
||||
// Stream bodies can't be replayed (same limitation as reqwest).
|
||||
current_body = if matches!(behavior, RedirectBehavior::Preserve) {
|
||||
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;
|
||||
}
|
||||
@@ -231,7 +258,8 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
headers: &mut Vec<(String, String)>,
|
||||
previous_url: &str,
|
||||
next_url: &str,
|
||||
) {
|
||||
) -> Vec<String> {
|
||||
let mut dropped_headers = Vec::new();
|
||||
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)))
|
||||
});
|
||||
@@ -241,13 +269,24 @@ impl<S: HttpSender> HttpTransaction<S> {
|
||||
if previous_host != next_host {
|
||||
headers.retain(|h| {
|
||||
let name_lower = h.0.to_lowercase();
|
||||
name_lower != "authorization"
|
||||
&& name_lower != "cookie"
|
||||
&& name_lower != "cookie2"
|
||||
&& name_lower != "proxy-authorization"
|
||||
&& name_lower != "www-authenticate"
|
||||
let should_drop = name_lower == "authorization"
|
||||
|| name_lower == "cookie"
|
||||
|| name_lower == "cookie2"
|
||||
|| name_lower == "proxy-authorization"
|
||||
|| 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
|
||||
|
||||
@@ -9,8 +9,9 @@ use std::collections::BTreeMap;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncRead;
|
||||
use yaak_common::serde::{get_bool, get_str, get_str_map};
|
||||
use yaak_common::serde::{get_bool, get_bool_map, get_str, get_str_map};
|
||||
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";
|
||||
|
||||
@@ -134,16 +135,69 @@ pub fn append_query_params(url: &str, params: Vec<(String, String)>) -> String {
|
||||
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 {
|
||||
let (url_string, params) = apply_path_placeholders(&ensure_proto(&r.url), &r.url_parameters);
|
||||
append_query_params(
|
||||
let mut url = append_query_params(
|
||||
&url_string,
|
||||
params
|
||||
.iter()
|
||||
.filter(|p| p.enabled && !p.name.is_empty())
|
||||
.map(|p| (p.name.clone(), p.value.clone()))
|
||||
.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)> {
|
||||
@@ -177,7 +231,7 @@ async fn build_body(
|
||||
(build_form_body(&body), Some("application/x-www-form-urlencoded".to_string()))
|
||||
}
|
||||
"multipart/form-data" => build_multipart_body(&body, &headers).await?,
|
||||
_ if body.contains_key("text") => (build_text_body(&body), None),
|
||||
_ if body.contains_key("text") => (build_text_body(&body, body_type), None),
|
||||
t => {
|
||||
warn!("Unsupported body type: {}", t);
|
||||
(None, None)
|
||||
@@ -252,13 +306,20 @@ async fn build_binary_body(
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_text_body(body: &BTreeMap<String, serde_json::Value>) -> Option<SendableBodyWithMeta> {
|
||||
fn build_text_body(body: &BTreeMap<String, serde_json::Value>, body_type: &str) -> Option<SendableBodyWithMeta> {
|
||||
let text = get_str_map(body, "text");
|
||||
if text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(SendableBodyWithMeta::Bytes(Bytes::from(text.to_string())))
|
||||
return 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 {
|
||||
text.to_string()
|
||||
};
|
||||
|
||||
Some(SendableBodyWithMeta::Bytes(Bytes::from(text)))
|
||||
}
|
||||
|
||||
fn build_graphql_body(
|
||||
@@ -266,7 +327,7 @@ fn build_graphql_body(
|
||||
body: &BTreeMap<String, serde_json::Value>,
|
||||
) -> Option<SendableBodyWithMeta> {
|
||||
let query = get_str_map(body, "query");
|
||||
let variables = get_str_map(body, "variables");
|
||||
let variables = strip_json_comments(&get_str_map(body, "variables"));
|
||||
|
||||
if method.to_lowercase() == "get" {
|
||||
// GraphQL GET requests use query parameters, not a body
|
||||
@@ -684,7 +745,7 @@ mod tests {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!("Hello, World!"));
|
||||
|
||||
let result = build_text_body(&body);
|
||||
let result = build_text_body(&body, "application/json");
|
||||
match result {
|
||||
Some(SendableBodyWithMeta::Bytes(bytes)) => {
|
||||
assert_eq!(bytes, Bytes::from("Hello, World!"))
|
||||
@@ -698,7 +759,7 @@ mod tests {
|
||||
let mut body = BTreeMap::new();
|
||||
body.insert("text".to_string(), json!(""));
|
||||
|
||||
let result = build_text_body(&body);
|
||||
let result = build_text_body(&body, "application/json");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
@@ -706,10 +767,57 @@ mod tests {
|
||||
async fn test_text_body_missing() {
|
||||
let body = BTreeMap::new();
|
||||
|
||||
let result = build_text_body(&body);
|
||||
let result = build_text_body(&body, "application/json");
|
||||
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]
|
||||
async fn test_form_urlencoded_body() -> Result<()> {
|
||||
let mut body = BTreeMap::new();
|
||||
|
||||
2
crates/yaak-models/bindings/gen_models.ts
generated
2
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.
|
||||
* 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, } | { "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, 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 HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
|
||||
@@ -1499,6 +1499,10 @@ pub enum HttpResponseEventData {
|
||||
url: String,
|
||||
status: u16,
|
||||
behavior: String,
|
||||
#[serde(default)]
|
||||
dropped_body: bool,
|
||||
#[serde(default)]
|
||||
dropped_headers: Vec<String>,
|
||||
},
|
||||
SendUrl {
|
||||
method: String,
|
||||
|
||||
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 CallHttpAuthenticationResponse = {
|
||||
export type CallHttpAuthenticationResponse = {
|
||||
/**
|
||||
* HTTP headers to add to the request. Existing headers will be replaced, while
|
||||
* new headers will be added.
|
||||
*/
|
||||
setHeaders?: Array<HttpHeader>,
|
||||
setHeaders?: Array<HttpHeader>,
|
||||
/**
|
||||
* Query parameters to add to the request. Existing params will be replaced, while
|
||||
* new params will be added.
|
||||
@@ -78,7 +78,7 @@ export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
|
||||
|
||||
export type ExportHttpRequestResponse = { content: string, };
|
||||
|
||||
export type FileFilter = { name: string,
|
||||
export type FileFilter = { name: string,
|
||||
/**
|
||||
* 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 FormInputBase = {
|
||||
export type FormInputBase = {
|
||||
/**
|
||||
* 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
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputEditor = {
|
||||
export type FormInputEditor = {
|
||||
/**
|
||||
* Placeholder for the text input
|
||||
*/
|
||||
placeholder?: string | null,
|
||||
placeholder?: string | null,
|
||||
/**
|
||||
* Don't show the editor gutter (line numbers, folds, etc.)
|
||||
*/
|
||||
hideGutter?: boolean,
|
||||
hideGutter?: boolean,
|
||||
/**
|
||||
* Language for syntax highlighting
|
||||
*/
|
||||
language?: EditorLanguage, readOnly?: boolean,
|
||||
language?: EditorLanguage, readOnly?: boolean,
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputFile = {
|
||||
export type FormInputFile = {
|
||||
/**
|
||||
* The title of the file selection window
|
||||
*/
|
||||
title: string,
|
||||
title: string,
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* 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 FormInputHttpRequest = {
|
||||
export type FormInputHttpRequest = {
|
||||
/**
|
||||
* 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
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* 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 FormInputSelect = {
|
||||
export type FormInputSelect = {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* 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 FormInputText = {
|
||||
export type FormInputText = {
|
||||
/**
|
||||
* Placeholder for the text input
|
||||
*/
|
||||
placeholder?: string | null,
|
||||
placeholder?: string | null,
|
||||
/**
|
||||
* Placeholder for the text input
|
||||
*/
|
||||
password?: boolean,
|
||||
password?: boolean,
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* 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 OpenWindowRequest = { url: string,
|
||||
export type OpenWindowRequest = { url: string,
|
||||
/**
|
||||
* 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 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
|
||||
*/
|
||||
confirmText?: string, password?: boolean,
|
||||
confirmText?: string, password?: boolean,
|
||||
/**
|
||||
* Text to add to the cancel button
|
||||
*/
|
||||
cancelText?: string,
|
||||
cancelText?: string,
|
||||
/**
|
||||
* 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 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
|
||||
* 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).
|
||||
*/
|
||||
@@ -546,23 +546,23 @@ export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, }
|
||||
|
||||
export type TemplateRenderResponse = { data: JsonValue, };
|
||||
|
||||
export type Theme = {
|
||||
export type Theme = {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
label: string,
|
||||
label: string,
|
||||
/**
|
||||
* Whether the theme will be used for dark or light appearance
|
||||
*/
|
||||
dark: boolean,
|
||||
dark: boolean,
|
||||
/**
|
||||
* The default top-level colors for the theme
|
||||
*/
|
||||
base: ThemeComponentColors,
|
||||
base: ThemeComponentColors,
|
||||
/**
|
||||
* 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,7 +18,12 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
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, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||
/**
|
||||
* 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, };
|
||||
|
||||
@@ -34,9 +39,17 @@ 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 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, };
|
||||
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,
|
||||
/**
|
||||
* 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, urlParameters: Array<HttpUrlParameter>, };
|
||||
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,
|
||||
/**
|
||||
* 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, };
|
||||
|
||||
@@ -49,17 +62,24 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||
* 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, } | { "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, 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 HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* 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 Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
|
||||
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 PluginSource = "bundled" | "filesystem" | "registry";
|
||||
|
||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||
|
||||
@@ -77,7 +97,11 @@ export type WebsocketEvent = { model: "websocket_event", id: string, createdAt:
|
||||
|
||||
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, urlParameters: Array<HttpUrlParameter>, };
|
||||
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,
|
||||
/**
|
||||
* 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>, };
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
let mut new_json = "".to_string();
|
||||
let mut depth = 0;
|
||||
let mut state = FormatState::None;
|
||||
let mut saw_newline_in_whitespace = false;
|
||||
|
||||
loop {
|
||||
let rest_of_chars = chars.clone();
|
||||
@@ -61,6 +62,62 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
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 {
|
||||
',' => {
|
||||
new_json.push(current_char);
|
||||
@@ -125,20 +182,37 @@ pub fn format_json(text: &str, tab: &str) -> String {
|
||||
|| current_char == '\t'
|
||||
|| current_char == '\r'
|
||||
{
|
||||
if current_char == '\n' {
|
||||
saw_newline_in_whitespace = true;
|
||||
}
|
||||
// Don't add these
|
||||
} else {
|
||||
saw_newline_in_whitespace = false;
|
||||
new_json.push(current_char);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace only lines containing whitespace with nothing
|
||||
new_json
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty()) // Filter out whitespace-only lines
|
||||
.collect::<Vec<&str>>() // Collect the non-empty lines into a vector
|
||||
.join("\n") // Join the lines back into a single string
|
||||
// Filter out whitespace-only lines, but preserve empty lines inside block comments
|
||||
let mut result_lines: Vec<&str> = Vec::new();
|
||||
let mut in_block_comment = false;
|
||||
for line in new_json.lines() {
|
||||
if in_block_comment {
|
||||
result_lines.push(line);
|
||||
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)]
|
||||
@@ -297,6 +371,161 @@ mod tests {
|
||||
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()
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod error;
|
||||
pub mod escape;
|
||||
pub mod format_json;
|
||||
pub mod strip_json_comments;
|
||||
pub mod parser;
|
||||
pub mod renderer;
|
||||
pub mod wasm;
|
||||
|
||||
318
crates/yaak-templates/src/strip_json_comments.rs
Normal file
318
crates/yaak-templates/src/strip_json_comments.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use log::info;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use yaak_http::path_placeholders::apply_path_placeholders;
|
||||
use yaak_models::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
||||
use yaak_models::models::{Environment, GrpcRequest, HttpRequest, HttpRequestHeader, HttpUrlParameter};
|
||||
use yaak_models::render::make_vars_hashmap;
|
||||
use yaak_templates::{RenderOptions, TemplateCallback, parse_and_render, render_json_value_raw};
|
||||
|
||||
@@ -89,6 +89,64 @@ pub async fn render_http_request<T: TemplateCallback>(
|
||||
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 {
|
||||
match v {
|
||||
Value::Array(items) => Value::Array(
|
||||
|
||||
339
package-lock.json
generated
339
package-lock.json
generated
@@ -1480,9 +1480,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
||||
"version": "1.19.10",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.10.tgz",
|
||||
"integrity": "sha512-hZ7nOssGqRgyV3FVVQdfi+U4q02uB23bpnYpdvNXkYTRRyWx84b7yf1ans+dnJ/7h41sGL3CeQTfO+ZGxuO+Iw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
@@ -2584,6 +2584,16 @@
|
||||
"ebnf": "^1.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@shopify/lang-jsonc": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/lang-jsonc/-/lang-jsonc-1.0.1.tgz",
|
||||
"integrity": "sha512-KrBrRFhvr1qJiZBODAtqbL1u1e67UR3plBN79Z8nd5TQAAzpx66jS4zs7Ss9M22ygGrpWFhyhSoNVlp5VCYktQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lezer/highlight": "^1.0.0",
|
||||
"@lezer/lr": "^1.3.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
|
||||
@@ -5330,105 +5340,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clone-regexp": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-3.0.0.tgz",
|
||||
@@ -6047,15 +5958,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decode-named-character-reference": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
|
||||
@@ -7200,19 +7102,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/focus-trap": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.7.1.tgz",
|
||||
@@ -7305,30 +7194,6 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/format-graphql": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/format-graphql/-/format-graphql-1.5.0.tgz",
|
||||
"integrity": "sha512-ZZWcjwJ1IMdnW9l3CeYccC/J7skqOB18tY3autO5OUQuGVZpQu6Es3SThRm25SfiMeZO1+UbzIqnGbjAURu/UA==",
|
||||
"dependencies": {
|
||||
"graphql": "^15.1.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"format-graphql": "dist/bin/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/format-graphql/node_modules/graphql": {
|
||||
"version": "15.10.1",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz",
|
||||
"integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -8008,9 +7873,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.11.10",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.10.tgz",
|
||||
"integrity": "sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==",
|
||||
"version": "4.12.4",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.4.tgz",
|
||||
"integrity": "sha512-ooiZW1Xy8rQ4oELQ++otI2T9DsKpV0M6c6cO6JGx4RTfav9poFFLlet9UMXHZnoM1yG0HWGlQLswBGX3RZmHtg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -9271,18 +9136,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
@@ -11605,33 +11458,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-map": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
|
||||
@@ -11658,15 +11484,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -11760,15 +11577,6 @@
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -12883,12 +12691,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
@@ -13356,12 +13158,6 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -15601,12 +15397,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/which-typed-array": {
|
||||
"version": "1.1.19",
|
||||
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
|
||||
@@ -15822,12 +15612,6 @@
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
@@ -15850,91 +15634,6 @@
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser/node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
@@ -16064,9 +15763,9 @@
|
||||
"version": "0.2.1",
|
||||
"dependencies": {
|
||||
"@hono/mcp": "^0.2.3",
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"@hono/node-server": "^1.19.10",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"hono": "^4.11.10",
|
||||
"hono": "^4.12.4",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -16323,6 +16022,7 @@
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"@shopify/lang-jsonc": "^1.0.1",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tanstack/react-router": "^1.133.13",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
@@ -16342,7 +16042,6 @@
|
||||
"deep-equal": "^2.2.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"focus-trap-react": "^11.0.4",
|
||||
"format-graphql": "^1.5.0",
|
||||
"fuzzbunny": "^1.0.1",
|
||||
"hexy": "^0.3.5",
|
||||
"history": "^5.3.0",
|
||||
|
||||
160
packages/plugin-runtime-types/src/bindings/gen_events.ts
generated
160
packages/plugin-runtime-types/src/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 CallHttpAuthenticationResponse = {
|
||||
export type CallHttpAuthenticationResponse = {
|
||||
/**
|
||||
* HTTP headers to add to the request. Existing headers will be replaced, while
|
||||
* new headers will be added.
|
||||
*/
|
||||
setHeaders?: Array<HttpHeader>,
|
||||
setHeaders?: Array<HttpHeader>,
|
||||
/**
|
||||
* Query parameters to add to the request. Existing params will be replaced, while
|
||||
* new params will be added.
|
||||
@@ -78,7 +78,7 @@ export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
|
||||
|
||||
export type ExportHttpRequestResponse = { content: string, };
|
||||
|
||||
export type FileFilter = { name: string,
|
||||
export type FileFilter = { name: string,
|
||||
/**
|
||||
* 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 FormInputBase = {
|
||||
export type FormInputBase = {
|
||||
/**
|
||||
* 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
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputEditor = {
|
||||
export type FormInputEditor = {
|
||||
/**
|
||||
* Placeholder for the text input
|
||||
*/
|
||||
placeholder?: string | null,
|
||||
placeholder?: string | null,
|
||||
/**
|
||||
* Don't show the editor gutter (line numbers, folds, etc.)
|
||||
*/
|
||||
hideGutter?: boolean,
|
||||
hideGutter?: boolean,
|
||||
/**
|
||||
* Language for syntax highlighting
|
||||
*/
|
||||
language?: EditorLanguage, readOnly?: boolean,
|
||||
language?: EditorLanguage, readOnly?: boolean,
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
description?: string, };
|
||||
|
||||
export type FormInputFile = {
|
||||
export type FormInputFile = {
|
||||
/**
|
||||
* The title of the file selection window
|
||||
*/
|
||||
title: string,
|
||||
title: string,
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* 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 FormInputHttpRequest = {
|
||||
export type FormInputHttpRequest = {
|
||||
/**
|
||||
* 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
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* Longer description of the input, likely shown in a tooltip
|
||||
*/
|
||||
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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* 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 FormInputSelect = {
|
||||
export type FormInputSelect = {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* 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 FormInputText = {
|
||||
export type FormInputText = {
|
||||
/**
|
||||
* Placeholder for the text input
|
||||
*/
|
||||
placeholder?: string | null,
|
||||
placeholder?: string | null,
|
||||
/**
|
||||
* Placeholder for the text input
|
||||
*/
|
||||
password?: boolean,
|
||||
password?: boolean,
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
name: string,
|
||||
name: string,
|
||||
/**
|
||||
* Whether this input is visible for the given configuration. Use this to
|
||||
* make branching forms.
|
||||
*/
|
||||
hidden?: boolean,
|
||||
hidden?: boolean,
|
||||
/**
|
||||
* Whether the user must fill in the argument
|
||||
*/
|
||||
optional?: boolean,
|
||||
optional?: boolean,
|
||||
/**
|
||||
* The label of the input
|
||||
*/
|
||||
label?: string,
|
||||
label?: string,
|
||||
/**
|
||||
* Visually hide the label of the input
|
||||
*/
|
||||
hideLabel?: boolean,
|
||||
hideLabel?: boolean,
|
||||
/**
|
||||
* The default value
|
||||
*/
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
defaultValue?: string, disabled?: boolean,
|
||||
/**
|
||||
* 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 OpenWindowRequest = { url: string,
|
||||
export type OpenWindowRequest = { url: string,
|
||||
/**
|
||||
* 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 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
|
||||
*/
|
||||
confirmText?: string, password?: boolean,
|
||||
confirmText?: string, password?: boolean,
|
||||
/**
|
||||
* Text to add to the cancel button
|
||||
*/
|
||||
cancelText?: string,
|
||||
cancelText?: string,
|
||||
/**
|
||||
* 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 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
|
||||
* 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).
|
||||
*/
|
||||
@@ -546,23 +546,23 @@ export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, }
|
||||
|
||||
export type TemplateRenderResponse = { data: JsonValue, };
|
||||
|
||||
export type Theme = {
|
||||
export type Theme = {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
label: string,
|
||||
label: string,
|
||||
/**
|
||||
* Whether the theme will be used for dark or light appearance
|
||||
*/
|
||||
dark: boolean,
|
||||
dark: boolean,
|
||||
/**
|
||||
* The default top-level colors for the theme
|
||||
*/
|
||||
base: ThemeComponentColors,
|
||||
base: ThemeComponentColors,
|
||||
/**
|
||||
* Optionally override theme for individual UI components for more control
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,12 @@ export type EditorKeymap = "default" | "vim" | "vscode" | "emacs";
|
||||
|
||||
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, variables: Array<EnvironmentVariable>, color: string | null, sortPriority: number, };
|
||||
export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null,
|
||||
/**
|
||||
* 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, };
|
||||
|
||||
@@ -34,9 +39,17 @@ 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 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, };
|
||||
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,
|
||||
/**
|
||||
* 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, urlParameters: Array<HttpUrlParameter>, };
|
||||
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,
|
||||
/**
|
||||
* 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, };
|
||||
|
||||
@@ -49,17 +62,24 @@ export type HttpResponseEvent = { model: "http_response_event", id: string, crea
|
||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||
* 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, } | { "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, 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 HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
export type HttpResponseState = "initialized" | "connected" | "closed";
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* 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 Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, };
|
||||
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 PluginSource = "bundled" | "filesystem" | "registry";
|
||||
|
||||
export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" };
|
||||
|
||||
@@ -77,7 +97,11 @@ export type WebsocketEvent = { model: "websocket_event", id: string, createdAt:
|
||||
|
||||
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, urlParameters: Array<HttpUrlParameter>, };
|
||||
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,
|
||||
/**
|
||||
* 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>, };
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/mcp": "^0.2.3",
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"@hono/node-server": "^1.19.10",
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"hono": "^4.11.10",
|
||||
"hono": "^4.12.4",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import http from 'node:http';
|
||||
import type { Context } from '@yaakapp/api';
|
||||
|
||||
export const HOSTED_CALLBACK_URL = 'https://oauth.yaak.app/redirect';
|
||||
export const HOSTED_CALLBACK_URL_BASE = 'https://oauth.yaak.app/redirect';
|
||||
export const DEFAULT_LOCALHOST_PORT = 8765;
|
||||
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
@@ -176,12 +176,15 @@ export function startCallbackServer(options: {
|
||||
|
||||
/**
|
||||
* Build the redirect URI for the hosted callback page.
|
||||
* The hosted page will redirect to the local server with the OAuth response.
|
||||
* The port is encoded in the URL path so the hosted page can redirect
|
||||
* to the local server without relying on query params (which some OAuth
|
||||
* providers strip). The default port is omitted for a cleaner URL.
|
||||
*/
|
||||
export function buildHostedCallbackRedirectUri(localPort: number, localPath: string): string {
|
||||
const localRedirectUri = `http://127.0.0.1:${localPort}${localPath}`;
|
||||
// The hosted callback page will read params and redirect to the local server
|
||||
return `${HOSTED_CALLBACK_URL}?redirect_to=${encodeURIComponent(localRedirectUri)}`;
|
||||
export function buildHostedCallbackRedirectUri(localPort: number): string {
|
||||
if (localPort === DEFAULT_LOCALHOST_PORT) {
|
||||
return HOSTED_CALLBACK_URL_BASE;
|
||||
}
|
||||
return `${HOSTED_CALLBACK_URL_BASE}/${localPort}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,14 +216,9 @@ export async function getRedirectUrlViaExternalBrowser(
|
||||
): Promise<{ callbackUrl: string; redirectUri: string }> {
|
||||
const { callbackType, callbackPort } = options;
|
||||
|
||||
// Determine port based on callback type:
|
||||
// - localhost: use specified port or default stable port
|
||||
// - hosted: use random port (0) since hosted page redirects to local
|
||||
const port = callbackType === 'localhost' ? (callbackPort ?? DEFAULT_LOCALHOST_PORT) : 0;
|
||||
const port = callbackPort ?? DEFAULT_LOCALHOST_PORT;
|
||||
|
||||
console.log(
|
||||
`[oauth2] Starting callback server (type: ${callbackType}, port: ${port || 'random'})`,
|
||||
);
|
||||
console.log(`[oauth2] Starting callback server (type: ${callbackType}, port: ${port})`);
|
||||
|
||||
const server = await startCallbackServer({
|
||||
port,
|
||||
@@ -232,7 +230,7 @@ export async function getRedirectUrlViaExternalBrowser(
|
||||
let oauthRedirectUri: string;
|
||||
|
||||
if (callbackType === 'hosted') {
|
||||
oauthRedirectUri = buildHostedCallbackRedirectUri(server.port, '/callback');
|
||||
oauthRedirectUri = buildHostedCallbackRedirectUri(server.port);
|
||||
console.log('[oauth2] Using hosted callback redirect:', oauthRedirectUri);
|
||||
} else {
|
||||
oauthRedirectUri = server.redirectUri;
|
||||
|
||||
@@ -6,7 +6,11 @@ import type {
|
||||
PluginDefinition,
|
||||
} from '@yaakapp/api';
|
||||
import type { Algorithm } from 'jsonwebtoken';
|
||||
import { DEFAULT_LOCALHOST_PORT, HOSTED_CALLBACK_URL, stopActiveServer } from './callbackServer';
|
||||
import {
|
||||
buildHostedCallbackRedirectUri,
|
||||
DEFAULT_LOCALHOST_PORT,
|
||||
stopActiveServer,
|
||||
} from './callbackServer';
|
||||
import {
|
||||
type CallbackType,
|
||||
DEFAULT_PKCE_METHOD,
|
||||
@@ -300,8 +304,7 @@ export const plugin: PluginDefinition = {
|
||||
optional: true,
|
||||
dynamic: hiddenIfNot(
|
||||
['authorization_code', 'implicit'],
|
||||
({ useExternalBrowser, callbackType }) =>
|
||||
!!useExternalBrowser && callbackType === 'localhost',
|
||||
({ useExternalBrowser }) => !!useExternalBrowser,
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -328,11 +331,11 @@ export const plugin: PluginDefinition = {
|
||||
}
|
||||
|
||||
// Compute the redirect URI based on callback type
|
||||
const port = intArg(values, 'callbackPort') || DEFAULT_LOCALHOST_PORT;
|
||||
let redirectUri: string;
|
||||
if (callbackType === 'hosted') {
|
||||
redirectUri = HOSTED_CALLBACK_URL;
|
||||
redirectUri = buildHostedCallbackRedirectUri(port);
|
||||
} else {
|
||||
const port = intArg(values, 'callbackPort') || DEFAULT_LOCALHOST_PORT;
|
||||
redirectUri = `http://127.0.0.1:${port}/callback`;
|
||||
}
|
||||
|
||||
|
||||
@@ -317,7 +317,8 @@ async function getResponse(
|
||||
finalBehavior === 'always' ||
|
||||
(finalBehavior === BEHAVIOR_TTL && shouldSendExpired(response, ttl))
|
||||
) {
|
||||
// NOTE: Render inside this conditional, or we'll get infinite recursion (render->render->...)
|
||||
// Explicitly render the request before send (instead of relying on send() to render) so that we can
|
||||
// preserve the render purpose.
|
||||
const renderedHttpRequest = await ctx.httpRequest.render({ httpRequest, purpose });
|
||||
response = await ctx.httpRequest.send({ httpRequest: renderedHttpRequest });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { jsonLanguage } from '@codemirror/lang-json';
|
||||
import { jsoncLanguage } from '@shopify/lang-jsonc';
|
||||
import { linter } from '@codemirror/lint';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { GrpcRequest } from '@yaakapp-internal/models';
|
||||
@@ -115,7 +115,7 @@ export function GrpcEditor({
|
||||
delay: 200,
|
||||
needsRefresh: handleRefresh,
|
||||
}),
|
||||
jsonLanguage.data.of({
|
||||
jsoncLanguage.data.of({
|
||||
autocomplete: jsonCompletion(),
|
||||
}),
|
||||
stateExtensions({}),
|
||||
|
||||
@@ -40,7 +40,7 @@ export function HeaderSize({
|
||||
} else if (type() === 'macos') {
|
||||
if (!isFullscreen) {
|
||||
// Add large padding for window controls
|
||||
s.paddingLeft = 72 / settings.interfaceScale;
|
||||
s.paddingLeft = 76 / settings.interfaceScale;
|
||||
}
|
||||
} else if (!ignoreControlsSpacing && !settings.hideWindowControls) {
|
||||
s.paddingRight = WINDOW_CONTROLS_WIDTH;
|
||||
|
||||
@@ -48,6 +48,7 @@ import { FormMultipartEditor } from './FormMultipartEditor';
|
||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { JsonBodyEditor } from './JsonBodyEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { RequestMethodDropdown } from './RequestMethodDropdown';
|
||||
import { UrlBar } from './UrlBar';
|
||||
@@ -257,7 +258,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
);
|
||||
|
||||
const handleBodyTextChange = useCallback(
|
||||
(text: string) => patchModel(activeRequest, { body: { text } }),
|
||||
(text: string) => patchModel(activeRequest, { body: { ...activeRequest.body, text } }),
|
||||
[activeRequest],
|
||||
);
|
||||
|
||||
@@ -370,16 +371,10 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
<TabContent value={TAB_BODY}>
|
||||
<ConfirmLargeRequestBody request={activeRequest}>
|
||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||
<Editor
|
||||
<JsonBodyEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
language="json"
|
||||
onChange={handleBodyTextChange}
|
||||
stateKey={`json.${activeRequest.id}`}
|
||||
request={activeRequest}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||
<Editor
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import type { HttpResponse, HttpResponseEvent } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import type { ComponentType, CSSProperties } from 'react';
|
||||
import { lazy, Suspense, useMemo } from 'react';
|
||||
@@ -18,11 +18,14 @@ import { CountBadge } from './core/CountBadge';
|
||||
import { HotkeyList } from './core/HotkeyList';
|
||||
import { HttpResponseDurationTag } from './core/HttpResponseDurationTag';
|
||||
import { HttpStatusTag } from './core/HttpStatusTag';
|
||||
import { Icon } from './core/Icon';
|
||||
import { LoadingIcon } from './core/LoadingIcon';
|
||||
import { PillButton } from './core/PillButton';
|
||||
import { SizeTag } from './core/SizeTag';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { Tooltip } from './core/Tooltip';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { ErrorBoundary } from './ErrorBoundary';
|
||||
import { HttpResponseTimeline } from './HttpResponseTimeline';
|
||||
@@ -57,6 +60,11 @@ const TAB_TIMELINE = 'timeline';
|
||||
|
||||
export type TimelineViewMode = 'timeline' | 'text';
|
||||
|
||||
interface RedirectDropWarning {
|
||||
droppedBodyCount: number;
|
||||
droppedHeaders: string[];
|
||||
}
|
||||
|
||||
export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequestId);
|
||||
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
||||
@@ -65,6 +73,12 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
const mimeType = contentType == null ? null : getMimeTypeFromContentType(contentType).essence;
|
||||
|
||||
const responseEvents = useHttpResponseEvents(activeResponse);
|
||||
const redirectDropWarning = useMemo(
|
||||
() => getRedirectDropWarning(responseEvents.data),
|
||||
[responseEvents.data],
|
||||
);
|
||||
const shouldShowRedirectDropWarning =
|
||||
activeResponse?.state === 'closed' && redirectDropWarning != null;
|
||||
|
||||
const cookieCounts = useMemo(() => getCookieCounts(responseEvents.data), [responseEvents.data]);
|
||||
|
||||
@@ -162,32 +176,77 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
)}
|
||||
>
|
||||
{activeResponse && (
|
||||
<HStack
|
||||
space={2}
|
||||
alignItems="center"
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-cols-[auto_minmax(4rem,1fr)_auto]',
|
||||
'cursor-default select-none',
|
||||
'whitespace-nowrap w-full pl-3 overflow-x-auto font-mono text-sm hide-scrollbars',
|
||||
)}
|
||||
>
|
||||
{activeResponse.state !== 'closed' && <LoadingIcon size="sm" />}
|
||||
<HttpStatusTag showReason response={activeResponse} />
|
||||
<span>•</span>
|
||||
<HttpResponseDurationTag response={activeResponse} />
|
||||
<span>•</span>
|
||||
<SizeTag
|
||||
contentLength={activeResponse.contentLength ?? 0}
|
||||
contentLengthCompressed={activeResponse.contentLengthCompressed}
|
||||
/>
|
||||
|
||||
<div className="ml-auto">
|
||||
<HStack space={2} className="w-full flex-shrink-0">
|
||||
{activeResponse.state !== 'closed' && <LoadingIcon size="sm" />}
|
||||
<HttpStatusTag showReason response={activeResponse} />
|
||||
<span>•</span>
|
||||
<HttpResponseDurationTag response={activeResponse} />
|
||||
<span>•</span>
|
||||
<SizeTag
|
||||
contentLength={activeResponse.contentLength ?? 0}
|
||||
contentLengthCompressed={activeResponse.contentLengthCompressed}
|
||||
/>
|
||||
</HStack>
|
||||
{shouldShowRedirectDropWarning ? (
|
||||
<Tooltip
|
||||
tabIndex={0}
|
||||
className="my-auto pl-3 flex-shrink-0 max-w-full justify-self-end overflow-hidden"
|
||||
content={
|
||||
<VStack alignItems="start" space={1} className="text-xs">
|
||||
<span className="font-medium text-warning">
|
||||
Redirect changed this request
|
||||
</span>
|
||||
{redirectDropWarning.droppedBodyCount > 0 && (
|
||||
<span>
|
||||
Body dropped on {redirectDropWarning.droppedBodyCount}{' '}
|
||||
{redirectDropWarning.droppedBodyCount === 1
|
||||
? 'redirect hop'
|
||||
: 'redirect hops'}
|
||||
</span>
|
||||
)}
|
||||
{redirectDropWarning.droppedHeaders.length > 0 && (
|
||||
<span>
|
||||
Headers dropped:{' '}
|
||||
<span className="font-mono">
|
||||
{redirectDropWarning.droppedHeaders.join(', ')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
<span className="text-text-subtle">See Timeline for details.</span>
|
||||
</VStack>
|
||||
}
|
||||
>
|
||||
<span className="inline-flex min-w-0">
|
||||
<PillButton
|
||||
color="warning"
|
||||
className="font-sans text-sm !flex-shrink max-w-full"
|
||||
innerClassName="flex items-center"
|
||||
leftSlot={<Icon icon="alert_triangle" size="xs" color="warning" />}
|
||||
>
|
||||
<span className="truncate">
|
||||
{getRedirectWarningLabel(redirectDropWarning)}
|
||||
</span>
|
||||
</PillButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<div className="justify-self-end flex-shrink-0">
|
||||
<RecentHttpResponsesDropdown
|
||||
responses={responses}
|
||||
activeResponse={activeResponse}
|
||||
onPinnedResponseId={setPinnedResponseId}
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
@@ -274,6 +333,54 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function getRedirectDropWarning(
|
||||
events: HttpResponseEvent[] | undefined,
|
||||
): RedirectDropWarning | null {
|
||||
if (events == null || events.length === 0) return null;
|
||||
|
||||
let droppedBodyCount = 0;
|
||||
const droppedHeaders = new Set<string>();
|
||||
for (const e of events) {
|
||||
const event = e.event;
|
||||
if (event.type !== 'redirect') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (event.dropped_body) {
|
||||
droppedBodyCount += 1;
|
||||
}
|
||||
for (const headerName of event.dropped_headers ?? []) {
|
||||
pushHeaderName(droppedHeaders, headerName);
|
||||
}
|
||||
}
|
||||
|
||||
if (droppedBodyCount === 0 && droppedHeaders.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
droppedBodyCount,
|
||||
droppedHeaders: Array.from(droppedHeaders).sort(),
|
||||
};
|
||||
}
|
||||
|
||||
function pushHeaderName(headers: Set<string>, headerName: string): void {
|
||||
const existing = Array.from(headers).find((h) => h.toLowerCase() === headerName.toLowerCase());
|
||||
if (existing == null) {
|
||||
headers.add(headerName);
|
||||
}
|
||||
}
|
||||
|
||||
function getRedirectWarningLabel(warning: RedirectDropWarning): string {
|
||||
if (warning.droppedBodyCount > 0 && warning.droppedHeaders.length > 0) {
|
||||
return 'Dropped body and headers';
|
||||
}
|
||||
if (warning.droppedBodyCount > 0) {
|
||||
return 'Dropped body';
|
||||
}
|
||||
return 'Dropped headers';
|
||||
}
|
||||
|
||||
function EnsureCompleteResponse({
|
||||
response,
|
||||
Component,
|
||||
|
||||
@@ -187,6 +187,7 @@ function EventDetails({
|
||||
|
||||
// Redirect - show status, URL, and behavior
|
||||
if (e.type === 'redirect') {
|
||||
const droppedHeaders = e.dropped_headers ?? [];
|
||||
return (
|
||||
<KeyValueRows>
|
||||
<KeyValueRow label="Status">
|
||||
@@ -196,6 +197,10 @@ function EventDetails({
|
||||
<KeyValueRow label="Behavior">
|
||||
{e.behavior === 'drop_body' ? 'Drop body, change to GET' : 'Preserve method and body'}
|
||||
</KeyValueRow>
|
||||
<KeyValueRow label="Body Dropped">{e.dropped_body ? 'Yes' : 'No'}</KeyValueRow>
|
||||
<KeyValueRow label="Headers Dropped">
|
||||
{droppedHeaders.length > 0 ? droppedHeaders.join(', ') : '--'}
|
||||
</KeyValueRow>
|
||||
</KeyValueRows>
|
||||
);
|
||||
}
|
||||
@@ -268,7 +273,17 @@ function getEventTextParts(event: HttpResponseEventData): EventTextParts {
|
||||
return { prefix: '<', text: `${event.name}: ${event.value}` };
|
||||
case 'redirect': {
|
||||
const behavior = event.behavior === 'drop_body' ? 'drop body' : 'preserve';
|
||||
return { prefix: '*', text: `Redirect ${event.status} -> ${event.url} (${behavior})` };
|
||||
const droppedHeaders = event.dropped_headers ?? [];
|
||||
const dropped = [
|
||||
event.dropped_body ? 'body dropped' : null,
|
||||
droppedHeaders.length > 0 ? `headers dropped: ${droppedHeaders.join(', ')}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
return {
|
||||
prefix: '*',
|
||||
text: `Redirect ${event.status} -> ${event.url} (${behavior}${dropped ? `, ${dropped}` : ''})`,
|
||||
};
|
||||
}
|
||||
case 'setting':
|
||||
return { prefix: '*', text: `Setting ${event.name}=${event.value}` };
|
||||
@@ -323,13 +338,23 @@ function getEventDisplay(event: HttpResponseEventData): EventDisplay {
|
||||
label: 'Info',
|
||||
summary: event.message,
|
||||
};
|
||||
case 'redirect':
|
||||
case 'redirect': {
|
||||
const droppedHeaders = event.dropped_headers ?? [];
|
||||
const dropped = [
|
||||
event.dropped_body ? 'drop body' : null,
|
||||
droppedHeaders.length > 0
|
||||
? `drop ${droppedHeaders.length} ${droppedHeaders.length === 1 ? 'header' : 'headers'}`
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
return {
|
||||
icon: 'arrow_big_right_dash',
|
||||
color: 'success',
|
||||
label: 'Redirect',
|
||||
summary: `Redirecting ${event.status} ${event.url}${event.behavior === 'drop_body' ? ' (drop body)' : ''}`,
|
||||
summary: `Redirecting ${event.status} ${event.url}${dropped ? ` (${dropped})` : ''}`,
|
||||
};
|
||||
}
|
||||
case 'send_url':
|
||||
return {
|
||||
icon: 'arrow_big_up_dash',
|
||||
|
||||
122
src-web/components/JsonBodyEditor.tsx
Normal file
122
src-web/components/JsonBodyEditor.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { linter } from '@codemirror/lint';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { patchModel } from '@yaakapp-internal/models';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { textLikelyContainsJsonComments } from '../lib/jsonComments';
|
||||
import { Banner } from './core/Banner';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import type { EditorProps } from './core/Editor/Editor';
|
||||
import { jsonParseLinter } from './core/Editor/json-lint';
|
||||
import { Editor } from './core/Editor/LazyEditor';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { IconTooltip } from './core/IconTooltip';
|
||||
|
||||
interface Props {
|
||||
forceUpdateKey: string;
|
||||
heightMode: EditorProps['heightMode'];
|
||||
request: HttpRequest;
|
||||
}
|
||||
|
||||
export function JsonBodyEditor({ forceUpdateKey, heightMode, request }: Props) {
|
||||
const handleChange = useCallback(
|
||||
(text: string) => patchModel(request, { body: { ...request.body, text } }),
|
||||
[request],
|
||||
);
|
||||
|
||||
const autoFix = request.body?.sendJsonComments !== true;
|
||||
|
||||
const lintExtension = useMemo(
|
||||
() =>
|
||||
linter(
|
||||
jsonParseLinter(
|
||||
autoFix
|
||||
? { allowComments: true, allowTrailingCommas: true }
|
||||
: { allowComments: false, allowTrailingCommas: false },
|
||||
),
|
||||
),
|
||||
[autoFix],
|
||||
);
|
||||
|
||||
const hasComments = useMemo(
|
||||
() => textLikelyContainsJsonComments(request.body?.text ?? ''),
|
||||
[request.body?.text],
|
||||
);
|
||||
|
||||
const { value: bannerDismissed, set: setBannerDismissed } = useKeyValue<boolean>({
|
||||
namespace: 'no_sync',
|
||||
key: ['json-fix-3', request.workspaceId],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
const handleToggleAutoFix = useCallback(() => {
|
||||
const newBody = { ...request.body };
|
||||
if (autoFix) {
|
||||
newBody.sendJsonComments = true;
|
||||
} else {
|
||||
delete newBody.sendJsonComments;
|
||||
}
|
||||
patchModel(request, { body: newBody });
|
||||
}, [request, autoFix]);
|
||||
|
||||
const handleDropdownOpen = useCallback(() => {
|
||||
if (!bannerDismissed) {
|
||||
setBannerDismissed(true);
|
||||
}
|
||||
}, [bannerDismissed, setBannerDismissed]);
|
||||
|
||||
const showBanner = hasComments && autoFix && !bannerDismissed;
|
||||
|
||||
const stripMessage = 'Automatically strip comments and trailing commas before sending';
|
||||
const actions = useMemo<EditorProps['actions']>(
|
||||
() => [
|
||||
showBanner && (
|
||||
<Banner color="notice" className="!opacity-100 h-sm !py-0 !px-2 flex items-center text-xs">
|
||||
<p className="inline-flex items-center gap-1 min-w-0">
|
||||
<span className="truncate">Auto-fix enabled</span>
|
||||
<Icon icon="arrow_right" size="sm" className="opacity-disabled" />
|
||||
</p>
|
||||
</Banner>
|
||||
),
|
||||
<div key="settings" className="!opacity-100 !shadow">
|
||||
<Dropdown
|
||||
onOpen={handleDropdownOpen}
|
||||
items={
|
||||
[
|
||||
{
|
||||
label: 'Automatically Fix JSON',
|
||||
keepOpenOnSelect: true,
|
||||
onSelect: handleToggleAutoFix,
|
||||
rightSlot: <IconTooltip content={stripMessage} />,
|
||||
leftSlot: (
|
||||
<Icon icon={autoFix ? 'check_square_checked' : 'check_square_unchecked'} />
|
||||
),
|
||||
},
|
||||
] satisfies DropdownItem[]
|
||||
}
|
||||
>
|
||||
<IconButton size="sm" variant="border" icon="settings" title="JSON Settings" />
|
||||
</Dropdown>
|
||||
</div>,
|
||||
],
|
||||
[handleDropdownOpen, handleToggleAutoFix, autoFix, showBanner],
|
||||
);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
heightMode={heightMode}
|
||||
defaultValue={`${request.body?.text ?? ''}`}
|
||||
language="json"
|
||||
onChange={handleChange}
|
||||
stateKey={`json.${request.id}`}
|
||||
actions={actions}
|
||||
lintExtension={lintExtension}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -39,9 +39,9 @@ const tabs = [
|
||||
TAB_THEME,
|
||||
TAB_INTERFACE,
|
||||
TAB_SHORTCUTS,
|
||||
TAB_PLUGINS,
|
||||
TAB_CERTIFICATES,
|
||||
TAB_PROXY,
|
||||
TAB_PLUGINS,
|
||||
TAB_LICENSE,
|
||||
] as const;
|
||||
export type SettingsTab = (typeof tabs)[number];
|
||||
@@ -120,7 +120,7 @@ export default function Settings({ hide }: Props) {
|
||||
value === TAB_CERTIFICATES ? (
|
||||
<CountBadge count={settings.clientCertificates.length} />
|
||||
) : value === TAB_PLUGINS ? (
|
||||
<CountBadge count={plugins.length} />
|
||||
<CountBadge count={plugins.filter((p) => p.source !== 'bundled').length} />
|
||||
) : value === TAB_PROXY && settings.proxy?.type === 'enabled' ? (
|
||||
<CountBadge count />
|
||||
) : value === TAB_LICENSE && licenseCheck.check.data?.status === 'personal_use' ? (
|
||||
@@ -141,7 +141,7 @@ export default function Settings({ hide }: Props) {
|
||||
<TabContent value={TAB_SHORTCUTS} className="overflow-y-auto h-full px-6 !py-4">
|
||||
<SettingsHotkeys />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 px-6 !py-4">
|
||||
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1">
|
||||
<SettingsPlugins defaultSubtab={mainTab === TAB_PLUGINS ? subtab : undefined} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4">
|
||||
|
||||
@@ -13,7 +13,6 @@ import { Link } from '../core/Link';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import { Separator } from '../core/Separator';
|
||||
import { HStack, VStack } from '../core/Stacks';
|
||||
import { LocalImage } from '../LocalImage';
|
||||
|
||||
export function SettingsLicense() {
|
||||
return (
|
||||
@@ -41,11 +40,7 @@ function SettingsLicenseCmp() {
|
||||
|
||||
case 'trialing':
|
||||
return (
|
||||
<Banner color="info" className="@container flex items-center gap-x-5 max-w-xl">
|
||||
<LocalImage
|
||||
src="static/greg.jpeg"
|
||||
className="hidden @sm:block rounded-full h-14 w-14"
|
||||
/>
|
||||
<Banner color="info" className="max-w-lg">
|
||||
<p className="w-full">
|
||||
<strong>
|
||||
{pluralizeCount('day', differenceInDays(check.data.data.end, new Date()))}
|
||||
@@ -55,10 +50,6 @@ function SettingsLicenseCmp() {
|
||||
<span className="opacity-50">Personal use is always free, forever.</span>
|
||||
<Separator className="my-2" />
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
||||
<Link noUnderline href="mailto:support@yaak.app">
|
||||
Contact Support
|
||||
</Link>
|
||||
<Icon icon="dot" size="sm" color="secondary" />
|
||||
<Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
@@ -69,24 +60,16 @@ function SettingsLicenseCmp() {
|
||||
|
||||
case 'personal_use':
|
||||
return (
|
||||
<Banner color="notice" className="@container flex items-center gap-x-5 max-w-xl">
|
||||
<LocalImage
|
||||
src="static/greg.jpeg"
|
||||
className="hidden @sm:block rounded-full h-14 w-14"
|
||||
/>
|
||||
<Banner color="notice" className="max-w-lg">
|
||||
<p className="w-full">
|
||||
Your commercial-use trial has ended.
|
||||
<br />
|
||||
<span className="opacity-50">
|
||||
You may continue using Yaak for personal use free, forever.
|
||||
You may continue using Yaak for personal use only.
|
||||
<br />A license is required for commercial use.
|
||||
</span>
|
||||
<Separator className="my-2" />
|
||||
<div className="flex flex-wrap items-center gap-x-2 text-sm text-notice">
|
||||
<Link noUnderline href="mailto:support@yaak.app">
|
||||
Contact Support
|
||||
</Link>
|
||||
<Icon icon="dot" size="sm" color="secondary" />
|
||||
<Link noUnderline href={`https://yaak.app/pricing?s=learn&t=${check.data.status}`}>
|
||||
Learn More
|
||||
</Link>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
searchPlugins,
|
||||
uninstallPlugin,
|
||||
} from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
|
||||
@@ -49,6 +50,7 @@ export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
|
||||
defaultValue={defaultSubtab}
|
||||
label="Plugins"
|
||||
addBorders
|
||||
tabListClassName="px-6 pt-2"
|
||||
tabs={[
|
||||
{ label: 'Discover', value: 'search' },
|
||||
{
|
||||
@@ -63,13 +65,13 @@ export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TabContent value="search">
|
||||
<TabContent value="search" className="px-6">
|
||||
<PluginSearch />
|
||||
</TabContent>
|
||||
<TabContent value="installed" className="pb-0">
|
||||
<div className="h-full grid grid-rows-[minmax(0,1fr)_auto]">
|
||||
<InstalledPlugins plugins={installedPlugins} />
|
||||
<footer className="grid grid-cols-[minmax(0,1fr)_auto] -mx-4 py-2 px-4 border-t bg-surface-highlight border-border-subtle min-w-0">
|
||||
<InstalledPlugins plugins={installedPlugins} className="px-6" />
|
||||
<footer className="grid grid-cols-[minmax(0,1fr)_auto] py-2 px-4 border-t bg-surface-highlight border-border-subtle min-w-0">
|
||||
<SelectFile
|
||||
size="xs"
|
||||
noun="Plugin"
|
||||
@@ -111,7 +113,7 @@ export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
|
||||
</footer>
|
||||
</div>
|
||||
</TabContent>
|
||||
<TabContent value="bundled" className="pb-0">
|
||||
<TabContent value="bundled" className="pb-0 px-6">
|
||||
<BundledPlugins plugins={bundledPlugins} />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
@@ -330,9 +332,9 @@ function PluginSearch() {
|
||||
);
|
||||
}
|
||||
|
||||
function InstalledPlugins({ plugins }: { plugins: Plugin[] }) {
|
||||
function InstalledPlugins({ plugins, className }: { plugins: Plugin[]; className?: string }) {
|
||||
return plugins.length === 0 ? (
|
||||
<div className="pb-4">
|
||||
<div className={classNames(className, 'pb-4')}>
|
||||
<EmptyStateText className="text-center">
|
||||
Plugins extend the functionality of Yaak.
|
||||
<br />
|
||||
@@ -340,7 +342,7 @@ function InstalledPlugins({ plugins }: { plugins: Plugin[] }) {
|
||||
</EmptyStateText>
|
||||
</div>
|
||||
) : (
|
||||
<Table scrollable>
|
||||
<Table scrollable className={className}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell className="w-0" />
|
||||
|
||||
@@ -64,19 +64,27 @@ export function SettingsDropdown() {
|
||||
onSelect: () => openUrl('https://yaak.app/button/new'),
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appInfo.version}` },
|
||||
{
|
||||
label: 'Purchase License',
|
||||
color: 'success',
|
||||
hidden: check.data == null || check.data.status === 'active',
|
||||
leftSlot: <Icon icon="circle_dollar_sign" />,
|
||||
onSelect: () => openSettings.mutate('license'),
|
||||
},
|
||||
{
|
||||
label: 'Check for Updates',
|
||||
leftSlot: <Icon icon="update" />,
|
||||
hidden: !appInfo.featureUpdater,
|
||||
onSelect: () => checkForUpdates.mutate(),
|
||||
},
|
||||
{
|
||||
label: 'Purchase License',
|
||||
color: 'success',
|
||||
hidden: check.data == null || check.data.status === 'active',
|
||||
leftSlot: <Icon icon="circle_dollar_sign" />,
|
||||
rightSlot: <Icon icon="external_link" color="success" className="opacity-60" />,
|
||||
onSelect: () => openUrl('https://yaak.app/pricing'),
|
||||
},
|
||||
{
|
||||
label: 'Install CLI',
|
||||
hidden: appInfo.cliVersion != null,
|
||||
leftSlot: <Icon icon="square_terminal" />,
|
||||
rightSlot: <Icon icon="external_link" color="secondary" />,
|
||||
onSelect: () => openUrl('https://yaak.app/docs/cli'),
|
||||
},
|
||||
{
|
||||
label: 'Feedback',
|
||||
leftSlot: <Icon icon="chat" />,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useMemo } from 'react';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Heading } from './Heading';
|
||||
import { IconButton } from './IconButton';
|
||||
import { DialogSize } from '@yaakapp-internal/plugins';
|
||||
import type { DialogSize } from '@yaakapp-internal/plugins';
|
||||
|
||||
export interface DialogProps {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface EditorProps {
|
||||
hideGutter?: boolean;
|
||||
id?: string;
|
||||
language?: EditorLanguage | 'pairs' | 'url' | 'timeline' | null;
|
||||
lintExtension?: Extension;
|
||||
graphQLSchema?: GraphQLSchema | null;
|
||||
onBlur?: () => void;
|
||||
onChange?: (value: string) => void;
|
||||
@@ -124,6 +125,7 @@ function EditorInner({
|
||||
hideGutter,
|
||||
graphQLSchema,
|
||||
language,
|
||||
lintExtension,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
@@ -325,13 +327,14 @@ function EditorInner({
|
||||
);
|
||||
|
||||
// Update the language extension when the language changes
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: none
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally limited deps
|
||||
useEffect(() => {
|
||||
if (cm.current === null) return;
|
||||
const { view, languageCompartment } = cm.current;
|
||||
const ext = getLanguageExtension({
|
||||
useTemplating,
|
||||
language,
|
||||
lintExtension,
|
||||
hideGutter,
|
||||
environmentVariables,
|
||||
autocomplete,
|
||||
@@ -344,6 +347,7 @@ function EditorInner({
|
||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||
}, [
|
||||
language,
|
||||
lintExtension,
|
||||
autocomplete,
|
||||
environmentVariables,
|
||||
onClickFunction,
|
||||
@@ -357,7 +361,7 @@ function EditorInner({
|
||||
]);
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Only reinitialize when necessary
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: only reinitialize when necessary
|
||||
const initEditorRef = useCallback(
|
||||
function initEditorRef(container: HTMLDivElement | null) {
|
||||
if (container === null) {
|
||||
@@ -371,6 +375,7 @@ function EditorInner({
|
||||
const langExt = getLanguageExtension({
|
||||
useTemplating,
|
||||
language,
|
||||
lintExtension,
|
||||
completionOptions,
|
||||
autocomplete,
|
||||
environmentVariables,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { history, historyKeymap } from '@codemirror/commands';
|
||||
import { go } from '@codemirror/lang-go';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { php } from '@codemirror/lang-php';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
@@ -34,7 +33,6 @@ import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { linter, lintGutter, lintKeymap } from '@codemirror/lint';
|
||||
|
||||
import { search, searchKeymap } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
@@ -50,6 +48,7 @@ import {
|
||||
rectangularSelection,
|
||||
} from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { jsonc, jsoncLanguage } from '@shopify/lang-jsonc';
|
||||
import { graphql } from 'cm6-graphql';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId';
|
||||
@@ -61,13 +60,13 @@ import { showGraphQLDocExplorerAtom } from '../../graphql/graphqlAtoms';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { jsonParseLinter } from './json-lint';
|
||||
import { pairs } from './pairs/extension';
|
||||
import { searchMatchCount } from './searchMatchCount';
|
||||
import { text } from './text/extension';
|
||||
import { timeline } from './timeline/extension';
|
||||
import type { TwigCompletionOption } from './twig/completion';
|
||||
import { twig } from './twig/extension';
|
||||
import { pathParametersPlugin } from './twig/pathParameters';
|
||||
import { url } from './url/extension';
|
||||
import { searchMatchCount } from './searchMatchCount';
|
||||
|
||||
export const syntaxHighlightStyle = HighlightStyle.define([
|
||||
{
|
||||
@@ -107,7 +106,7 @@ const syntaxExtensions: Record<
|
||||
null | (() => LanguageSupport)
|
||||
> = {
|
||||
graphql: null,
|
||||
json: json,
|
||||
json: jsonc,
|
||||
javascript: javascript,
|
||||
// HTML as XML because HTML is oddly slow
|
||||
html: xml,
|
||||
@@ -140,6 +139,7 @@ const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript
|
||||
export function getLanguageExtension({
|
||||
useTemplating,
|
||||
language = 'text',
|
||||
lintExtension,
|
||||
environmentVariables,
|
||||
autocomplete,
|
||||
hideGutter,
|
||||
@@ -156,7 +156,7 @@ export function getLanguageExtension({
|
||||
onClickPathParameter: (name: string) => void;
|
||||
completionOptions: TwigCompletionOption[];
|
||||
graphQLSchema: GraphQLSchema | null;
|
||||
} & Pick<EditorProps, 'language' | 'autocomplete' | 'hideGutter'>) {
|
||||
} & Pick<EditorProps, 'language' | 'autocomplete' | 'hideGutter' | 'lintExtension'>) {
|
||||
const extraExtensions: Extension[] = [];
|
||||
|
||||
if (language === 'url') {
|
||||
@@ -193,7 +193,12 @@ export function getLanguageExtension({
|
||||
}
|
||||
|
||||
if (language === 'json') {
|
||||
extraExtensions.push(linter(jsonParseLinter()));
|
||||
extraExtensions.push(lintExtension ?? linter(jsonParseLinter()));
|
||||
extraExtensions.push(
|
||||
jsoncLanguage.data.of({
|
||||
commentTokens: { line: '//', block: { open: '/*', close: '*/' } },
|
||||
}),
|
||||
);
|
||||
if (!hideGutter) {
|
||||
extraExtensions.push(lintGutter());
|
||||
}
|
||||
|
||||
@@ -4,14 +4,22 @@ import { parse as jsonLintParse } from '@prantlf/jsonlint';
|
||||
|
||||
const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g;
|
||||
|
||||
export function jsonParseLinter() {
|
||||
interface JsonLintOptions {
|
||||
allowComments?: boolean;
|
||||
allowTrailingCommas?: boolean;
|
||||
}
|
||||
|
||||
export function jsonParseLinter(options?: JsonLintOptions) {
|
||||
return (view: EditorView): Diagnostic[] => {
|
||||
try {
|
||||
const doc = view.state.doc.toString();
|
||||
// We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template
|
||||
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
|
||||
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => '1'.repeat(m.length));
|
||||
jsonLintParse(escapedDoc);
|
||||
jsonLintParse(escapedDoc, {
|
||||
mode: (options?.allowComments ?? true) ? 'cjson' : 'json',
|
||||
ignoreTrailingCommas: options?.allowTrailingCommas ?? false,
|
||||
});
|
||||
// biome-ignore lint/suspicious/noExplicitAny: none
|
||||
} catch (err: any) {
|
||||
if (!('location' in err)) {
|
||||
|
||||
@@ -20,8 +20,15 @@ const hiddenStyles: CSSProperties = {
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
type TooltipPosition = 'top' | 'bottom';
|
||||
|
||||
interface TooltipOpenState {
|
||||
styles: CSSProperties;
|
||||
position: TooltipPosition;
|
||||
}
|
||||
|
||||
export function Tooltip({ children, className, content, tabIndex, size = 'md' }: TooltipProps) {
|
||||
const [isOpen, setIsOpen] = useState<CSSProperties>();
|
||||
const [openState, setOpenState] = useState<TooltipOpenState | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const showTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
@@ -29,16 +36,25 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
const handleOpenImmediate = () => {
|
||||
if (triggerRef.current == null || tooltipRef.current == null) return;
|
||||
clearTimeout(showTimeout.current);
|
||||
setIsOpen(undefined);
|
||||
const triggerRect = triggerRef.current.getBoundingClientRect();
|
||||
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
||||
const docRect = document.documentElement.getBoundingClientRect();
|
||||
const viewportHeight = document.documentElement.clientHeight;
|
||||
|
||||
const margin = 8;
|
||||
const spaceAbove = Math.max(0, triggerRect.top - margin);
|
||||
const spaceBelow = Math.max(0, viewportHeight - triggerRect.bottom - margin);
|
||||
const preferBottom = spaceAbove < tooltipRect.height + margin && spaceBelow > spaceAbove;
|
||||
const position: TooltipPosition = preferBottom ? 'bottom' : 'top';
|
||||
|
||||
const styles: CSSProperties = {
|
||||
bottom: docRect.height - triggerRect.top,
|
||||
left: Math.max(0, triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2),
|
||||
maxHeight: triggerRect.top,
|
||||
maxHeight: position === 'top' ? spaceAbove : spaceBelow,
|
||||
...(position === 'top'
|
||||
? { bottom: viewportHeight - triggerRect.top }
|
||||
: { top: triggerRect.bottom }),
|
||||
};
|
||||
setIsOpen(styles);
|
||||
|
||||
setOpenState({ styles, position });
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
@@ -48,16 +64,16 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
|
||||
const handleClose = () => {
|
||||
clearTimeout(showTimeout.current);
|
||||
setIsOpen(undefined);
|
||||
setOpenState(null);
|
||||
};
|
||||
|
||||
const handleToggleImmediate = () => {
|
||||
if (isOpen) handleClose();
|
||||
if (openState) handleClose();
|
||||
else handleOpenImmediate();
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (isOpen && e.key === 'Escape') {
|
||||
if (openState && e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
@@ -71,10 +87,10 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
<Portal name="tooltip">
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
style={isOpen ?? hiddenStyles}
|
||||
style={openState?.styles ?? hiddenStyles}
|
||||
id={id.current}
|
||||
role="tooltip"
|
||||
aria-hidden={!isOpen}
|
||||
aria-hidden={openState == null}
|
||||
onMouseEnter={handleOpenImmediate}
|
||||
onMouseLeave={handleClose}
|
||||
className="p-2 fixed z-50 text-sm transition-opacity grid grid-rows-[minmax(0,1fr)]"
|
||||
@@ -88,14 +104,17 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
<Triangle className="text-border mb-2" />
|
||||
<Triangle
|
||||
className="text-border"
|
||||
position={openState?.position === 'bottom' ? 'top' : 'bottom'}
|
||||
/>
|
||||
</div>
|
||||
</Portal>
|
||||
{/** biome-ignore lint/a11y/useSemanticElements: Needs to be usable in other buttons */}
|
||||
<span
|
||||
ref={triggerRef}
|
||||
role="button"
|
||||
aria-describedby={isOpen ? id.current : undefined}
|
||||
aria-describedby={openState ? id.current : undefined}
|
||||
tabIndex={tabIndex ?? -1}
|
||||
className={classNames(className, 'flex-grow-0 flex items-center')}
|
||||
onClick={handleToggleImmediate}
|
||||
@@ -111,7 +130,9 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
);
|
||||
}
|
||||
|
||||
function Triangle({ className }: { className?: string }) {
|
||||
function Triangle({ className, position }: { className?: string; position: 'top' | 'bottom' }) {
|
||||
const isBottom = position === 'bottom';
|
||||
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
@@ -120,15 +141,19 @@ function Triangle({ className }: { className?: string }) {
|
||||
shapeRendering="crispEdges"
|
||||
className={classNames(
|
||||
className,
|
||||
'absolute z-50 border-t-[2px] border-surface-highlight',
|
||||
'-bottom-[calc(0.5rem-3px)] left-[calc(50%-0.4rem)]',
|
||||
'h-[0.5rem] w-[0.8rem]',
|
||||
'absolute z-50 left-[calc(50%-0.4rem)] h-[0.5rem] w-[0.8rem]',
|
||||
isBottom
|
||||
? 'border-t-[2px] border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2'
|
||||
: 'border-b-[2px] border-surface-highlight -top-[calc(0.5rem-3px)] mt-2',
|
||||
)}
|
||||
>
|
||||
<title>Triangle</title>
|
||||
<polygon className="fill-surface-highlight" points="0,0 30,0 15,10" />
|
||||
<polygon
|
||||
className="fill-surface-highlight"
|
||||
points={isBottom ? '0,0 30,0 15,10' : '0,10 30,10 15,0'}
|
||||
/>
|
||||
<path
|
||||
d="M0 0 L15 9 L30 0"
|
||||
d={isBottom ? 'M0 0 L15 9 L30 0' : 'M0 10 L15 1 L30 10'}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCachedNode } from '@dnd-kit/core/dist/hooks/utilities';
|
||||
|
||||
import type { GitStatusEntry } from '@yaakapp-internal/git';
|
||||
import { useGit } from '@yaakapp-internal/git';
|
||||
import type {
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { modelToYaml } from '../../lib/diffYaml';
|
||||
import { isSubEnvironment } from '../../lib/model_util';
|
||||
import { resolvedModelName } from '../../lib/resolvedModelName';
|
||||
import { showErrorToast } from '../../lib/toast';
|
||||
import { Banner } from '../core/Banner';
|
||||
|
||||
@@ -75,7 +75,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) {
|
||||
|
||||
const currentBranch = status.data.headRefShorthand;
|
||||
const hasChanges = status.data.entries.some((e) => e.status !== 'current');
|
||||
const hasRemotes = (status.data.origins ?? []).length > 0;
|
||||
const _hasRemotes = (status.data.origins ?? []).length > 0;
|
||||
const { ahead, behind } = status.data;
|
||||
|
||||
const tryCheckout = (branch: string, force: boolean) => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
|
||||
import { formatSdl } from 'format-graphql';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
@@ -16,6 +15,7 @@ import { Editor } from '../core/Editor/LazyEditor';
|
||||
import { FormattedError } from '../core/FormattedError';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { Separator } from '../core/Separator';
|
||||
import { tryFormatGraphql } from '../../lib/formatters';
|
||||
import { showGraphQLDocExplorerAtom } from './graphqlAtoms';
|
||||
|
||||
type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> & {
|
||||
@@ -156,6 +156,7 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
||||
{ type: 'separator', label: 'Setting' },
|
||||
{
|
||||
label: 'Automatic Introspection',
|
||||
keepOpenOnSelect: true,
|
||||
onSelect: () => {
|
||||
setAutoIntrospectDisabled({
|
||||
...autoIntrospectDisabled,
|
||||
@@ -210,7 +211,7 @@ function GraphQLEditorInner({ request, onChange, baseRequest, ...extraEditorProp
|
||||
language="graphql"
|
||||
heightMode="auto"
|
||||
graphQLSchema={schema}
|
||||
format={formatSdl}
|
||||
format={tryFormatGraphql}
|
||||
defaultValue={currentBody.query}
|
||||
onChange={handleChangeQuery}
|
||||
placeholder="..."
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const HEADER_SIZE_MD = '27px';
|
||||
export const HEADER_SIZE_MD = '30px';
|
||||
export const HEADER_SIZE_LG = '40px';
|
||||
|
||||
export const WINDOW_CONTROLS_WIDTH = '10.5rem';
|
||||
|
||||
@@ -20,6 +20,18 @@ export async function tryFormatJson(text: string): Promise<string> {
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function tryFormatGraphql(text: string): Promise<string> {
|
||||
if (text === '') return text;
|
||||
|
||||
try {
|
||||
return await invokeCmd<string>('cmd_format_graphql', { text });
|
||||
} catch (err) {
|
||||
console.warn('Failed to format GraphQL', err);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function tryFormatXml(text: string): Promise<string> {
|
||||
if (text === '') return text;
|
||||
|
||||
|
||||
30
src-web/lib/jsonComments.ts
Normal file
30
src-web/lib/jsonComments.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Simple heuristic to detect if a string likely contains JSON/JSONC comments.
|
||||
* Checks for // and /* patterns that are NOT inside double-quoted strings.
|
||||
* Used for UI hints only — doesn't need to be perfect.
|
||||
*/
|
||||
export function textLikelyContainsJsonComments(text: string): boolean {
|
||||
let inString = false;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (inString) {
|
||||
if (ch === '"') {
|
||||
inString = false;
|
||||
} else if (ch === '\\') {
|
||||
i++; // skip escaped char
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
inString = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '/' && i + 1 < text.length) {
|
||||
const next = text[i + 1];
|
||||
if (next === '/' || next === '*') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type TauriCmd =
|
||||
| 'cmd_delete_send_history'
|
||||
| 'cmd_dismiss_notification'
|
||||
| 'cmd_export_data'
|
||||
| 'cmd_format_graphql'
|
||||
| 'cmd_format_json'
|
||||
| 'cmd_get_http_authentication_config'
|
||||
| 'cmd_get_http_authentication_summaries'
|
||||
|
||||
@@ -138,7 +138,7 @@ function bannerColorVariables(color: YaakColor | null): Partial<CSSVariables> {
|
||||
};
|
||||
}
|
||||
|
||||
function inputCSS(color: YaakColor | null): Partial<CSSVariables> {
|
||||
function _inputCSS(color: YaakColor | null): Partial<CSSVariables> {
|
||||
if (color == null) return {};
|
||||
|
||||
const theme: Partial<ThemeComponentColors> = {
|
||||
|
||||
1
src-web/modules.d.ts
vendored
1
src-web/modules.d.ts
vendored
@@ -1,2 +1 @@
|
||||
declare module 'format-graphql';
|
||||
declare module 'vkbeautify';
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@replit/codemirror-emacs": "^6.1.0",
|
||||
"@replit/codemirror-vim": "^6.3.0",
|
||||
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
||||
"@shopify/lang-jsonc": "^1.0.1",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"@tanstack/react-router": "^1.133.13",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
@@ -46,7 +47,6 @@
|
||||
"deep-equal": "^2.2.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"focus-trap-react": "^11.0.4",
|
||||
"format-graphql": "^1.5.0",
|
||||
"fuzzbunny": "^1.0.1",
|
||||
"hexy": "^0.3.5",
|
||||
"history": "^5.3.0",
|
||||
|
||||
Reference in New Issue
Block a user