From 5f18bf25e26f099f4637a539fb8fef3c12b3283a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 9 Feb 2026 08:22:11 -0800 Subject: [PATCH 01/29] Replace shell-quote with shlex for curl import (#387) --- package-lock.json | 23 ++- plugins/importer-curl/package.json | 5 +- plugins/importer-curl/src/index.ts | 178 +++++++++++----------- plugins/importer-curl/tests/index.test.ts | 145 +++++++++++++++++- 4 files changed, 247 insertions(+), 104 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ad5040d..8833651d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3922,13 +3922,6 @@ "@types/react": "*" } }, - "node_modules/@types/shell-quote": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz", - "integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -13405,6 +13398,7 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13413,6 +13407,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/shlex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shlex/-/shlex-3.0.0.tgz", + "integrity": "sha512-jHPXQQk9d/QXCvJuLPYMOYWez3c43sORAgcIEoV7bFv5AJSJRAOyw5lQO12PMfd385qiLRCaDt7OtEzgrIGZUA==", + "license": "MIT" + }, "node_modules/should": { "version": "13.2.3", "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", @@ -15955,7 +15955,7 @@ }, "plugins-external/httpsnippet": { "name": "@yaak/httpsnippet", - "version": "1.0.0", + "version": "1.0.3", "dependencies": { "@readme/httpsnippet": "^11.0.0" }, @@ -15983,7 +15983,7 @@ }, "plugins-external/mcp-server": { "name": "@yaak/mcp-server", - "version": "0.1.7", + "version": "0.2.1", "dependencies": { "@hono/mcp": "^0.2.3", "@hono/node-server": "^1.19.7", @@ -16080,10 +16080,7 @@ "name": "@yaak/importer-curl", "version": "0.1.0", "dependencies": { - "shell-quote": "^1.8.1" - }, - "devDependencies": { - "@types/shell-quote": "^1.7.5" + "shlex": "^3.0.0" } }, "plugins/importer-insomnia": { diff --git a/plugins/importer-curl/package.json b/plugins/importer-curl/package.json index abb89b97..645204ad 100644 --- a/plugins/importer-curl/package.json +++ b/plugins/importer-curl/package.json @@ -10,9 +10,6 @@ "test": "vitest --run tests" }, "dependencies": { - "shell-quote": "^1.8.1" - }, - "devDependencies": { - "@types/shell-quote": "^1.7.5" + "shlex": "^3.0.0" } } diff --git a/plugins/importer-curl/src/index.ts b/plugins/importer-curl/src/index.ts index f8b0606e..23542c1e 100644 --- a/plugins/importer-curl/src/index.ts +++ b/plugins/importer-curl/src/index.ts @@ -7,8 +7,7 @@ import type { PluginDefinition, Workspace, } from '@yaakapp/api'; -import type { ControlOperator, ParseEntry } from 'shell-quote'; -import { parse } from 'shell-quote'; +import { split } from 'shlex'; type AtLeast = Partial & Pick; @@ -56,31 +55,89 @@ export const plugin: PluginDefinition = { }; /** - * Decodes escape sequences in shell $'...' strings - * Handles Unicode escape sequences (\uXXXX) and common escape codes + * Splits raw input into individual shell command strings. + * Handles line continuations, semicolons, and newline-separated curl commands. */ -function decodeShellString(str: string): string { - return str - .replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))) - .replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))) - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\t/g, '\t') - .replace(/\\'/g, "'") - .replace(/\\"/g, '"') - .replace(/\\\\/g, '\\'); -} +function splitCommands(rawData: string): string[] { + // Join line continuations (backslash-newline, and backslash-CRLF for Windows) + const joined = rawData.replace(/\\\r?\n/g, ' '); -/** - * Checks if a string might contain escape sequences that need decoding - * If so, decodes them; otherwise returns the string as-is - */ -function maybeDecodeEscapeSequences(str: string): string { - // Check if the string contains escape sequences that shell-quote might not handle - if (str.includes('\\u') || str.includes('\\x')) { - return decodeShellString(str); + // Count consecutive backslashes immediately before position i. + // An even count means the quote at i is NOT escaped; odd means it IS escaped. + function isEscaped(i: number): boolean { + let backslashes = 0; + let j = i - 1; + while (j >= 0 && joined[j] === '\\') { + backslashes++; + j--; + } + return backslashes % 2 !== 0; } - return str; + + // Split on semicolons and newlines to separate commands + const commands: string[] = []; + let current = ''; + let inSingleQuote = false; + let inDoubleQuote = false; + let inDollarQuote = false; + + for (let i = 0; i < joined.length; i++) { + const ch = joined[i]!; + const next = joined[i + 1]; + + // Track quoting state to avoid splitting inside quoted strings + if (!inDoubleQuote && !inDollarQuote && ch === "'" && !inSingleQuote) { + inSingleQuote = true; + current += ch; + continue; + } + if (inSingleQuote && ch === "'") { + inSingleQuote = false; + current += ch; + continue; + } + if (!inSingleQuote && !inDollarQuote && ch === '"' && !inDoubleQuote) { + inDoubleQuote = true; + current += ch; + continue; + } + if (inDoubleQuote && ch === '"' && !isEscaped(i)) { + inDoubleQuote = false; + current += ch; + continue; + } + if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && ch === '$' && next === "'") { + inDollarQuote = true; + current += ch + next; + i++; // Skip the opening quote + continue; + } + if (inDollarQuote && ch === "'" && !isEscaped(i)) { + inDollarQuote = false; + current += ch; + continue; + } + + const inQuote = inSingleQuote || inDoubleQuote || inDollarQuote; + + // Split on ;, newline, or CRLF when not inside quotes and not escaped + if (!inQuote && !isEscaped(i) && (ch === ';' || ch === '\n' || (ch === '\r' && next === '\n'))) { + if (ch === '\r') i++; // Skip the \n in \r\n + if (current.trim()) { + commands.push(current.trim()); + } + current = ''; + continue; + } + + current += ch; + } + + if (current.trim()) { + commands.push(current.trim()); + } + + return commands; } export function convertCurl(rawData: string) { @@ -88,68 +145,17 @@ export function convertCurl(rawData: string) { return null; } - const commands: ParseEntry[][] = []; + const commands: string[][] = splitCommands(rawData).map((cmd) => { + const tokens = split(cmd); - // Replace non-escaped newlines with semicolons to make parsing easier - // NOTE: This is really slow in debug build but fast in release mode - const normalizedData = rawData.replace(/\ncurl/g, '; curl'); - - let currentCommand: ParseEntry[] = []; - - const parsed = parse(normalizedData); - - // Break up `-XPOST` into `-X POST` - const normalizedParseEntries = parsed.flatMap((entry) => { - if ( - typeof entry === 'string' && - entry.startsWith('-') && - !entry.startsWith('--') && - entry.length > 2 - ) { - return [entry.slice(0, 2), entry.slice(2)]; - } - return entry; - }); - - for (const parseEntry of normalizedParseEntries) { - if (typeof parseEntry === 'string') { - if (parseEntry.startsWith('$')) { - // Handle $'...' strings from shell-quote - decode escape sequences - currentCommand.push(decodeShellString(parseEntry.slice(1))); - } else { - // Decode escape sequences that shell-quote might not handle - currentCommand.push(maybeDecodeEscapeSequences(parseEntry)); + // Break up squished arguments like `-XPOST` into `-X POST` + return tokens.flatMap((token) => { + if (token.startsWith('-') && !token.startsWith('--') && token.length > 2) { + return [token.slice(0, 2), token.slice(2)]; } - continue; - } - - if ('comment' in parseEntry) { - continue; - } - - const { op } = parseEntry as { op: 'glob'; pattern: string } | { op: ControlOperator }; - - // `;` separates commands - if (op === ';') { - commands.push(currentCommand); - currentCommand = []; - continue; - } - - if (op?.startsWith('$')) { - // Handle the case where literal like -H $'Header: \'Some Quoted Thing\'' - const str = decodeShellString(op.slice(2, op.length - 1)); - - currentCommand.push(str); - continue; - } - - if (op === 'glob') { - currentCommand.push((parseEntry as { op: 'glob'; pattern: string }).pattern); - } - } - - commands.push(currentCommand); + return token; + }); + }); const workspace: ExportResources['workspaces'][0] = { model: 'workspace', @@ -169,12 +175,12 @@ export function convertCurl(rawData: string) { }; } -function importCommand(parseEntries: ParseEntry[], workspaceId: string) { +function importCommand(parseEntries: string[], workspaceId: string) { // ~~~~~~~~~~~~~~~~~~~~~ // // Collect all the flags // // ~~~~~~~~~~~~~~~~~~~~~ // const flagsByName: FlagsByName = {}; - const singletons: ParseEntry[] = []; + const singletons: string[] = []; // Start at 1 so we can skip the ^curl part for (let i = 1; i < parseEntries.length; i++) { diff --git a/plugins/importer-curl/tests/index.test.ts b/plugins/importer-curl/tests/index.test.ts index 6f75b8e4..3c986817 100644 --- a/plugins/importer-curl/tests/index.test.ts +++ b/plugins/importer-curl/tests/index.test.ts @@ -112,9 +112,28 @@ describe('importer-curl', () => { }); }); + test('Imports with Windows CRLF line endings', () => { + expect( + convertCurl('curl \\\r\n -X POST \\\r\n https://yaak.app'), + ).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ url: 'https://yaak.app', method: 'POST' }), + ], + }, + }); + }); + + test('Throws on malformed quotes', () => { + expect(() => + convertCurl('curl -X POST -F "a=aaa" -F b=bbb" https://yaak.app'), + ).toThrow(); + }); + test('Imports form data', () => { expect( - convertCurl('curl -X POST -F "a=aaa" -F b=bbb" -F f=@filepath https://yaak.app'), + convertCurl('curl -X POST -F "a=aaa" -F b=bbb -F f=@filepath https://yaak.app'), ).toEqual({ resources: { workspaces: [baseWorkspace()], @@ -476,6 +495,130 @@ describe('importer-curl', () => { }); }); + test('Imports JSON body with newlines in $quotes', () => { + expect( + convertCurl( + `curl 'https://yaak.app' -H 'Content-Type: application/json' --data-raw $'{\\n "foo": "bar",\\n "baz": "qux"\\n}' -X POST`, + ), + ).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + method: 'POST', + headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }], + bodyType: 'application/json', + body: { text: '{\n "foo": "bar",\n "baz": "qux"\n}' }, + }), + ], + }, + }); + }); + + test('Handles double-quoted string ending with even backslashes before semicolon', () => { + // "C:\\" has two backslashes which escape each other, so the closing " is real. + // The ; after should split into a second command. + expect( + convertCurl( + 'curl -d "C:\\\\" https://yaak.app;curl https://example.com', + ), + ).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + method: 'POST', + bodyType: 'application/x-www-form-urlencoded', + body: { + form: [{ name: 'C:\\', value: '', enabled: true }], + }, + headers: [ + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + enabled: true, + }, + ], + }), + baseRequest({ url: 'https://example.com' }), + ], + }, + }); + }); + + test('Handles $quoted string ending with a literal backslash before semicolon', () => { + // $'C:\\\\' has two backslashes which become one literal backslash. + // The closing ' must not be misinterpreted as escaped. + // The ; after should split into a second command. + expect( + convertCurl( + "curl -d $'C:\\\\' https://yaak.app;curl https://example.com", + ), + ).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + method: 'POST', + bodyType: 'application/x-www-form-urlencoded', + body: { + form: [{ name: 'C:\\', value: '', enabled: true }], + }, + headers: [ + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + enabled: true, + }, + ], + }), + baseRequest({ url: 'https://example.com' }), + ], + }, + }); + }); + + test('Imports $quoted header with escaped single quotes', () => { + expect( + convertCurl( + `curl https://yaak.app -H $'X-Custom: it\\'s a test'`, + ), + ).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + headers: [{ name: 'X-Custom', value: "it's a test", enabled: true }], + }), + ], + }, + }); + }); + + test('Does not split on escaped semicolon outside quotes', () => { + // In shell, \; is a literal semicolon and should not split commands. + // This should be treated as a single curl command with the URL "https://yaak.app?a=1;b=2" + expect( + convertCurl('curl https://yaak.app?a=1\\;b=2'), + ).toEqual({ + resources: { + workspaces: [baseWorkspace()], + httpRequests: [ + baseRequest({ + url: 'https://yaak.app', + urlParameters: [ + { name: 'a', value: '1;b=2', enabled: true }, + ], + }), + ], + }, + }); + }); + test('Imports multipart form data with text-only fields from --data-raw', () => { const curlCommand = `curl 'http://example.com/api' \ -H 'Content-Type: multipart/form-data; boundary=----FormBoundary123' \ From 957d8d9d465c969a9000bce7b68880ccaceb5109 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 9 Feb 2026 08:43:49 -0800 Subject: [PATCH 02/29] Move faker plugin from external to bundled --- package-lock.json | 45 ++++++++++++++++++- package.json | 2 +- .../template-function-faker}/README.md | 0 .../template-function-faker}/package.json | 2 +- .../template-function-faker}/src/index.ts | 0 .../tests/init.test.ts | 0 .../template-function-faker}/tsconfig.json | 0 7 files changed, 46 insertions(+), 3 deletions(-) rename {plugins-external/faker => plugins/template-function-faker}/README.md (100%) rename {plugins-external/faker => plugins/template-function-faker}/package.json (91%) rename {plugins-external/faker => plugins/template-function-faker}/src/index.ts (100%) rename {plugins-external/faker => plugins/template-function-faker}/tests/init.test.ts (100%) rename {plugins-external/faker => plugins/template-function-faker}/tsconfig.json (100%) diff --git a/package-lock.json b/package-lock.json index 8833651d..167cad0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "packages/plugin-runtime", "packages/plugin-runtime-types", "plugins-external/mcp-server", - "plugins-external/template-function-faker", + "plugins/template-function-faker", "plugins-external/httpsnippet", "plugins/action-copy-curl", "plugins/action-copy-grpcurl", @@ -4153,6 +4153,10 @@ "resolved": "plugins/auth-oauth2", "link": true }, + "node_modules/@yaak/faker": { + "resolved": "plugins/template-function-faker", + "link": true + }, "node_modules/@yaak/filter-jsonpath": { "resolved": "plugins/filter-jsonpath", "link": true @@ -16058,6 +16062,18 @@ "name": "@yaak/auth-oauth2", "version": "0.1.0" }, + "plugins/faker": { + "name": "@yaak/faker", + "version": "1.1.1", + "extraneous": true, + "dependencies": { + "@faker-js/faker": "^10.1.0" + }, + "devDependencies": { + "@types/node": "^25.0.3", + "typescript": "^5.9.3" + } + }, "plugins/filter-jsonpath": { "name": "@yaak/filter-jsonpath", "version": "0.1.0", @@ -16135,6 +16151,33 @@ "name": "@yaak/template-function-encode", "version": "0.1.0" }, + "plugins/template-function-faker": { + "name": "@yaak/faker", + "version": "1.1.1", + "dependencies": { + "@faker-js/faker": "^10.1.0" + }, + "devDependencies": { + "@types/node": "^25.0.3", + "typescript": "^5.9.3" + } + }, + "plugins/template-function-faker/node_modules/@faker-js/faker": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.3.0.tgz", + "integrity": "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, "plugins/template-function-fs": { "name": "@yaak/template-function-fs", "version": "0.1.0" diff --git a/package.json b/package.json index 2501b08a..3a9d76df 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "packages/plugin-runtime", "packages/plugin-runtime-types", "plugins-external/mcp-server", - "plugins-external/template-function-faker", + "plugins/template-function-faker", "plugins-external/httpsnippet", "plugins/action-copy-curl", "plugins/action-copy-grpcurl", diff --git a/plugins-external/faker/README.md b/plugins/template-function-faker/README.md similarity index 100% rename from plugins-external/faker/README.md rename to plugins/template-function-faker/README.md diff --git a/plugins-external/faker/package.json b/plugins/template-function-faker/package.json similarity index 91% rename from plugins-external/faker/package.json rename to plugins/template-function-faker/package.json index b5d00a7e..5e281288 100755 --- a/plugins-external/faker/package.json +++ b/plugins/template-function-faker/package.json @@ -7,7 +7,7 @@ "repository": { "type": "git", "url": "https://github.com/mountain-loop/yaak.git", - "directory": "plugins-external/faker" + "directory": "plugins/template-function-faker" }, "scripts": { "build": "yaakcli build", diff --git a/plugins-external/faker/src/index.ts b/plugins/template-function-faker/src/index.ts similarity index 100% rename from plugins-external/faker/src/index.ts rename to plugins/template-function-faker/src/index.ts diff --git a/plugins-external/faker/tests/init.test.ts b/plugins/template-function-faker/tests/init.test.ts similarity index 100% rename from plugins-external/faker/tests/init.test.ts rename to plugins/template-function-faker/tests/init.test.ts diff --git a/plugins-external/faker/tsconfig.json b/plugins/template-function-faker/tsconfig.json similarity index 100% rename from plugins-external/faker/tsconfig.json rename to plugins/template-function-faker/tsconfig.json From a8176d6e9e6002461bde7c9de7590b0f04cd7bc0 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 9 Feb 2026 10:17:43 -0800 Subject: [PATCH 03/29] Skip disabled key-value entries during request rendering Skip disabled headers, metadata, URL parameters, and form body entries in the render phase for HTTP, gRPC, and WebSocket requests. Previously, disabled entries were still template-rendered even though they were filtered out later at the use site. --- crates-tauri/yaak-app/src/render.rs | 72 +++++++++++++++++++++++++++++ crates/yaak-ws/src/render.rs | 6 +++ 2 files changed, 78 insertions(+) diff --git a/crates-tauri/yaak-app/src/render.rs b/crates-tauri/yaak-app/src/render.rs index f5ad8fad..e63f525f 100644 --- a/crates-tauri/yaak-app/src/render.rs +++ b/crates-tauri/yaak-app/src/render.rs @@ -38,6 +38,9 @@ pub async fn render_grpc_request( 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?, @@ -119,6 +122,7 @@ pub async fn render_http_request( let mut body = BTreeMap::new(); for (k, v) in r.body.clone() { + let v = if k == "form" { strip_disabled_form_entries(v) } else { v }; body.insert(k, render_json_value_raw(v, vars, cb, &opt).await?); } @@ -161,3 +165,71 @@ pub async fn render_http_request( Ok(HttpRequest { url, url_parameters, headers, body, authentication, ..r.to_owned() }) } + +/// Strip disabled entries from a JSON array of form objects. +fn strip_disabled_form_entries(v: Value) -> Value { + match v { + Value::Array(items) => Value::Array( + items + .into_iter() + .filter(|item| item.get("enabled").and_then(|e| e.as_bool()).unwrap_or(true)) + .collect(), + ), + v => v, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_strip_disabled_form_entries() { + let input = json!([ + {"enabled": true, "name": "foo", "value": "bar"}, + {"enabled": false, "name": "disabled", "value": "gone"}, + {"enabled": true, "name": "baz", "value": "qux"}, + ]); + let result = strip_disabled_form_entries(input); + assert_eq!( + result, + json!([ + {"enabled": true, "name": "foo", "value": "bar"}, + {"enabled": true, "name": "baz", "value": "qux"}, + ]) + ); + } + + #[test] + fn test_strip_disabled_form_entries_all_disabled() { + let input = json!([ + {"enabled": false, "name": "a", "value": "b"}, + {"enabled": false, "name": "c", "value": "d"}, + ]); + let result = strip_disabled_form_entries(input); + assert_eq!(result, json!([])); + } + + #[test] + fn test_strip_disabled_form_entries_missing_enabled_defaults_to_kept() { + let input = json!([ + {"name": "no_enabled_field", "value": "kept"}, + {"enabled": false, "name": "disabled", "value": "gone"}, + ]); + let result = strip_disabled_form_entries(input); + assert_eq!( + result, + json!([ + {"name": "no_enabled_field", "value": "kept"}, + ]) + ); + } + + #[test] + fn test_strip_disabled_form_entries_non_array_passthrough() { + let input = json!("just a string"); + let result = strip_disabled_form_entries(input.clone()); + assert_eq!(result, input); + } +} diff --git a/crates/yaak-ws/src/render.rs b/crates/yaak-ws/src/render.rs index 1b9c8961..151b41fd 100644 --- a/crates/yaak-ws/src/render.rs +++ b/crates/yaak-ws/src/render.rs @@ -16,6 +16,9 @@ pub async fn render_websocket_request( let mut url_parameters = Vec::new(); for p in r.url_parameters.clone() { + if !p.enabled { + continue; + } url_parameters.push(HttpUrlParameter { enabled: p.enabled, name: parse_and_render(&p.name, vars, cb, opt).await?, @@ -26,6 +29,9 @@ pub async fn render_websocket_request( let mut headers = Vec::new(); for p in r.headers.clone() { + if !p.enabled { + continue; + } headers.push(HttpRequestHeader { enabled: p.enabled, name: parse_and_render(&p.name, vars, cb, opt).await?, From fda18c543412de8f417a091fe3641b399e66eab1 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 9 Feb 2026 10:22:03 -0800 Subject: [PATCH 04/29] Snapshot faker template function names in test Replace the brittle count assertion (toBe(226)) with a snapshot of all exported function names. This catches accidental additions, removals, or renames across faker upgrades with a clear diff. Co-Authored-By: Claude Opus 4.6 --- .../tests/__snapshots__/init.test.ts.snap | 233 ++++++++++++++++++ .../tests/init.test.ts | 13 +- 2 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 plugins/template-function-faker/tests/__snapshots__/init.test.ts.snap diff --git a/plugins/template-function-faker/tests/__snapshots__/init.test.ts.snap b/plugins/template-function-faker/tests/__snapshots__/init.test.ts.snap new file mode 100644 index 00000000..adc26a67 --- /dev/null +++ b/plugins/template-function-faker/tests/__snapshots__/init.test.ts.snap @@ -0,0 +1,233 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`template-function-faker > exports all expected template functions 1`] = ` +[ + "faker.airline.aircraftType", + "faker.airline.airline", + "faker.airline.airplane", + "faker.airline.airport", + "faker.airline.flightNumber", + "faker.airline.recordLocator", + "faker.airline.seat", + "faker.animal.bear", + "faker.animal.bird", + "faker.animal.cat", + "faker.animal.cetacean", + "faker.animal.cow", + "faker.animal.crocodilia", + "faker.animal.dog", + "faker.animal.fish", + "faker.animal.horse", + "faker.animal.insect", + "faker.animal.lion", + "faker.animal.petName", + "faker.animal.rabbit", + "faker.animal.rodent", + "faker.animal.snake", + "faker.animal.type", + "faker.color.cmyk", + "faker.color.colorByCSSColorSpace", + "faker.color.cssSupportedFunction", + "faker.color.cssSupportedSpace", + "faker.color.hsl", + "faker.color.human", + "faker.color.hwb", + "faker.color.lab", + "faker.color.lch", + "faker.color.rgb", + "faker.color.space", + "faker.commerce.department", + "faker.commerce.isbn", + "faker.commerce.price", + "faker.commerce.product", + "faker.commerce.productAdjective", + "faker.commerce.productDescription", + "faker.commerce.productMaterial", + "faker.commerce.productName", + "faker.commerce.upc", + "faker.company.buzzAdjective", + "faker.company.buzzNoun", + "faker.company.buzzPhrase", + "faker.company.buzzVerb", + "faker.company.catchPhrase", + "faker.company.catchPhraseAdjective", + "faker.company.catchPhraseDescriptor", + "faker.company.catchPhraseNoun", + "faker.company.name", + "faker.database.collation", + "faker.database.column", + "faker.database.engine", + "faker.database.mongodbObjectId", + "faker.database.type", + "faker.date.anytime", + "faker.date.between", + "faker.date.betweens", + "faker.date.birthdate", + "faker.date.future", + "faker.date.month", + "faker.date.past", + "faker.date.recent", + "faker.date.soon", + "faker.date.timeZone", + "faker.date.weekday", + "faker.finance.accountName", + "faker.finance.accountNumber", + "faker.finance.amount", + "faker.finance.bic", + "faker.finance.bitcoinAddress", + "faker.finance.creditCardCVV", + "faker.finance.creditCardIssuer", + "faker.finance.creditCardNumber", + "faker.finance.currency", + "faker.finance.currencyCode", + "faker.finance.currencyName", + "faker.finance.currencyNumericCode", + "faker.finance.currencySymbol", + "faker.finance.ethereumAddress", + "faker.finance.iban", + "faker.finance.litecoinAddress", + "faker.finance.pin", + "faker.finance.routingNumber", + "faker.finance.transactionDescription", + "faker.finance.transactionType", + "faker.git.branch", + "faker.git.commitDate", + "faker.git.commitEntry", + "faker.git.commitMessage", + "faker.git.commitSha", + "faker.hacker.abbreviation", + "faker.hacker.adjective", + "faker.hacker.ingverb", + "faker.hacker.noun", + "faker.hacker.phrase", + "faker.hacker.verb", + "faker.image.avatar", + "faker.image.avatarGitHub", + "faker.image.dataUri", + "faker.image.personPortrait", + "faker.image.url", + "faker.image.urlLoremFlickr", + "faker.image.urlPicsumPhotos", + "faker.internet.displayName", + "faker.internet.domainName", + "faker.internet.domainSuffix", + "faker.internet.domainWord", + "faker.internet.email", + "faker.internet.emoji", + "faker.internet.exampleEmail", + "faker.internet.httpMethod", + "faker.internet.httpStatusCode", + "faker.internet.ip", + "faker.internet.ipv4", + "faker.internet.ipv6", + "faker.internet.jwt", + "faker.internet.jwtAlgorithm", + "faker.internet.mac", + "faker.internet.password", + "faker.internet.port", + "faker.internet.protocol", + "faker.internet.url", + "faker.internet.userAgent", + "faker.internet.username", + "faker.location.buildingNumber", + "faker.location.cardinalDirection", + "faker.location.city", + "faker.location.continent", + "faker.location.country", + "faker.location.countryCode", + "faker.location.county", + "faker.location.direction", + "faker.location.language", + "faker.location.latitude", + "faker.location.longitude", + "faker.location.nearbyGPSCoordinate", + "faker.location.ordinalDirection", + "faker.location.secondaryAddress", + "faker.location.state", + "faker.location.street", + "faker.location.streetAddress", + "faker.location.timeZone", + "faker.location.zipCode", + "faker.lorem.lines", + "faker.lorem.paragraph", + "faker.lorem.paragraphs", + "faker.lorem.sentence", + "faker.lorem.sentences", + "faker.lorem.slug", + "faker.lorem.text", + "faker.lorem.word", + "faker.lorem.words", + "faker.music.album", + "faker.music.artist", + "faker.music.genre", + "faker.music.songName", + "faker.number.bigInt", + "faker.number.binary", + "faker.number.float", + "faker.number.hex", + "faker.number.int", + "faker.number.octal", + "faker.number.romanNumeral", + "faker.person.bio", + "faker.person.firstName", + "faker.person.fullName", + "faker.person.gender", + "faker.person.jobArea", + "faker.person.jobDescriptor", + "faker.person.jobTitle", + "faker.person.jobType", + "faker.person.lastName", + "faker.person.middleName", + "faker.person.prefix", + "faker.person.sex", + "faker.person.sexType", + "faker.person.suffix", + "faker.person.zodiacSign", + "faker.phone.imei", + "faker.phone.number", + "faker.science.chemicalElement", + "faker.science.unit", + "faker.string.alpha", + "faker.string.alphanumeric", + "faker.string.binary", + "faker.string.fromCharacters", + "faker.string.hexadecimal", + "faker.string.nanoid", + "faker.string.numeric", + "faker.string.octal", + "faker.string.sample", + "faker.string.symbol", + "faker.string.ulid", + "faker.string.uuid", + "faker.system.commonFileExt", + "faker.system.commonFileName", + "faker.system.commonFileType", + "faker.system.cron", + "faker.system.directoryPath", + "faker.system.fileExt", + "faker.system.fileName", + "faker.system.filePath", + "faker.system.fileType", + "faker.system.mimeType", + "faker.system.networkInterface", + "faker.system.semver", + "faker.vehicle.bicycle", + "faker.vehicle.color", + "faker.vehicle.fuel", + "faker.vehicle.manufacturer", + "faker.vehicle.model", + "faker.vehicle.type", + "faker.vehicle.vehicle", + "faker.vehicle.vin", + "faker.vehicle.vrm", + "faker.word.adjective", + "faker.word.adverb", + "faker.word.conjunction", + "faker.word.interjection", + "faker.word.noun", + "faker.word.preposition", + "faker.word.sample", + "faker.word.verb", + "faker.word.words", +] +`; diff --git a/plugins/template-function-faker/tests/init.test.ts b/plugins/template-function-faker/tests/init.test.ts index e16b5bd1..e344e5ae 100644 --- a/plugins/template-function-faker/tests/init.test.ts +++ b/plugins/template-function-faker/tests/init.test.ts @@ -1,9 +1,12 @@ import { describe, expect, it } from 'vitest'; -describe('formatDatetime', () => { - it('returns formatted current date', async () => { - // Ensure the plugin imports properly - const faker = await import('../src/index'); - expect(faker.plugin.templateFunctions?.length).toBe(226); +describe('template-function-faker', () => { + it('exports all expected template functions', async () => { + const { plugin } = await import('../src/index'); + const names = plugin.templateFunctions?.map((fn) => fn.name).sort() ?? []; + + // Snapshot the full list of exported function names so we catch any + // accidental additions, removals, or renames across faker upgrades. + expect(names).toMatchSnapshot(); }); }); From 484dcfade0aaf2c6e57e75b83961ec1cc30b843c Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 14:38:40 -0800 Subject: [PATCH 05/29] Add Flatpak and Flathub packaging support (#388) --- .github/workflows/flathub.yml | 44 ++++++++++++++++ .gitignore | 4 ++ flatpak/app.yaak.Yaak.metainfo.xml | 57 +++++++++++++++++++++ flatpak/app.yaak.Yaak.yml | 64 +++++++++++++++++++++++ flatpak/update-manifest.sh | 82 ++++++++++++++++++++++++++++++ 5 files changed, 251 insertions(+) create mode 100644 .github/workflows/flathub.yml create mode 100644 flatpak/app.yaak.Yaak.metainfo.xml create mode 100644 flatpak/app.yaak.Yaak.yml create mode 100755 flatpak/update-manifest.sh diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml new file mode 100644 index 00000000..647627b9 --- /dev/null +++ b/.github/workflows/flathub.yml @@ -0,0 +1,44 @@ +name: Update Flathub +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + update-flathub: + name: Update Flathub manifest + runs-on: ubuntu-latest + # Only run for stable releases (skip betas/pre-releases) + if: ${{ !github.event.release.prerelease }} + steps: + - name: Checkout app repo + uses: actions/checkout@v4 + + - name: Run update-manifest.sh + run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" + + - name: Checkout Flathub repo + uses: actions/checkout@v4 + with: + repository: flathub/app.yaak.Yaak + token: ${{ secrets.FLATHUB_TOKEN }} + path: flathub-repo + + - name: Copy updated files to Flathub repo + run: | + cp flatpak/app.yaak.Yaak.yml flathub-repo/ + cp flatpak/app.yaak.Yaak.metainfo.xml flathub-repo/ + cp LICENSE flathub-repo/ + sed -i 's|path: \.\./LICENSE|path: LICENSE|' flathub-repo/app.yaak.Yaak.yml + + - name: Commit and push to Flathub + working-directory: flathub-repo + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --quiet && echo "No changes to commit" && exit 0 + git commit -m "Update to ${{ github.event.release.tag_name }}" + git push diff --git a/.gitignore b/.gitignore index e8922cf5..a8773a8a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,7 @@ crates-tauri/yaak-app/tauri.worktree.conf.json # Tauri auto-generated permission files **/permissions/autogenerated **/permissions/schemas + +# Flatpak build artifacts +flatpak-repo/ +.flatpak-builder/ diff --git a/flatpak/app.yaak.Yaak.metainfo.xml b/flatpak/app.yaak.Yaak.metainfo.xml new file mode 100644 index 00000000..6fd4120d --- /dev/null +++ b/flatpak/app.yaak.Yaak.metainfo.xml @@ -0,0 +1,57 @@ + + + app.yaak.Yaak + + Yaak + An offline, Git friendly API Client + + + Yaak + + + MIT + MIT + + https://yaak.app + https://yaak.app/feedback + https://yaak.app/feedback + https://github.com/mountain-loop/yaak + + +

+ A fast, privacy-first API client for REST, GraphQL, SSE, WebSocket, + and gRPC — built with Tauri, Rust, and React. +

+

Features include:

+
    +
  • REST, GraphQL, SSE, WebSocket, and gRPC support
  • +
  • Local-only data, secrets encryption, and zero telemetry
  • +
  • Git-friendly plain-text project storage
  • +
  • Environment variables and template functions
  • +
  • Request chaining and dynamic values
  • +
  • OAuth 2.0, Bearer, Basic, API Key, AWS, JWT, and NTLM authentication
  • +
  • Import from cURL, Postman, Insomnia, and OpenAPI
  • +
  • Extensible plugin system
  • +
+
+ + app.yaak.Yaak.desktop + + + #8b32ff + #c293ff + + + + + + + Crafting an API request + https://assets.yaak.app/uploads/screenshot-BLG1w_2310x1326.png + + + + + + +
diff --git a/flatpak/app.yaak.Yaak.yml b/flatpak/app.yaak.Yaak.yml new file mode 100644 index 00000000..e0713721 --- /dev/null +++ b/flatpak/app.yaak.Yaak.yml @@ -0,0 +1,64 @@ +id: app.yaak.Yaak +runtime: org.gnome.Platform +runtime-version: "48" +sdk: org.gnome.Sdk +command: yaak-app +rename-desktop-file: yaak.desktop +rename-icon: yaak-app + +finish-args: + - --socket=wayland + - --socket=fallback-x11 + - --share=ipc + - --device=dri + - --share=network + - --socket=pulseaudio # Preview audio responses + - --socket=ssh-auth # Git SSH remotes + - --socket=gpg-agent # Git commit signing + - --talk-name=org.freedesktop.secrets # Keyring for encryption + - --filesystem=home # Git repos, ~/.gitconfig, ~/.ssh, etc + +modules: + - name: git + cleanup: + - /share + make-args: + - NO_PERL=1 + - NO_TCLTK=1 + make-install-args: + - INSTALL_SYMLINKS=1 + - NO_PERL=1 + - NO_TCLTK=1 + sources: + - type: archive + url: https://www.kernel.org/pub/software/scm/git/git-2.48.1.tar.gz + sha256: 51b4d03b1e311ba673591210f94f24a4c5781453e1eb188822e3d9cdc04c2212 + + - name: yaak + buildsystem: simple + build-commands: + - ar -x yaak.deb + - tar -xf data.tar.gz + - mv usr/bin/* /app/bin + - mv usr/lib/* /app/lib + - mv usr/share/* /app/share + - install -Dm644 app.yaak.Yaak.metainfo.xml /app/share/metainfo/app.yaak.Yaak.metainfo.xml + - install -Dm644 LICENSE /app/share/licenses/app.yaak.Yaak/LICENSE + + sources: + - type: file + dest-filename: yaak.deb + url: https://github.com/mountain-loop/yaak/releases/download/v2026.1.2/yaak_2026.1.2_amd64.deb + sha256: "c4236b5bcf391e579dc79b71c3b5c58f6f9bfc6c175fc70426d0ca85799beba5" + only-arches: + - x86_64 + - type: file + dest-filename: yaak.deb + url: https://github.com/mountain-loop/yaak/releases/download/v2026.1.2/yaak_2026.1.2_arm64.deb + sha256: "9ba9b7c9df56ffb9b801e40cb38685f1650cf7e2f9e85dad0ae3329f8e01ff6d" + only-arches: + - aarch64 + - type: file + path: app.yaak.Yaak.metainfo.xml + - type: file + path: ../LICENSE diff --git a/flatpak/update-manifest.sh b/flatpak/update-manifest.sh new file mode 100755 index 00000000..e75c6186 --- /dev/null +++ b/flatpak/update-manifest.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# Update the Flatpak manifest with URLs and SHA256 hashes for a given release. +# +# Usage: +# ./flatpak/update-manifest.sh v2026.2.0 +# +# This script: +# 1. Downloads the x86_64 and aarch64 .deb files from the GitHub release +# 2. Computes their SHA256 checksums +# 3. Updates the manifest YAML with the correct URLs and hashes +# 4. Updates the metainfo.xml with a new entry + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +MANIFEST="$SCRIPT_DIR/app.yaak.Yaak.yml" +METAINFO="$SCRIPT_DIR/app.yaak.Yaak.metainfo.xml" + +if [ $# -lt 1 ]; then + echo "Usage: $0 " + echo "Example: $0 v2026.2.0" + exit 1 +fi + +VERSION_TAG="$1" +VERSION="${VERSION_TAG#v}" + +# Only allow stable releases (skip beta, alpha, rc, etc.) +if [[ "$VERSION" == *-* ]]; then + echo "Skipping pre-release version '$VERSION_TAG' (only stable releases are published to Flathub)" + exit 0 +fi + +REPO="mountain-loop/yaak" +BASE_URL="https://github.com/$REPO/releases/download/$VERSION_TAG" + +DEB_AMD64="yaak_${VERSION}_amd64.deb" +DEB_ARM64="yaak_${VERSION}_arm64.deb" + +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +echo "Downloading $DEB_AMD64..." +curl -fSL "$BASE_URL/$DEB_AMD64" -o "$TMPDIR/$DEB_AMD64" +SHA_AMD64=$(sha256sum "$TMPDIR/$DEB_AMD64" | cut -d' ' -f1) +echo " SHA256: $SHA_AMD64" + +echo "Downloading $DEB_ARM64..." +curl -fSL "$BASE_URL/$DEB_ARM64" -o "$TMPDIR/$DEB_ARM64" +SHA_ARM64=$(sha256sum "$TMPDIR/$DEB_ARM64" | cut -d' ' -f1) +echo " SHA256: $SHA_ARM64" + +echo "" +echo "Updating manifest: $MANIFEST" + +# Update URLs by matching the arch-specific deb filename +sed -i "s|url: .*amd64\.deb|url: $BASE_URL/$DEB_AMD64|" "$MANIFEST" +sed -i "s|url: .*arm64\.deb|url: $BASE_URL/$DEB_ARM64|" "$MANIFEST" + +# Update SHA256 hashes by finding the current ones and replacing +OLD_SHA_AMD64=$(grep -A2 "amd64\.deb" "$MANIFEST" | grep sha256 | sed 's/.*"\(.*\)"/\1/') +OLD_SHA_ARM64=$(grep -A2 "arm64\.deb" "$MANIFEST" | grep sha256 | sed 's/.*"\(.*\)"/\1/') + +sed -i "s|$OLD_SHA_AMD64|$SHA_AMD64|" "$MANIFEST" +sed -i "s|$OLD_SHA_ARM64|$SHA_ARM64|" "$MANIFEST" + +echo " Manifest updated." + +echo "Updating metainfo: $METAINFO" + +TODAY=$(date +%Y-%m-%d) + +# Insert new release entry after +sed -i "s| | \n |" "$METAINFO" + +echo " Metainfo updated." + +echo "" +echo "Done! Review the changes:" +echo " $MANIFEST" +echo " $METAINFO" From 654af0995161a8b47a0b7733ffac70ce5f353b0c Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 15:22:51 -0800 Subject: [PATCH 06/29] Bump GNOME runtime to 49, fix corrupted arm64 SHA256 --- flatpak/app.yaak.Yaak.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatpak/app.yaak.Yaak.yml b/flatpak/app.yaak.Yaak.yml index e0713721..44f9be31 100644 --- a/flatpak/app.yaak.Yaak.yml +++ b/flatpak/app.yaak.Yaak.yml @@ -1,6 +1,6 @@ id: app.yaak.Yaak runtime: org.gnome.Platform -runtime-version: "48" +runtime-version: "49" sdk: org.gnome.Sdk command: yaak-app rename-desktop-file: yaak.desktop From 7fef35ce0a157eca96c6fe3e058ebeba979a32d4 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 15:26:40 -0800 Subject: [PATCH 07/29] Ship metainfo in deb, remove from Flatpak manifest --- .github/workflows/flathub.yml | 1 - crates-tauri/yaak-app/tauri.release.conf.json | 32 +++++++------------ flatpak/app.yaak.Yaak.yml | 3 -- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml index 647627b9..98d95e02 100644 --- a/.github/workflows/flathub.yml +++ b/.github/workflows/flathub.yml @@ -29,7 +29,6 @@ jobs: - name: Copy updated files to Flathub repo run: | cp flatpak/app.yaak.Yaak.yml flathub-repo/ - cp flatpak/app.yaak.Yaak.metainfo.xml flathub-repo/ cp LICENSE flathub-repo/ sed -i 's|path: \.\./LICENSE|path: LICENSE|' flathub-repo/app.yaak.Yaak.yml diff --git a/crates-tauri/yaak-app/tauri.release.conf.json b/crates-tauri/yaak-app/tauri.release.conf.json index 20ad2868..a6b4ebfa 100644 --- a/crates-tauri/yaak-app/tauri.release.conf.json +++ b/crates-tauri/yaak-app/tauri.release.conf.json @@ -1,9 +1,6 @@ { "build": { - "features": [ - "updater", - "license" - ] + "features": ["updater", "license"] }, "app": { "security": { @@ -11,12 +8,8 @@ "default", { "identifier": "release", - "windows": [ - "*" - ], - "permissions": [ - "yaak-license:default" - ] + "windows": ["*"], + "permissions": ["yaak-license:default"] } ] } @@ -39,14 +32,7 @@ "createUpdaterArtifacts": true, "longDescription": "A cross-platform desktop app for interacting with REST, GraphQL, and gRPC", "shortDescription": "Play with APIs, intuitively", - "targets": [ - "app", - "appimage", - "deb", - "dmg", - "nsis", - "rpm" - ], + "targets": ["app", "appimage", "deb", "dmg", "nsis", "rpm"], "macOS": { "minimumSystemVersion": "13.0", "exceptionDomain": "", @@ -58,10 +44,16 @@ }, "linux": { "deb": { - "desktopTemplate": "./template.desktop" + "desktopTemplate": "./template.desktop", + "files": { + "usr/share/metainfo/app.yaak.Yaak.metainfo.xml": "../../flatpak/app.yaak.Yaak.metainfo.xml" + } }, "rpm": { - "desktopTemplate": "./template.desktop" + "desktopTemplate": "./template.desktop", + "files": { + "usr/share/metainfo/app.yaak.Yaak.metainfo.xml": "../../flatpak/app.yaak.Yaak.metainfo.xml" + } } } } diff --git a/flatpak/app.yaak.Yaak.yml b/flatpak/app.yaak.Yaak.yml index 44f9be31..ad4e5817 100644 --- a/flatpak/app.yaak.Yaak.yml +++ b/flatpak/app.yaak.Yaak.yml @@ -42,7 +42,6 @@ modules: - mv usr/bin/* /app/bin - mv usr/lib/* /app/lib - mv usr/share/* /app/share - - install -Dm644 app.yaak.Yaak.metainfo.xml /app/share/metainfo/app.yaak.Yaak.metainfo.xml - install -Dm644 LICENSE /app/share/licenses/app.yaak.Yaak/LICENSE sources: @@ -58,7 +57,5 @@ modules: sha256: "9ba9b7c9df56ffb9b801e40cb38685f1650cf7e2f9e85dad0ae3329f8e01ff6d" only-arches: - aarch64 - - type: file - path: app.yaak.Yaak.metainfo.xml - type: file path: ../LICENSE From 76ee3fa61bebe8a5cc57cb286e8210155f516b75 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 23:05:33 -0800 Subject: [PATCH 08/29] Flatpak: build from source instead of repackaging debs (#389) --- .github/workflows/flathub.yml | 18 ++++ .gitignore | 3 + crates/yaak-templates/build-wasm.cjs | 8 ++ crates/yaak-templates/package.json | 2 +- flatpak/app.yaak.Yaak.metainfo.xml | 1 - flatpak/app.yaak.Yaak.yml | 140 ++++++++++++++++++++++++--- flatpak/fix-lockfile.mjs | 73 ++++++++++++++ flatpak/generate-sources.sh | 43 ++++++++ flatpak/update-manifest.sh | 79 ++++++++------- package-lock.json | 12 --- scripts/vendor-node.cjs | 20 ++++ scripts/vendor-protoc.cjs | 20 ++++ 12 files changed, 356 insertions(+), 63 deletions(-) create mode 100644 crates/yaak-templates/build-wasm.cjs create mode 100644 flatpak/fix-lockfile.mjs create mode 100755 flatpak/generate-sources.sh diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml index 98d95e02..5ecad626 100644 --- a/.github/workflows/flathub.yml +++ b/.github/workflows/flathub.yml @@ -16,6 +16,21 @@ jobs: - name: Checkout app repo uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install source generators + run: | + pip install flatpak-node-generator tomlkit aiohttp + git clone --depth 1 https://github.com/flatpak/flatpak-builder-tools flatpak/flatpak-builder-tools + - name: Run update-manifest.sh run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" @@ -29,6 +44,9 @@ jobs: - name: Copy updated files to Flathub repo run: | cp flatpak/app.yaak.Yaak.yml flathub-repo/ + cp flatpak/app.yaak.Yaak.metainfo.xml flathub-repo/ + cp flatpak/cargo-sources.json flathub-repo/ + cp flatpak/node-sources.json flathub-repo/ cp LICENSE flathub-repo/ sed -i 's|path: \.\./LICENSE|path: LICENSE|' flathub-repo/app.yaak.Yaak.yml diff --git a/.gitignore b/.gitignore index a8773a8a..40c48bd8 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ crates-tauri/yaak-app/tauri.worktree.conf.json # Flatpak build artifacts flatpak-repo/ .flatpak-builder/ +flatpak/flatpak-builder-tools/ +flatpak/cargo-sources.json +flatpak/node-sources.json diff --git a/crates/yaak-templates/build-wasm.cjs b/crates/yaak-templates/build-wasm.cjs new file mode 100644 index 00000000..4f86941b --- /dev/null +++ b/crates/yaak-templates/build-wasm.cjs @@ -0,0 +1,8 @@ +const { execSync } = require('node:child_process'); + +if (process.env.SKIP_WASM_BUILD === '1') { + console.log('Skipping wasm-pack build (SKIP_WASM_BUILD=1)'); + return; +} + +execSync('wasm-pack build --target bundler', { stdio: 'inherit' }); diff --git a/crates/yaak-templates/package.json b/crates/yaak-templates/package.json index f4c75149..9826bfb6 100644 --- a/crates/yaak-templates/package.json +++ b/crates/yaak-templates/package.json @@ -6,7 +6,7 @@ "scripts": { "bootstrap": "npm run build", "build": "run-s build:*", - "build:pack": "wasm-pack build --target bundler", + "build:pack": "node build-wasm.cjs", "build:clean": "rimraf ./pkg/.gitignore" }, "devDependencies": { diff --git a/flatpak/app.yaak.Yaak.metainfo.xml b/flatpak/app.yaak.Yaak.metainfo.xml index 6fd4120d..83f52d38 100644 --- a/flatpak/app.yaak.Yaak.metainfo.xml +++ b/flatpak/app.yaak.Yaak.metainfo.xml @@ -52,6 +52,5 @@ - diff --git a/flatpak/app.yaak.Yaak.yml b/flatpak/app.yaak.Yaak.yml index ad4e5817..d5f6119c 100644 --- a/flatpak/app.yaak.Yaak.yml +++ b/flatpak/app.yaak.Yaak.yml @@ -3,6 +3,11 @@ runtime: org.gnome.Platform runtime-version: "49" sdk: org.gnome.Sdk command: yaak-app + +sdk-extensions: + - org.freedesktop.Sdk.Extension.node22 + - org.freedesktop.Sdk.Extension.rust-stable + rename-desktop-file: yaak.desktop rename-icon: yaak-app @@ -36,26 +41,135 @@ modules: - name: yaak buildsystem: simple + build-options: + append-path: /app/bin:/usr/lib/sdk/node22/bin:/usr/lib/sdk/rust-stable/bin + env: + CARGO_HOME: /run/build/yaak/cargo + XDG_CACHE_HOME: /run/build/yaak/flatpak-node/cache + npm_config_cache: /run/build/yaak/flatpak-node/npm-cache + npm_config_offline: "true" + npm_config_nodedir: /usr/lib/sdk/node22 + NODE_OPTIONS: --max_old_space_size=4096 + build-commands: - - ar -x yaak.deb - - tar -xf data.tar.gz - - mv usr/bin/* /app/bin - - mv usr/lib/* /app/lib - - mv usr/share/* /app/share + # Vendor Node.js binary (sidecar for plugin runtime) + - mkdir -p crates-tauri/yaak-app/vendored/node + - install -Dm755 vendored-node/bin/node crates-tauri/yaak-app/vendored/node/yaaknode + + # Vendor protoc binary and includes + - mkdir -p crates-tauri/yaak-app/vendored/protoc + - install -Dm755 protoc-bin/protoc crates-tauri/yaak-app/vendored/protoc/yaakprotoc + - mkdir -p crates-tauri/yaak-app/vendored/protoc/include && cp -r protoc-bin/google crates-tauri/yaak-app/vendored/protoc/include/google + + # Patch lockfile: add resolved URLs for nested workspace deps that npm + # omits (see https://github.com/npm/cli/issues/4460) + - >- + node -e "const fs=require('fs'); + const p='package-lock.json'; + const d=JSON.parse(fs.readFileSync(p,'utf-8')); + for(const[n,info]of Object.entries(d.packages||{})){ + if(!n||info.link||info.resolved||!n.includes('node_modules/')||!info.version)continue; + const pkg=n.split('node_modules/').pop(); + const base=pkg.split('/').pop(); + info.resolved='https://registry.npmjs.org/'+pkg+'/-/'+base+'-'+info.version+'.tgz'; + }fs.writeFileSync(p,JSON.stringify(d,null,2));" + + # Install npm dependencies offline + - npm ci --offline + + # Pre-fetch Cargo dependencies offline + - cargo --offline fetch --manifest-path Cargo.toml + + # Skip wasm-pack build (pre-built wasm is checked into the repo) + - >- + node -e "const fs=require('fs'); + const p='crates/yaak-templates/package.json'; + const d=JSON.parse(fs.readFileSync(p)); + d.scripts['build:pack']='echo Skipping wasm-pack build'; + fs.writeFileSync(p,JSON.stringify(d,null,2));" + + # Build all workspace packages (frontend, plugins, wasm, plugin-runtime) + - npm run build + + # Copy built plugins to vendored directory + - npm run vendor:vendor-plugins + + # Build the Tauri app (cargo build directly to avoid inotify limits from tauri CLI) + - cargo build --offline --release -p yaak-app + + # Install binary + - install -Dm755 target/release/yaak-app /app/bin/yaak-app + + # Install icons from source + - install -Dm644 crates-tauri/yaak-app/icons/release/32x32.png /app/share/icons/hicolor/32x32/apps/yaak-app.png + - install -Dm644 crates-tauri/yaak-app/icons/release/64x64.png /app/share/icons/hicolor/64x64/apps/yaak-app.png + - install -Dm644 crates-tauri/yaak-app/icons/release/128x128.png /app/share/icons/hicolor/128x128/apps/yaak-app.png + - install -Dm644 crates-tauri/yaak-app/icons/release/icon.png /app/share/icons/hicolor/512x512/apps/yaak-app.png + + # Install desktop file + - >- + printf '[Desktop Entry]\nCategories=Development;\nComment=The API client for modern developers\nExec=yaak-app\nIcon=yaak-app\nName=Yaak\nStartupWMClass=yaak\nTerminal=false\nType=Application\n' + > yaak.desktop + - install -Dm644 yaak.desktop /app/share/applications/yaak.desktop + + # Install metainfo and license + - install -Dm644 app.yaak.Yaak.metainfo.xml /app/share/metainfo/app.yaak.Yaak.metainfo.xml - install -Dm644 LICENSE /app/share/licenses/app.yaak.Yaak/LICENSE sources: - - type: file - dest-filename: yaak.deb - url: https://github.com/mountain-loop/yaak/releases/download/v2026.1.2/yaak_2026.1.2_amd64.deb - sha256: "c4236b5bcf391e579dc79b71c3b5c58f6f9bfc6c175fc70426d0ca85799beba5" + # Application source + - type: git + url: https://github.com/mountain-loop/yaak.git + tag: v2026.1.2 + commit: bd7e840a5700ddefb5ef1f22771cc5555000f777 + x-checker-data: + type: git + tag-pattern: ^v(\d+\.\d+\.\d+)$ + + # Offline npm dependencies + - node-sources.json + + # Offline Cargo dependencies + - cargo-sources.json + + # Vendored Node.js binary (x86_64) + - type: archive + url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-x64.tar.gz + sha256: 58a5ff5cc8f2200e458bea22e329d5c1994aa1b111d499ca46ec2411d58239ca + strip-components: 1 + dest: vendored-node only-arches: - x86_64 - - type: file - dest-filename: yaak.deb - url: https://github.com/mountain-loop/yaak/releases/download/v2026.1.2/yaak_2026.1.2_arm64.deb - sha256: "9ba9b7c9df56ffb9b801e40cb38685f1650cf7e2f9e85dad0ae3329f8e01ff6d" + + # Vendored Node.js binary (aarch64) + - type: archive + url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-arm64.tar.gz + sha256: 0dc93ec5c798b0d347f068db6d205d03dea9a71765e6a53922b682b91265d71f + strip-components: 1 + dest: vendored-node only-arches: - aarch64 + + # Vendored protoc binary and includes (x86_64) + - type: archive + url: https://github.com/protocolbuffers/protobuf/releases/download/v33.1/protoc-33.1-linux-x86_64.zip + sha256: f3340e28a83d1c637d8bafdeed92b9f7db6a384c26bca880a6e5217b40a4328b + dest: protoc-bin + only-arches: + - x86_64 + + # Vendored protoc binary and includes (aarch64) + - type: archive + url: https://github.com/protocolbuffers/protobuf/releases/download/v33.1/protoc-33.1-linux-aarch_64.zip + sha256: 6018147740548e0e0f764408c87f4cd040e6e1c1203e13aeacaf811892b604f3 + dest: protoc-bin + only-arches: + - aarch64 + + # License file - type: file path: ../LICENSE + + # Metainfo file (not in tagged source yet) + - type: file + path: app.yaak.Yaak.metainfo.xml diff --git a/flatpak/fix-lockfile.mjs b/flatpak/fix-lockfile.mjs new file mode 100644 index 00000000..aa3826de --- /dev/null +++ b/flatpak/fix-lockfile.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node + +// Adds missing `resolved` and `integrity` fields to npm package-lock.json. +// +// npm sometimes omits these fields for nested dependencies inside workspace +// packages. This breaks offline installs and tools like flatpak-node-generator +// that need explicit tarball URLs for every package. +// +// Based on https://github.com/grant-dennison/npm-package-lock-add-resolved +// (MIT License, Copyright (c) 2024 Grant Dennison) + +import { readFile, writeFile } from "node:fs/promises"; +import { get } from "node:https"; + +const lockfilePath = process.argv[2] || "package-lock.json"; + +function fetchJson(url) { + return new Promise((resolve, reject) => { + get(url, (res) => { + let data = ""; + res.on("data", (chunk) => { + data += chunk; + }); + res.on("end", () => { + if (res.statusCode === 200) { + resolve(JSON.parse(data)); + } else { + reject(`${url} returned ${res.statusCode} ${res.statusMessage}`); + } + }); + res.on("error", reject); + }).on("error", reject); + }); +} + +async function fillResolved(name, p) { + const version = p.version.replace(/^.*@/, ""); + console.log(`Retrieving metadata for ${name}@${version}`); + const metadataUrl = `https://registry.npmjs.com/${name}/${version}`; + const metadata = await fetchJson(metadataUrl); + p.resolved = metadata.dist.tarball; + p.integrity = metadata.dist.integrity; +} + +let changesMade = false; + +async function fillAllResolved(packages) { + for (const packagePath in packages) { + if (packagePath === "") continue; + const p = packages[packagePath]; + if (!p.inBundle && !p.bundled && (!p.resolved || !p.integrity)) { + const packageName = + p.name || + /^npm:(.+?)@.+$/.exec(p.version)?.[1] || + packagePath.replace(/^.*node_modules\/(?=.+?$)/, ""); + await fillResolved(packageName, p); + changesMade = true; + } + } +} + +const oldContents = await readFile(lockfilePath, "utf-8"); +const packageLock = JSON.parse(oldContents); + +await fillAllResolved(packageLock.packages ?? []); + +if (changesMade) { + const newContents = JSON.stringify(packageLock, null, 2) + "\n"; + await writeFile(lockfilePath, newContents); + console.log(`Updated ${lockfilePath}`); +} else { + console.log("No changes needed."); +} diff --git a/flatpak/generate-sources.sh b/flatpak/generate-sources.sh new file mode 100755 index 00000000..f656d0aa --- /dev/null +++ b/flatpak/generate-sources.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# +# Generate offline dependency source files for Flatpak builds. +# +# Prerequisites: +# pip install flatpak-node-generator tomlkit aiohttp +# Clone https://github.com/flatpak/flatpak-builder-tools (for cargo generator) +# +# Usage: +# ./flatpak/generate-sources.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Generate cargo-sources.json +python3 "$SCRIPT_DIR/flatpak-builder-tools/cargo/flatpak-cargo-generator.py" \ + -o "$SCRIPT_DIR/cargo-sources.json" "$REPO_ROOT/Cargo.lock" + +# Generate node-sources.json from a patched copy of the lockfile. +# npm omits resolved/integrity for some workspace deps, and +# flatpak-node-generator can't handle workspace link entries. +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"' EXIT + +cp "$REPO_ROOT/package-lock.json" "$TMPDIR/package-lock.json" +cp "$REPO_ROOT/package.json" "$TMPDIR/package.json" + +node "$SCRIPT_DIR/fix-lockfile.mjs" "$TMPDIR/package-lock.json" + +node -e " + const fs = require('fs'); + const p = process.argv[1]; + const d = JSON.parse(fs.readFileSync(p, 'utf-8')); + for (const [name, info] of Object.entries(d.packages || {})) { + if (name && (info.link || !info.resolved)) delete d.packages[name]; + } + fs.writeFileSync(p, JSON.stringify(d, null, 2)); +" "$TMPDIR/package-lock.json" + +flatpak-node-generator --no-requests-cache \ + -o "$SCRIPT_DIR/node-sources.json" npm "$TMPDIR/package-lock.json" diff --git a/flatpak/update-manifest.sh b/flatpak/update-manifest.sh index e75c6186..1676365c 100755 --- a/flatpak/update-manifest.sh +++ b/flatpak/update-manifest.sh @@ -1,19 +1,19 @@ #!/usr/bin/env bash # -# Update the Flatpak manifest with URLs and SHA256 hashes for a given release. +# Update the Flatpak manifest for a new release. # # Usage: # ./flatpak/update-manifest.sh v2026.2.0 # # This script: -# 1. Downloads the x86_64 and aarch64 .deb files from the GitHub release -# 2. Computes their SHA256 checksums -# 3. Updates the manifest YAML with the correct URLs and hashes -# 4. Updates the metainfo.xml with a new entry +# 1. Updates the git tag and commit in the manifest +# 2. Regenerates cargo-sources.json and node-sources.json from the tagged lockfiles +# 3. Adds a new entry to the metainfo set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" MANIFEST="$SCRIPT_DIR/app.yaak.Yaak.yml" METAINFO="$SCRIPT_DIR/app.yaak.Yaak.metainfo.xml" @@ -26,57 +26,64 @@ fi VERSION_TAG="$1" VERSION="${VERSION_TAG#v}" -# Only allow stable releases (skip beta, alpha, rc, etc.) if [[ "$VERSION" == *-* ]]; then echo "Skipping pre-release version '$VERSION_TAG' (only stable releases are published to Flathub)" exit 0 fi REPO="mountain-loop/yaak" -BASE_URL="https://github.com/$REPO/releases/download/$VERSION_TAG" +COMMIT=$(git ls-remote "https://github.com/$REPO.git" "refs/tags/$VERSION_TAG" | cut -f1) -DEB_AMD64="yaak_${VERSION}_amd64.deb" -DEB_ARM64="yaak_${VERSION}_arm64.deb" +if [ -z "$COMMIT" ]; then + echo "Error: Could not resolve commit for tag $VERSION_TAG" + exit 1 +fi +echo "Tag: $VERSION_TAG" +echo "Commit: $COMMIT" + +# Update git tag and commit in the manifest +sed -i "s|tag: v.*|tag: $VERSION_TAG|" "$MANIFEST" +sed -i "s|commit: .*|commit: $COMMIT|" "$MANIFEST" +echo "Updated manifest tag and commit." + +# Regenerate offline dependency sources from the tagged lockfiles TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT -echo "Downloading $DEB_AMD64..." -curl -fSL "$BASE_URL/$DEB_AMD64" -o "$TMPDIR/$DEB_AMD64" -SHA_AMD64=$(sha256sum "$TMPDIR/$DEB_AMD64" | cut -d' ' -f1) -echo " SHA256: $SHA_AMD64" +echo "Fetching lockfiles from $VERSION_TAG..." +curl -fsSL "https://raw.githubusercontent.com/$REPO/$VERSION_TAG/Cargo.lock" -o "$TMPDIR/Cargo.lock" +curl -fsSL "https://raw.githubusercontent.com/$REPO/$VERSION_TAG/package-lock.json" -o "$TMPDIR/package-lock.json" +curl -fsSL "https://raw.githubusercontent.com/$REPO/$VERSION_TAG/package.json" -o "$TMPDIR/package.json" -echo "Downloading $DEB_ARM64..." -curl -fSL "$BASE_URL/$DEB_ARM64" -o "$TMPDIR/$DEB_ARM64" -SHA_ARM64=$(sha256sum "$TMPDIR/$DEB_ARM64" | cut -d' ' -f1) -echo " SHA256: $SHA_ARM64" +echo "Generating cargo-sources.json..." +python3 "$SCRIPT_DIR/flatpak-builder-tools/cargo/flatpak-cargo-generator.py" \ + -o "$SCRIPT_DIR/cargo-sources.json" "$TMPDIR/Cargo.lock" -echo "" -echo "Updating manifest: $MANIFEST" +echo "Generating node-sources.json..." +node "$SCRIPT_DIR/fix-lockfile.mjs" "$TMPDIR/package-lock.json" -# Update URLs by matching the arch-specific deb filename -sed -i "s|url: .*amd64\.deb|url: $BASE_URL/$DEB_AMD64|" "$MANIFEST" -sed -i "s|url: .*arm64\.deb|url: $BASE_URL/$DEB_ARM64|" "$MANIFEST" +node -e " + const fs = require('fs'); + const p = process.argv[1]; + const d = JSON.parse(fs.readFileSync(p, 'utf-8')); + for (const [name, info] of Object.entries(d.packages || {})) { + if (name && (info.link || !info.resolved)) delete d.packages[name]; + } + fs.writeFileSync(p, JSON.stringify(d, null, 2)); +" "$TMPDIR/package-lock.json" -# Update SHA256 hashes by finding the current ones and replacing -OLD_SHA_AMD64=$(grep -A2 "amd64\.deb" "$MANIFEST" | grep sha256 | sed 's/.*"\(.*\)"/\1/') -OLD_SHA_ARM64=$(grep -A2 "arm64\.deb" "$MANIFEST" | grep sha256 | sed 's/.*"\(.*\)"/\1/') - -sed -i "s|$OLD_SHA_AMD64|$SHA_AMD64|" "$MANIFEST" -sed -i "s|$OLD_SHA_ARM64|$SHA_ARM64|" "$MANIFEST" - -echo " Manifest updated." - -echo "Updating metainfo: $METAINFO" +flatpak-node-generator --no-requests-cache \ + -o "$SCRIPT_DIR/node-sources.json" npm "$TMPDIR/package-lock.json" +# Update metainfo with new release TODAY=$(date +%Y-%m-%d) - -# Insert new release entry after sed -i "s| | \n |" "$METAINFO" - -echo " Metainfo updated." +echo "Updated metainfo with release $VERSION." echo "" echo "Done! Review the changes:" echo " $MANIFEST" echo " $METAINFO" +echo " $SCRIPT_DIR/cargo-sources.json" +echo " $SCRIPT_DIR/node-sources.json" diff --git a/package-lock.json b/package-lock.json index 167cad0f..1946700b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16062,18 +16062,6 @@ "name": "@yaak/auth-oauth2", "version": "0.1.0" }, - "plugins/faker": { - "name": "@yaak/faker", - "version": "1.1.1", - "extraneous": true, - "dependencies": { - "@faker-js/faker": "^10.1.0" - }, - "devDependencies": { - "@types/node": "^25.0.3", - "typescript": "^5.9.3" - } - }, "plugins/filter-jsonpath": { "name": "@yaak/filter-jsonpath", "version": "0.1.0", diff --git a/scripts/vendor-node.cjs b/scripts/vendor-node.cjs index 1dd29cf0..92160b3a 100644 --- a/scripts/vendor-node.cjs +++ b/scripts/vendor-node.cjs @@ -1,4 +1,6 @@ const path = require('node:path'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); const decompress = require('decompress'); const Downloader = require('nodejs-file-downloader'); const { rmSync, cpSync, mkdirSync, existsSync } = require('node:fs'); @@ -41,6 +43,15 @@ const DST_BIN_MAP = { [WIN_ARM]: 'yaaknode.exe', }; +const SHA256_MAP = { + [MAC_ARM]: 'b05aa3a66efe680023f930bd5af3fdbbd542794da5644ca2ad711d68cbd4dc35', + [MAC_X64]: '096081b6d6fcdd3f5ba0f5f1d44a47e83037ad2e78eada26671c252fe64dd111', + [LNX_ARM]: '0dc93ec5c798b0d347f068db6d205d03dea9a71765e6a53922b682b91265d71f', + [LNX_X64]: '58a5ff5cc8f2200e458bea22e329d5c1994aa1b111d499ca46ec2411d58239ca', + [WIN_X64]: '5355ae6d7c49eddcfde7d34ac3486820600a831bf81dc3bdca5c8db6a9bb0e76', + [WIN_ARM]: 'ce9ee4e547ebdff355beb48e309b166c24df6be0291c9eaf103ce15f3de9e5b4', +}; + const key = `${process.platform}_${process.env.YAAK_TARGET_ARCH ?? process.arch}`; const destDir = path.join(__dirname, `..`, 'crates-tauri', 'yaak-app', 'vendored', 'node'); @@ -68,6 +79,15 @@ rmSync(tmpDir, { recursive: true, force: true }); timeout: 1000 * 60 * 2, }).download(); + // Verify SHA256 + const expectedHash = SHA256_MAP[key]; + const fileBuffer = fs.readFileSync(filePath); + const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + if (actualHash !== expectedHash) { + throw new Error(`SHA256 mismatch for ${path.basename(filePath)}\n expected: ${expectedHash}\n actual: ${actualHash}`); + } + console.log('SHA256 verified:', actualHash); + // Decompress to the same directory await decompress(filePath, tmpDir, {}); diff --git a/scripts/vendor-protoc.cjs b/scripts/vendor-protoc.cjs index 48a867ee..439bb10c 100644 --- a/scripts/vendor-protoc.cjs +++ b/scripts/vendor-protoc.cjs @@ -1,3 +1,5 @@ +const crypto = require('node:crypto'); +const fs = require('node:fs'); const decompress = require('decompress'); const Downloader = require('nodejs-file-downloader'); const path = require('node:path'); @@ -41,6 +43,15 @@ const DST_BIN_MAP = { [WIN_ARM]: 'yaakprotoc.exe', }; +const SHA256_MAP = { + [MAC_ARM]: 'db7e66ff7f9080614d0f5505a6b0ac488cf89a15621b6a361672d1332ec2e14e', + [MAC_X64]: 'e20b5f930e886da85e7402776a4959efb1ed60c57e72794bcade765e67abaa82', + [LNX_ARM]: '6018147740548e0e0f764408c87f4cd040e6e1c1203e13aeacaf811892b604f3', + [LNX_X64]: 'f3340e28a83d1c637d8bafdeed92b9f7db6a384c26bca880a6e5217b40a4328b', + [WIN_X64]: 'd7a207fb6eec0e4b1b6613be3b7d11905375b6fd1147a071116eb8e9f24ac53b', + [WIN_ARM]: 'd7a207fb6eec0e4b1b6613be3b7d11905375b6fd1147a071116eb8e9f24ac53b', +}; + const dstDir = path.join(__dirname, `..`, 'crates-tauri', 'yaak-app', 'vendored', 'protoc'); const key = `${process.platform}_${process.env.YAAK_TARGET_ARCH ?? process.arch}`; console.log(`Vendoring protoc ${VERSION} for ${key}`); @@ -63,6 +74,15 @@ mkdirSync(dstDir, { recursive: true }); // Download GitHub release artifact const { filePath } = await new Downloader({ url, directory: tmpDir }).download(); + // Verify SHA256 + const expectedHash = SHA256_MAP[key]; + const fileBuffer = fs.readFileSync(filePath); + const actualHash = crypto.createHash('sha256').update(fileBuffer).digest('hex'); + if (actualHash !== expectedHash) { + throw new Error(`SHA256 mismatch for ${path.basename(filePath)}\n expected: ${expectedHash}\n actual: ${actualHash}`); + } + console.log('SHA256 verified:', actualHash); + // Decompress to the same directory await decompress(filePath, tmpDir, {}); From a1c629581060e9a9c7f131b01b94859b65c13b56 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 23:19:23 -0800 Subject: [PATCH 09/29] Clean up Flatpak manifest for v2026.2.0 - Update tag to v2026.2.0 - Use SKIP_WASM_BUILD env var instead of build-time package.json patch - Install metainfo from git source (remove temporary type: file source) - Fix fix-lockfile.mjs to skip workspace packages - CI: commit metainfo releases back to app repo, bump permissions to write --- .github/workflows/flathub.yml | 11 ++++++++++- flatpak/app.yaak.Yaak.yml | 18 ++++-------------- flatpak/fix-lockfile.mjs | 2 ++ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml index 5ecad626..30005891 100644 --- a/.github/workflows/flathub.yml +++ b/.github/workflows/flathub.yml @@ -4,7 +4,7 @@ on: types: [published] permissions: - contents: read + contents: write jobs: update-flathub: @@ -34,6 +34,15 @@ jobs: - name: Run update-manifest.sh run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" + - name: Commit metainfo update to app repo + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add flatpak/app.yaak.Yaak.metainfo.xml + git diff --cached --quiet && echo "No metainfo changes" && exit 0 + git commit -m "Add ${{ github.event.release.tag_name }} to metainfo releases" + git push origin HEAD:main + - name: Checkout Flathub repo uses: actions/checkout@v4 with: diff --git a/flatpak/app.yaak.Yaak.yml b/flatpak/app.yaak.Yaak.yml index d5f6119c..81c1f68c 100644 --- a/flatpak/app.yaak.Yaak.yml +++ b/flatpak/app.yaak.Yaak.yml @@ -50,6 +50,7 @@ modules: npm_config_offline: "true" npm_config_nodedir: /usr/lib/sdk/node22 NODE_OPTIONS: --max_old_space_size=4096 + SKIP_WASM_BUILD: "1" build-commands: # Vendor Node.js binary (sidecar for plugin runtime) @@ -80,14 +81,6 @@ modules: # Pre-fetch Cargo dependencies offline - cargo --offline fetch --manifest-path Cargo.toml - # Skip wasm-pack build (pre-built wasm is checked into the repo) - - >- - node -e "const fs=require('fs'); - const p='crates/yaak-templates/package.json'; - const d=JSON.parse(fs.readFileSync(p)); - d.scripts['build:pack']='echo Skipping wasm-pack build'; - fs.writeFileSync(p,JSON.stringify(d,null,2));" - # Build all workspace packages (frontend, plugins, wasm, plugin-runtime) - npm run build @@ -113,15 +106,15 @@ modules: - install -Dm644 yaak.desktop /app/share/applications/yaak.desktop # Install metainfo and license - - install -Dm644 app.yaak.Yaak.metainfo.xml /app/share/metainfo/app.yaak.Yaak.metainfo.xml + - install -Dm644 flatpak/app.yaak.Yaak.metainfo.xml /app/share/metainfo/app.yaak.Yaak.metainfo.xml - install -Dm644 LICENSE /app/share/licenses/app.yaak.Yaak/LICENSE sources: # Application source - type: git url: https://github.com/mountain-loop/yaak.git - tag: v2026.1.2 - commit: bd7e840a5700ddefb5ef1f22771cc5555000f777 + tag: v2026.2.0 + commit: 76ee3fa61bebe8a5cc57cb286e8210155f516b75 x-checker-data: type: git tag-pattern: ^v(\d+\.\d+\.\d+)$ @@ -170,6 +163,3 @@ modules: - type: file path: ../LICENSE - # Metainfo file (not in tagged source yet) - - type: file - path: app.yaak.Yaak.metainfo.xml diff --git a/flatpak/fix-lockfile.mjs b/flatpak/fix-lockfile.mjs index aa3826de..e8adc89c 100644 --- a/flatpak/fix-lockfile.mjs +++ b/flatpak/fix-lockfile.mjs @@ -47,7 +47,9 @@ let changesMade = false; async function fillAllResolved(packages) { for (const packagePath in packages) { if (packagePath === "") continue; + if (!packagePath.includes("node_modules/")) continue; const p = packages[packagePath]; + if (p.link) continue; if (!p.inBundle && !p.bundled && (!p.resolved || !p.integrity)) { const packageName = p.name || From 68b2ff016fd90109735c52b881ee8b23fd1491eb Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 23:24:09 -0800 Subject: [PATCH 10/29] CI: rewrite metainfo paths for Flathub repo --- .github/workflows/flathub.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml index 30005891..55e0d8ad 100644 --- a/.github/workflows/flathub.yml +++ b/.github/workflows/flathub.yml @@ -58,6 +58,8 @@ jobs: cp flatpak/node-sources.json flathub-repo/ cp LICENSE flathub-repo/ sed -i 's|path: \.\./LICENSE|path: LICENSE|' flathub-repo/app.yaak.Yaak.yml + sed -i 's|install -Dm644 flatpak/app.yaak.Yaak.metainfo.xml|install -Dm644 app.yaak.Yaak.metainfo.xml|' flathub-repo/app.yaak.Yaak.yml + sed -i '/path: LICENSE/a\ # Metainfo file (with release history)\n - type: file\n path: app.yaak.Yaak.metainfo.xml' flathub-repo/app.yaak.Yaak.yml - name: Commit and push to Flathub working-directory: flathub-repo From f265b7a572046b0c2a859999cfb6a6dd9577b8c9 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 23:26:22 -0800 Subject: [PATCH 11/29] Simplify CI: metainfo releases only accumulate in Flathub repo - Remove metainfo update from update-manifest.sh - Remove CI step that committed metainfo back to app repo - Revert permissions back to read-only - CI now inserts release entry directly into Flathub repo's metainfo --- .github/workflows/flathub.yml | 17 ++++++----------- flatpak/update-manifest.sh | 8 -------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml index 55e0d8ad..f327f16e 100644 --- a/.github/workflows/flathub.yml +++ b/.github/workflows/flathub.yml @@ -4,7 +4,7 @@ on: types: [published] permissions: - contents: write + contents: read jobs: update-flathub: @@ -34,15 +34,6 @@ jobs: - name: Run update-manifest.sh run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" - - name: Commit metainfo update to app repo - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add flatpak/app.yaak.Yaak.metainfo.xml - git diff --cached --quiet && echo "No metainfo changes" && exit 0 - git commit -m "Add ${{ github.event.release.tag_name }} to metainfo releases" - git push origin HEAD:main - - name: Checkout Flathub repo uses: actions/checkout@v4 with: @@ -51,15 +42,19 @@ jobs: path: flathub-repo - name: Copy updated files to Flathub repo + env: + VERSION: ${{ github.event.release.tag_name }} run: | cp flatpak/app.yaak.Yaak.yml flathub-repo/ - cp flatpak/app.yaak.Yaak.metainfo.xml flathub-repo/ cp flatpak/cargo-sources.json flathub-repo/ cp flatpak/node-sources.json flathub-repo/ cp LICENSE flathub-repo/ + # Rewrite paths for Flathub repo flat structure sed -i 's|path: \.\./LICENSE|path: LICENSE|' flathub-repo/app.yaak.Yaak.yml sed -i 's|install -Dm644 flatpak/app.yaak.Yaak.metainfo.xml|install -Dm644 app.yaak.Yaak.metainfo.xml|' flathub-repo/app.yaak.Yaak.yml sed -i '/path: LICENSE/a\ # Metainfo file (with release history)\n - type: file\n path: app.yaak.Yaak.metainfo.xml' flathub-repo/app.yaak.Yaak.yml + # Add new release to Flathub metainfo (accumulates over time) + sed -i "s| | \n |" flathub-repo/app.yaak.Yaak.metainfo.xml - name: Commit and push to Flathub working-directory: flathub-repo diff --git a/flatpak/update-manifest.sh b/flatpak/update-manifest.sh index 1676365c..83ff1ee4 100755 --- a/flatpak/update-manifest.sh +++ b/flatpak/update-manifest.sh @@ -8,14 +8,12 @@ # This script: # 1. Updates the git tag and commit in the manifest # 2. Regenerates cargo-sources.json and node-sources.json from the tagged lockfiles -# 3. Adds a new entry to the metainfo set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" MANIFEST="$SCRIPT_DIR/app.yaak.Yaak.yml" -METAINFO="$SCRIPT_DIR/app.yaak.Yaak.metainfo.xml" if [ $# -lt 1 ]; then echo "Usage: $0 " @@ -76,14 +74,8 @@ node -e " flatpak-node-generator --no-requests-cache \ -o "$SCRIPT_DIR/node-sources.json" npm "$TMPDIR/package-lock.json" -# Update metainfo with new release -TODAY=$(date +%Y-%m-%d) -sed -i "s| | \n |" "$METAINFO" -echo "Updated metainfo with release $VERSION." - echo "" echo "Done! Review the changes:" echo " $MANIFEST" -echo " $METAINFO" echo " $SCRIPT_DIR/cargo-sources.json" echo " $SCRIPT_DIR/node-sources.json" From d253093333f12fabd0fc3780b0f30ee1f106ddf2 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 23:26:52 -0800 Subject: [PATCH 12/29] Revert "Simplify CI: metainfo releases only accumulate in Flathub repo" This reverts commit f265b7a572046b0c2a859999cfb6a6dd9577b8c9. --- .github/workflows/flathub.yml | 17 +++++++++++------ flatpak/update-manifest.sh | 8 ++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml index f327f16e..55e0d8ad 100644 --- a/.github/workflows/flathub.yml +++ b/.github/workflows/flathub.yml @@ -4,7 +4,7 @@ on: types: [published] permissions: - contents: read + contents: write jobs: update-flathub: @@ -34,6 +34,15 @@ jobs: - name: Run update-manifest.sh run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" + - name: Commit metainfo update to app repo + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add flatpak/app.yaak.Yaak.metainfo.xml + git diff --cached --quiet && echo "No metainfo changes" && exit 0 + git commit -m "Add ${{ github.event.release.tag_name }} to metainfo releases" + git push origin HEAD:main + - name: Checkout Flathub repo uses: actions/checkout@v4 with: @@ -42,19 +51,15 @@ jobs: path: flathub-repo - name: Copy updated files to Flathub repo - env: - VERSION: ${{ github.event.release.tag_name }} run: | cp flatpak/app.yaak.Yaak.yml flathub-repo/ + cp flatpak/app.yaak.Yaak.metainfo.xml flathub-repo/ cp flatpak/cargo-sources.json flathub-repo/ cp flatpak/node-sources.json flathub-repo/ cp LICENSE flathub-repo/ - # Rewrite paths for Flathub repo flat structure sed -i 's|path: \.\./LICENSE|path: LICENSE|' flathub-repo/app.yaak.Yaak.yml sed -i 's|install -Dm644 flatpak/app.yaak.Yaak.metainfo.xml|install -Dm644 app.yaak.Yaak.metainfo.xml|' flathub-repo/app.yaak.Yaak.yml sed -i '/path: LICENSE/a\ # Metainfo file (with release history)\n - type: file\n path: app.yaak.Yaak.metainfo.xml' flathub-repo/app.yaak.Yaak.yml - # Add new release to Flathub metainfo (accumulates over time) - sed -i "s| | \n |" flathub-repo/app.yaak.Yaak.metainfo.xml - name: Commit and push to Flathub working-directory: flathub-repo diff --git a/flatpak/update-manifest.sh b/flatpak/update-manifest.sh index 83ff1ee4..1676365c 100755 --- a/flatpak/update-manifest.sh +++ b/flatpak/update-manifest.sh @@ -8,12 +8,14 @@ # This script: # 1. Updates the git tag and commit in the manifest # 2. Regenerates cargo-sources.json and node-sources.json from the tagged lockfiles +# 3. Adds a new entry to the metainfo set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" MANIFEST="$SCRIPT_DIR/app.yaak.Yaak.yml" +METAINFO="$SCRIPT_DIR/app.yaak.Yaak.metainfo.xml" if [ $# -lt 1 ]; then echo "Usage: $0 " @@ -74,8 +76,14 @@ node -e " flatpak-node-generator --no-requests-cache \ -o "$SCRIPT_DIR/node-sources.json" npm "$TMPDIR/package-lock.json" +# Update metainfo with new release +TODAY=$(date +%Y-%m-%d) +sed -i "s| | \n |" "$METAINFO" +echo "Updated metainfo with release $VERSION." + echo "" echo "Done! Review the changes:" echo " $MANIFEST" +echo " $METAINFO" echo " $SCRIPT_DIR/cargo-sources.json" echo " $SCRIPT_DIR/node-sources.json" From adeaaccc4504db3c737334633b08dad3053aa82c Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 23:29:27 -0800 Subject: [PATCH 13/29] Add v2026.2.0 release to metainfo, simplify CI workflow - Metainfo is managed upstream (updated before tagging) - CI no longer modifies metainfo; just copies manifest and sources to Flathub - Flathub manifest installs metainfo from git source - Permissions reverted to read-only --- .github/workflows/flathub.yml | 14 +------------- flatpak/app.yaak.Yaak.metainfo.xml | 1 + 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml index 55e0d8ad..a0f6c21b 100644 --- a/.github/workflows/flathub.yml +++ b/.github/workflows/flathub.yml @@ -4,7 +4,7 @@ on: types: [published] permissions: - contents: write + contents: read jobs: update-flathub: @@ -34,15 +34,6 @@ jobs: - name: Run update-manifest.sh run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" - - name: Commit metainfo update to app repo - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add flatpak/app.yaak.Yaak.metainfo.xml - git diff --cached --quiet && echo "No metainfo changes" && exit 0 - git commit -m "Add ${{ github.event.release.tag_name }} to metainfo releases" - git push origin HEAD:main - - name: Checkout Flathub repo uses: actions/checkout@v4 with: @@ -53,13 +44,10 @@ jobs: - name: Copy updated files to Flathub repo run: | cp flatpak/app.yaak.Yaak.yml flathub-repo/ - cp flatpak/app.yaak.Yaak.metainfo.xml flathub-repo/ cp flatpak/cargo-sources.json flathub-repo/ cp flatpak/node-sources.json flathub-repo/ cp LICENSE flathub-repo/ sed -i 's|path: \.\./LICENSE|path: LICENSE|' flathub-repo/app.yaak.Yaak.yml - sed -i 's|install -Dm644 flatpak/app.yaak.Yaak.metainfo.xml|install -Dm644 app.yaak.Yaak.metainfo.xml|' flathub-repo/app.yaak.Yaak.yml - sed -i '/path: LICENSE/a\ # Metainfo file (with release history)\n - type: file\n path: app.yaak.Yaak.metainfo.xml' flathub-repo/app.yaak.Yaak.yml - name: Commit and push to Flathub working-directory: flathub-repo diff --git a/flatpak/app.yaak.Yaak.metainfo.xml b/flatpak/app.yaak.Yaak.metainfo.xml index 83f52d38..87a9772c 100644 --- a/flatpak/app.yaak.Yaak.metainfo.xml +++ b/flatpak/app.yaak.Yaak.metainfo.xml @@ -52,5 +52,6 @@ + From 935d613959a29cba0b589b71ed0e96a67c5d8beb Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 23:35:14 -0800 Subject: [PATCH 14/29] Move lockfile patch to standalone script --- flatpak/app.yaak.Yaak.yml | 13 ++----------- flatpak/patch-lockfile.cjs | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 flatpak/patch-lockfile.cjs diff --git a/flatpak/app.yaak.Yaak.yml b/flatpak/app.yaak.Yaak.yml index 81c1f68c..7fc72bf7 100644 --- a/flatpak/app.yaak.Yaak.yml +++ b/flatpak/app.yaak.Yaak.yml @@ -64,16 +64,7 @@ modules: # Patch lockfile: add resolved URLs for nested workspace deps that npm # omits (see https://github.com/npm/cli/issues/4460) - - >- - node -e "const fs=require('fs'); - const p='package-lock.json'; - const d=JSON.parse(fs.readFileSync(p,'utf-8')); - for(const[n,info]of Object.entries(d.packages||{})){ - if(!n||info.link||info.resolved||!n.includes('node_modules/')||!info.version)continue; - const pkg=n.split('node_modules/').pop(); - const base=pkg.split('/').pop(); - info.resolved='https://registry.npmjs.org/'+pkg+'/-/'+base+'-'+info.version+'.tgz'; - }fs.writeFileSync(p,JSON.stringify(d,null,2));" + - node flatpak/patch-lockfile.cjs # Install npm dependencies offline - npm ci --offline @@ -114,7 +105,7 @@ modules: - type: git url: https://github.com/mountain-loop/yaak.git tag: v2026.2.0 - commit: 76ee3fa61bebe8a5cc57cb286e8210155f516b75 + commit: adeaaccc4504db3c737334633b08dad3053aa82c x-checker-data: type: git tag-pattern: ^v(\d+\.\d+\.\d+)$ diff --git a/flatpak/patch-lockfile.cjs b/flatpak/patch-lockfile.cjs new file mode 100644 index 00000000..222f1b64 --- /dev/null +++ b/flatpak/patch-lockfile.cjs @@ -0,0 +1,20 @@ +// Adds missing `resolved` URLs to package-lock.json for nested workspace deps. +// npm omits these fields for some packages (see https://github.com/npm/cli/issues/4460), +// which breaks offline installs. This script constructs the URL from the package +// name and version without requiring network access. + +const fs = require("fs"); + +const p = process.argv[2] || "package-lock.json"; +const d = JSON.parse(fs.readFileSync(p, "utf-8")); + +for (const [name, info] of Object.entries(d.packages || {})) { + if (!name || info.link || info.resolved) continue; + if (!name.includes("node_modules/") || !info.version) continue; + const pkg = name.split("node_modules/").pop(); + const base = pkg.split("/").pop(); + info.resolved = + "https://registry.npmjs.org/" + pkg + "/-/" + base + "-" + info.version + ".tgz"; +} + +fs.writeFileSync(p, JSON.stringify(d, null, 2)); From ed13a62269909752b27a6659b9dda6784317f401 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 11 Feb 2026 06:25:32 -0800 Subject: [PATCH 15/29] Use static desktop file and clean up manifest comments --- flatpak/app.yaak.Yaak.yml | 32 ++------------------------------ flatpak/yaak.desktop | 9 +++++++++ 2 files changed, 11 insertions(+), 30 deletions(-) create mode 100644 flatpak/yaak.desktop diff --git a/flatpak/app.yaak.Yaak.yml b/flatpak/app.yaak.Yaak.yml index 7fc72bf7..e4da3202 100644 --- a/flatpak/app.yaak.Yaak.yml +++ b/flatpak/app.yaak.Yaak.yml @@ -53,50 +53,23 @@ modules: SKIP_WASM_BUILD: "1" build-commands: - # Vendor Node.js binary (sidecar for plugin runtime) - mkdir -p crates-tauri/yaak-app/vendored/node - install -Dm755 vendored-node/bin/node crates-tauri/yaak-app/vendored/node/yaaknode - - # Vendor protoc binary and includes - mkdir -p crates-tauri/yaak-app/vendored/protoc - install -Dm755 protoc-bin/protoc crates-tauri/yaak-app/vendored/protoc/yaakprotoc - mkdir -p crates-tauri/yaak-app/vendored/protoc/include && cp -r protoc-bin/google crates-tauri/yaak-app/vendored/protoc/include/google - - # Patch lockfile: add resolved URLs for nested workspace deps that npm - # omits (see https://github.com/npm/cli/issues/4460) - node flatpak/patch-lockfile.cjs - - # Install npm dependencies offline - npm ci --offline - - # Pre-fetch Cargo dependencies offline - cargo --offline fetch --manifest-path Cargo.toml - - # Build all workspace packages (frontend, plugins, wasm, plugin-runtime) - npm run build - - # Copy built plugins to vendored directory - npm run vendor:vendor-plugins - - # Build the Tauri app (cargo build directly to avoid inotify limits from tauri CLI) - cargo build --offline --release -p yaak-app - - # Install binary - install -Dm755 target/release/yaak-app /app/bin/yaak-app - - # Install icons from source - install -Dm644 crates-tauri/yaak-app/icons/release/32x32.png /app/share/icons/hicolor/32x32/apps/yaak-app.png - install -Dm644 crates-tauri/yaak-app/icons/release/64x64.png /app/share/icons/hicolor/64x64/apps/yaak-app.png - install -Dm644 crates-tauri/yaak-app/icons/release/128x128.png /app/share/icons/hicolor/128x128/apps/yaak-app.png - install -Dm644 crates-tauri/yaak-app/icons/release/icon.png /app/share/icons/hicolor/512x512/apps/yaak-app.png - - # Install desktop file - - >- - printf '[Desktop Entry]\nCategories=Development;\nComment=The API client for modern developers\nExec=yaak-app\nIcon=yaak-app\nName=Yaak\nStartupWMClass=yaak\nTerminal=false\nType=Application\n' - > yaak.desktop - - install -Dm644 yaak.desktop /app/share/applications/yaak.desktop - - # Install metainfo and license + - install -Dm644 flatpak/yaak.desktop /app/share/applications/yaak.desktop - install -Dm644 flatpak/app.yaak.Yaak.metainfo.xml /app/share/metainfo/app.yaak.Yaak.metainfo.xml - install -Dm644 LICENSE /app/share/licenses/app.yaak.Yaak/LICENSE @@ -105,7 +78,7 @@ modules: - type: git url: https://github.com/mountain-loop/yaak.git tag: v2026.2.0 - commit: adeaaccc4504db3c737334633b08dad3053aa82c + commit: bb1b1c2f15b85427fe057e17d98849fa8a5bb836 x-checker-data: type: git tag-pattern: ^v(\d+\.\d+\.\d+)$ @@ -153,4 +126,3 @@ modules: # License file - type: file path: ../LICENSE - diff --git a/flatpak/yaak.desktop b/flatpak/yaak.desktop new file mode 100644 index 00000000..0b7f0736 --- /dev/null +++ b/flatpak/yaak.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Categories=Development; +Comment=The API client for modern developers +Exec=yaak-app +Icon=yaak-app +Name=Yaak +StartupWMClass=yaak +Terminal=false +Type=Application From 510d1c7d17de6be5e7048584b259c7842d5364d9 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 11 Feb 2026 06:32:39 -0800 Subject: [PATCH 16/29] Remove Flatpak manifest (lives in Flathub repo) --- flatpak/app.yaak.Yaak.yml | 128 -------------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 flatpak/app.yaak.Yaak.yml diff --git a/flatpak/app.yaak.Yaak.yml b/flatpak/app.yaak.Yaak.yml deleted file mode 100644 index e4da3202..00000000 --- a/flatpak/app.yaak.Yaak.yml +++ /dev/null @@ -1,128 +0,0 @@ -id: app.yaak.Yaak -runtime: org.gnome.Platform -runtime-version: "49" -sdk: org.gnome.Sdk -command: yaak-app - -sdk-extensions: - - org.freedesktop.Sdk.Extension.node22 - - org.freedesktop.Sdk.Extension.rust-stable - -rename-desktop-file: yaak.desktop -rename-icon: yaak-app - -finish-args: - - --socket=wayland - - --socket=fallback-x11 - - --share=ipc - - --device=dri - - --share=network - - --socket=pulseaudio # Preview audio responses - - --socket=ssh-auth # Git SSH remotes - - --socket=gpg-agent # Git commit signing - - --talk-name=org.freedesktop.secrets # Keyring for encryption - - --filesystem=home # Git repos, ~/.gitconfig, ~/.ssh, etc - -modules: - - name: git - cleanup: - - /share - make-args: - - NO_PERL=1 - - NO_TCLTK=1 - make-install-args: - - INSTALL_SYMLINKS=1 - - NO_PERL=1 - - NO_TCLTK=1 - sources: - - type: archive - url: https://www.kernel.org/pub/software/scm/git/git-2.48.1.tar.gz - sha256: 51b4d03b1e311ba673591210f94f24a4c5781453e1eb188822e3d9cdc04c2212 - - - name: yaak - buildsystem: simple - build-options: - append-path: /app/bin:/usr/lib/sdk/node22/bin:/usr/lib/sdk/rust-stable/bin - env: - CARGO_HOME: /run/build/yaak/cargo - XDG_CACHE_HOME: /run/build/yaak/flatpak-node/cache - npm_config_cache: /run/build/yaak/flatpak-node/npm-cache - npm_config_offline: "true" - npm_config_nodedir: /usr/lib/sdk/node22 - NODE_OPTIONS: --max_old_space_size=4096 - SKIP_WASM_BUILD: "1" - - build-commands: - - mkdir -p crates-tauri/yaak-app/vendored/node - - install -Dm755 vendored-node/bin/node crates-tauri/yaak-app/vendored/node/yaaknode - - mkdir -p crates-tauri/yaak-app/vendored/protoc - - install -Dm755 protoc-bin/protoc crates-tauri/yaak-app/vendored/protoc/yaakprotoc - - mkdir -p crates-tauri/yaak-app/vendored/protoc/include && cp -r protoc-bin/google crates-tauri/yaak-app/vendored/protoc/include/google - - node flatpak/patch-lockfile.cjs - - npm ci --offline - - cargo --offline fetch --manifest-path Cargo.toml - - npm run build - - npm run vendor:vendor-plugins - - cargo build --offline --release -p yaak-app - - install -Dm755 target/release/yaak-app /app/bin/yaak-app - - install -Dm644 crates-tauri/yaak-app/icons/release/32x32.png /app/share/icons/hicolor/32x32/apps/yaak-app.png - - install -Dm644 crates-tauri/yaak-app/icons/release/64x64.png /app/share/icons/hicolor/64x64/apps/yaak-app.png - - install -Dm644 crates-tauri/yaak-app/icons/release/128x128.png /app/share/icons/hicolor/128x128/apps/yaak-app.png - - install -Dm644 crates-tauri/yaak-app/icons/release/icon.png /app/share/icons/hicolor/512x512/apps/yaak-app.png - - install -Dm644 flatpak/yaak.desktop /app/share/applications/yaak.desktop - - install -Dm644 flatpak/app.yaak.Yaak.metainfo.xml /app/share/metainfo/app.yaak.Yaak.metainfo.xml - - install -Dm644 LICENSE /app/share/licenses/app.yaak.Yaak/LICENSE - - sources: - # Application source - - type: git - url: https://github.com/mountain-loop/yaak.git - tag: v2026.2.0 - commit: bb1b1c2f15b85427fe057e17d98849fa8a5bb836 - x-checker-data: - type: git - tag-pattern: ^v(\d+\.\d+\.\d+)$ - - # Offline npm dependencies - - node-sources.json - - # Offline Cargo dependencies - - cargo-sources.json - - # Vendored Node.js binary (x86_64) - - type: archive - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-x64.tar.gz - sha256: 58a5ff5cc8f2200e458bea22e329d5c1994aa1b111d499ca46ec2411d58239ca - strip-components: 1 - dest: vendored-node - only-arches: - - x86_64 - - # Vendored Node.js binary (aarch64) - - type: archive - url: https://nodejs.org/download/release/v24.11.1/node-v24.11.1-linux-arm64.tar.gz - sha256: 0dc93ec5c798b0d347f068db6d205d03dea9a71765e6a53922b682b91265d71f - strip-components: 1 - dest: vendored-node - only-arches: - - aarch64 - - # Vendored protoc binary and includes (x86_64) - - type: archive - url: https://github.com/protocolbuffers/protobuf/releases/download/v33.1/protoc-33.1-linux-x86_64.zip - sha256: f3340e28a83d1c637d8bafdeed92b9f7db6a384c26bca880a6e5217b40a4328b - dest: protoc-bin - only-arches: - - x86_64 - - # Vendored protoc binary and includes (aarch64) - - type: archive - url: https://github.com/protocolbuffers/protobuf/releases/download/v33.1/protoc-33.1-linux-aarch_64.zip - sha256: 6018147740548e0e0f764408c87f4cd040e6e1c1203e13aeacaf811892b604f3 - dest: protoc-bin - only-arches: - - aarch64 - - # License file - - type: file - path: ../LICENSE From b64b5ec0f8cf0510f141262ed135d57b5031ae81 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Tue, 10 Feb 2026 14:03:31 -0800 Subject: [PATCH 17/29] Refresh Git dropdown data on open and fetch periodically - Add refreshKey to useGit queries so dropdown data refreshes on open - Convert fetchAll from mutation to query with 10-minute refetch interval - Re-run status query after fetchAll completes via dataUpdatedAt key - Use placeholderData to keep previous data during key changes - Remove disabled state from Push, Pull, and Commit menu items --- crates/yaak-git/index.ts | 21 ++++++++++++-------- src-web/components/git/GitDropdown.tsx | 27 ++++++++++++++++++-------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/crates/yaak-git/index.ts b/crates/yaak-git/index.ts index b90fa99b..b08d838b 100644 --- a/crates/yaak-git/index.ts +++ b/crates/yaak-git/index.ts @@ -32,22 +32,30 @@ export interface GitCallbacks { const onSuccess = () => queryClient.invalidateQueries({ queryKey: ['git'] }); -export function useGit(dir: string, callbacks: GitCallbacks) { +export function useGit(dir: string, callbacks: GitCallbacks, refreshKey?: string) { const mutations = useMemo(() => gitMutations(dir, callbacks), [dir, callbacks]); + const fetchAll = useQuery({ + queryKey: ['git', 'fetch_all', dir, refreshKey], + queryFn: () => invoke('cmd_git_fetch_all', { dir }), + refetchInterval: 10 * 60_000, + }); return [ { remotes: useQuery({ - queryKey: ['git', 'remotes', dir], + queryKey: ['git', 'remotes', dir, refreshKey], queryFn: () => getRemotes(dir), + placeholderData: (prev) => prev, }), log: useQuery({ - queryKey: ['git', 'log', dir], + queryKey: ['git', 'log', dir, refreshKey], queryFn: () => invoke('cmd_git_log', { dir }), + placeholderData: (prev) => prev, }), status: useQuery({ refetchOnMount: true, - queryKey: ['git', 'status', dir], + queryKey: ['git', 'status', dir, refreshKey, fetchAll.dataUpdatedAt], queryFn: () => invoke('cmd_git_status', { dir }), + placeholderData: (prev) => prev, }), }, mutations, @@ -152,10 +160,7 @@ export const gitMutations = (dir: string, callbacks: GitCallbacks) => { }, onSuccess, }), - fetchAll: createFastMutation({ - mutationKey: ['git', 'fetch_all', dir], - mutationFn: () => invoke('cmd_git_fetch_all', { dir }), - }), + push: createFastMutation({ mutationKey: ['git', 'push', dir], mutationFn: push, diff --git a/src-web/components/git/GitDropdown.tsx b/src-web/components/git/GitDropdown.tsx index 1915f6ec..563fef47 100644 --- a/src-web/components/git/GitDropdown.tsx +++ b/src-web/components/git/GitDropdown.tsx @@ -7,6 +7,7 @@ import { forwardRef } from 'react'; import { openWorkspaceSettings } from '../../commands/openWorkspaceSettings'; import { activeWorkspaceAtom, activeWorkspaceMetaAtom } from '../../hooks/useActiveWorkspace'; import { useKeyValue } from '../../hooks/useKeyValue'; +import { useRandomKey } from '../../hooks/useRandomKey'; import { sync } from '../../init/sync'; import { showConfirm, showConfirmDelete } from '../../lib/confirm'; import { showDialog } from '../../lib/dialog'; @@ -36,6 +37,7 @@ export function GitDropdown() { function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { const workspace = useAtomValue(activeWorkspaceAtom); + const [refreshKey, regenerateKey] = useRandomKey(); const [ { status, log }, { @@ -43,7 +45,6 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { deleteBranch, deleteRemoteBranch, renameBranch, - fetchAll, mergeBranch, push, pull, @@ -51,7 +52,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { resetChanges, init, }, - ] = useGit(syncDir, gitCallbacks(syncDir)); + ] = useGit(syncDir, gitCallbacks(syncDir), refreshKey); const localBranches = status.data?.localBranches ?? []; const remoteBranches = status.data?.remoteBranches ?? []; @@ -172,7 +173,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { { type: 'separator' }, { label: 'Push', - disabled: !hasRemotes || ahead === 0, + hidden: !hasRemotes, leftSlot: , waitForOnSelect: true, async onSelect() { @@ -191,7 +192,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { }, { label: 'Pull', - disabled: !hasRemotes || behind === 0, + hidden: !hasRemotes, leftSlot: , waitForOnSelect: true, async onSelect() { @@ -210,7 +211,7 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { }, { label: 'Commit...', - disabled: !hasChanges, + leftSlot: , onSelect() { showDialog({ @@ -502,15 +503,25 @@ function SyncDropdownWithSyncDir({ syncDir }: { syncDir: string }) { ]; return ( - + {currentBranch}
- {ahead > 0 && {ahead}} - {behind > 0 && {behind}} + {ahead > 0 && ( + + + {ahead} + + )} + {behind > 0 && ( + + + {behind} + + )}
From 3e4de7d3c4d25b1d7cc147093a72fe95a0d24545 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 11 Feb 2026 07:28:20 -0800 Subject: [PATCH 18/29] Move build scripts to Flathub repo, keep release prep scripts here --- .github/workflows/flathub.yml | 24 ++++++++---------------- flatpak/generate-sources.sh | 21 +++++++++++++-------- flatpak/patch-lockfile.cjs | 20 -------------------- flatpak/update-manifest.sh | 29 +++++++++++++---------------- flatpak/yaak.desktop | 9 --------- 5 files changed, 34 insertions(+), 69 deletions(-) delete mode 100644 flatpak/patch-lockfile.cjs delete mode 100644 flatpak/yaak.desktop diff --git a/.github/workflows/flathub.yml b/.github/workflows/flathub.yml index a0f6c21b..c8c33f66 100644 --- a/.github/workflows/flathub.yml +++ b/.github/workflows/flathub.yml @@ -16,6 +16,13 @@ jobs: - name: Checkout app repo uses: actions/checkout@v4 + - name: Checkout Flathub repo + uses: actions/checkout@v4 + with: + repository: flathub/app.yaak.Yaak + token: ${{ secrets.FLATHUB_TOKEN }} + path: flathub-repo + - name: Set up Python uses: actions/setup-python@v5 with: @@ -32,22 +39,7 @@ jobs: git clone --depth 1 https://github.com/flatpak/flatpak-builder-tools flatpak/flatpak-builder-tools - name: Run update-manifest.sh - run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" - - - name: Checkout Flathub repo - uses: actions/checkout@v4 - with: - repository: flathub/app.yaak.Yaak - token: ${{ secrets.FLATHUB_TOKEN }} - path: flathub-repo - - - name: Copy updated files to Flathub repo - run: | - cp flatpak/app.yaak.Yaak.yml flathub-repo/ - cp flatpak/cargo-sources.json flathub-repo/ - cp flatpak/node-sources.json flathub-repo/ - cp LICENSE flathub-repo/ - sed -i 's|path: \.\./LICENSE|path: LICENSE|' flathub-repo/app.yaak.Yaak.yml + run: bash flatpak/update-manifest.sh "${{ github.event.release.tag_name }}" flathub-repo - name: Commit and push to Flathub working-directory: flathub-repo diff --git a/flatpak/generate-sources.sh b/flatpak/generate-sources.sh index f656d0aa..216d0a4d 100755 --- a/flatpak/generate-sources.sh +++ b/flatpak/generate-sources.sh @@ -7,20 +7,25 @@ # Clone https://github.com/flatpak/flatpak-builder-tools (for cargo generator) # # Usage: -# ./flatpak/generate-sources.sh +# ./flatpak/generate-sources.sh +# ./flatpak/generate-sources.sh ../flathub-repo set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -# Generate cargo-sources.json -python3 "$SCRIPT_DIR/flatpak-builder-tools/cargo/flatpak-cargo-generator.py" \ - -o "$SCRIPT_DIR/cargo-sources.json" "$REPO_ROOT/Cargo.lock" +if [ $# -lt 1 ]; then + echo "Usage: $0 " + echo "Example: $0 ../flathub-repo" + exit 1 +fi + +FLATHUB_REPO="$(cd "$1" && pwd)" + +python3 "$SCRIPT_DIR/flatpak-builder-tools/cargo/flatpak-cargo-generator.py" \ + -o "$FLATHUB_REPO/cargo-sources.json" "$REPO_ROOT/Cargo.lock" -# Generate node-sources.json from a patched copy of the lockfile. -# npm omits resolved/integrity for some workspace deps, and -# flatpak-node-generator can't handle workspace link entries. TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT @@ -40,4 +45,4 @@ node -e " " "$TMPDIR/package-lock.json" flatpak-node-generator --no-requests-cache \ - -o "$SCRIPT_DIR/node-sources.json" npm "$TMPDIR/package-lock.json" + -o "$FLATHUB_REPO/node-sources.json" npm "$TMPDIR/package-lock.json" diff --git a/flatpak/patch-lockfile.cjs b/flatpak/patch-lockfile.cjs deleted file mode 100644 index 222f1b64..00000000 --- a/flatpak/patch-lockfile.cjs +++ /dev/null @@ -1,20 +0,0 @@ -// Adds missing `resolved` URLs to package-lock.json for nested workspace deps. -// npm omits these fields for some packages (see https://github.com/npm/cli/issues/4460), -// which breaks offline installs. This script constructs the URL from the package -// name and version without requiring network access. - -const fs = require("fs"); - -const p = process.argv[2] || "package-lock.json"; -const d = JSON.parse(fs.readFileSync(p, "utf-8")); - -for (const [name, info] of Object.entries(d.packages || {})) { - if (!name || info.link || info.resolved) continue; - if (!name.includes("node_modules/") || !info.version) continue; - const pkg = name.split("node_modules/").pop(); - const base = pkg.split("/").pop(); - info.resolved = - "https://registry.npmjs.org/" + pkg + "/-/" + base + "-" + info.version + ".tgz"; -} - -fs.writeFileSync(p, JSON.stringify(d, null, 2)); diff --git a/flatpak/update-manifest.sh b/flatpak/update-manifest.sh index 1676365c..8ad59f70 100755 --- a/flatpak/update-manifest.sh +++ b/flatpak/update-manifest.sh @@ -1,30 +1,27 @@ #!/usr/bin/env bash # -# Update the Flatpak manifest for a new release. +# Update the Flathub repo for a new release. # # Usage: -# ./flatpak/update-manifest.sh v2026.2.0 -# -# This script: -# 1. Updates the git tag and commit in the manifest -# 2. Regenerates cargo-sources.json and node-sources.json from the tagged lockfiles -# 3. Adds a new entry to the metainfo +# ./flatpak/update-manifest.sh +# ./flatpak/update-manifest.sh v2026.2.0 ../flathub-repo set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -MANIFEST="$SCRIPT_DIR/app.yaak.Yaak.yml" -METAINFO="$SCRIPT_DIR/app.yaak.Yaak.metainfo.xml" -if [ $# -lt 1 ]; then - echo "Usage: $0 " - echo "Example: $0 v2026.2.0" +if [ $# -lt 2 ]; then + echo "Usage: $0 " + echo "Example: $0 v2026.2.0 ../flathub-repo" exit 1 fi VERSION_TAG="$1" VERSION="${VERSION_TAG#v}" +FLATHUB_REPO="$(cd "$2" && pwd)" +MANIFEST="$FLATHUB_REPO/app.yaak.Yaak.yml" +METAINFO="$SCRIPT_DIR/app.yaak.Yaak.metainfo.xml" if [[ "$VERSION" == *-* ]]; then echo "Skipping pre-release version '$VERSION_TAG' (only stable releases are published to Flathub)" @@ -58,7 +55,7 @@ curl -fsSL "https://raw.githubusercontent.com/$REPO/$VERSION_TAG/package.json" - echo "Generating cargo-sources.json..." python3 "$SCRIPT_DIR/flatpak-builder-tools/cargo/flatpak-cargo-generator.py" \ - -o "$SCRIPT_DIR/cargo-sources.json" "$TMPDIR/Cargo.lock" + -o "$FLATHUB_REPO/cargo-sources.json" "$TMPDIR/Cargo.lock" echo "Generating node-sources.json..." node "$SCRIPT_DIR/fix-lockfile.mjs" "$TMPDIR/package-lock.json" @@ -74,7 +71,7 @@ node -e " " "$TMPDIR/package-lock.json" flatpak-node-generator --no-requests-cache \ - -o "$SCRIPT_DIR/node-sources.json" npm "$TMPDIR/package-lock.json" + -o "$FLATHUB_REPO/node-sources.json" npm "$TMPDIR/package-lock.json" # Update metainfo with new release TODAY=$(date +%Y-%m-%d) @@ -85,5 +82,5 @@ echo "" echo "Done! Review the changes:" echo " $MANIFEST" echo " $METAINFO" -echo " $SCRIPT_DIR/cargo-sources.json" -echo " $SCRIPT_DIR/node-sources.json" +echo " $FLATHUB_REPO/cargo-sources.json" +echo " $FLATHUB_REPO/node-sources.json" diff --git a/flatpak/yaak.desktop b/flatpak/yaak.desktop deleted file mode 100644 index 0b7f0736..00000000 --- a/flatpak/yaak.desktop +++ /dev/null @@ -1,9 +0,0 @@ -[Desktop Entry] -Categories=Development; -Comment=The API client for modern developers -Exec=yaak-app -Icon=yaak-app -Name=Yaak -StartupWMClass=yaak -Terminal=false -Type=Application From 9a1d6130346454a9eb07388e65f26076f2e0f59b Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 11 Feb 2026 08:14:16 -0800 Subject: [PATCH 19/29] Fix RPM bundle path validation for metainfo file --- crates-tauri/yaak-app/tauri.release.conf.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates-tauri/yaak-app/tauri.release.conf.json b/crates-tauri/yaak-app/tauri.release.conf.json index a6b4ebfa..0f6151a0 100644 --- a/crates-tauri/yaak-app/tauri.release.conf.json +++ b/crates-tauri/yaak-app/tauri.release.conf.json @@ -46,13 +46,13 @@ "deb": { "desktopTemplate": "./template.desktop", "files": { - "usr/share/metainfo/app.yaak.Yaak.metainfo.xml": "../../flatpak/app.yaak.Yaak.metainfo.xml" + "/usr/share/metainfo/app.yaak.Yaak.metainfo.xml": "../../flatpak/app.yaak.Yaak.metainfo.xml" } }, "rpm": { "desktopTemplate": "./template.desktop", "files": { - "usr/share/metainfo/app.yaak.Yaak.metainfo.xml": "../../flatpak/app.yaak.Yaak.metainfo.xml" + "/usr/share/metainfo/app.yaak.Yaak.metainfo.xml": "../../flatpak/app.yaak.Yaak.metainfo.xml" } } } From 26aba6034fe43eb0f2f5bfde6e41896a71bbae88 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 11 Feb 2026 10:21:55 -0800 Subject: [PATCH 20/29] Move faker plugin back to external (plugins-external/faker) --- package-lock.json | 58 +++++++++---------- package.json | 2 +- .../faker}/README.md | 0 .../faker}/package.json | 2 +- .../faker}/src/index.ts | 0 .../tests/__snapshots__/init.test.ts.snap | 0 .../faker}/tests/init.test.ts | 0 .../faker}/tsconfig.json | 0 8 files changed, 31 insertions(+), 31 deletions(-) rename {plugins/template-function-faker => plugins-external/faker}/README.md (100%) rename {plugins/template-function-faker => plugins-external/faker}/package.json (91%) rename {plugins/template-function-faker => plugins-external/faker}/src/index.ts (100%) rename {plugins/template-function-faker => plugins-external/faker}/tests/__snapshots__/init.test.ts.snap (100%) rename {plugins/template-function-faker => plugins-external/faker}/tests/init.test.ts (100%) rename {plugins/template-function-faker => plugins-external/faker}/tsconfig.json (100%) diff --git a/package-lock.json b/package-lock.json index 1946700b..a6112490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "packages/plugin-runtime", "packages/plugin-runtime-types", "plugins-external/mcp-server", - "plugins/template-function-faker", + "plugins-external/faker", "plugins-external/httpsnippet", "plugins/action-copy-curl", "plugins/action-copy-grpcurl", @@ -4154,7 +4154,7 @@ "link": true }, "node_modules/@yaak/faker": { - "resolved": "plugins/template-function-faker", + "resolved": "plugins-external/faker", "link": true }, "node_modules/@yaak/filter-jsonpath": { @@ -15957,6 +15957,33 @@ "undici-types": "~7.16.0" } }, + "plugins-external/faker": { + "name": "@yaak/faker", + "version": "1.1.1", + "dependencies": { + "@faker-js/faker": "^10.1.0" + }, + "devDependencies": { + "@types/node": "^25.0.3", + "typescript": "^5.9.3" + } + }, + "plugins-external/faker/node_modules/@faker-js/faker": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.3.0.tgz", + "integrity": "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, "plugins-external/httpsnippet": { "name": "@yaak/httpsnippet", "version": "1.0.3", @@ -16139,33 +16166,6 @@ "name": "@yaak/template-function-encode", "version": "0.1.0" }, - "plugins/template-function-faker": { - "name": "@yaak/faker", - "version": "1.1.1", - "dependencies": { - "@faker-js/faker": "^10.1.0" - }, - "devDependencies": { - "@types/node": "^25.0.3", - "typescript": "^5.9.3" - } - }, - "plugins/template-function-faker/node_modules/@faker-js/faker": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.3.0.tgz", - "integrity": "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", - "npm": ">=10" - } - }, "plugins/template-function-fs": { "name": "@yaak/template-function-fs", "version": "0.1.0" diff --git a/package.json b/package.json index 3a9d76df..2f03a786 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "packages/plugin-runtime", "packages/plugin-runtime-types", "plugins-external/mcp-server", - "plugins/template-function-faker", + "plugins-external/faker", "plugins-external/httpsnippet", "plugins/action-copy-curl", "plugins/action-copy-grpcurl", diff --git a/plugins/template-function-faker/README.md b/plugins-external/faker/README.md similarity index 100% rename from plugins/template-function-faker/README.md rename to plugins-external/faker/README.md diff --git a/plugins/template-function-faker/package.json b/plugins-external/faker/package.json similarity index 91% rename from plugins/template-function-faker/package.json rename to plugins-external/faker/package.json index 5e281288..b5d00a7e 100755 --- a/plugins/template-function-faker/package.json +++ b/plugins-external/faker/package.json @@ -7,7 +7,7 @@ "repository": { "type": "git", "url": "https://github.com/mountain-loop/yaak.git", - "directory": "plugins/template-function-faker" + "directory": "plugins-external/faker" }, "scripts": { "build": "yaakcli build", diff --git a/plugins/template-function-faker/src/index.ts b/plugins-external/faker/src/index.ts similarity index 100% rename from plugins/template-function-faker/src/index.ts rename to plugins-external/faker/src/index.ts diff --git a/plugins/template-function-faker/tests/__snapshots__/init.test.ts.snap b/plugins-external/faker/tests/__snapshots__/init.test.ts.snap similarity index 100% rename from plugins/template-function-faker/tests/__snapshots__/init.test.ts.snap rename to plugins-external/faker/tests/__snapshots__/init.test.ts.snap diff --git a/plugins/template-function-faker/tests/init.test.ts b/plugins-external/faker/tests/init.test.ts similarity index 100% rename from plugins/template-function-faker/tests/init.test.ts rename to plugins-external/faker/tests/init.test.ts diff --git a/plugins/template-function-faker/tsconfig.json b/plugins-external/faker/tsconfig.json similarity index 100% rename from plugins/template-function-faker/tsconfig.json rename to plugins-external/faker/tsconfig.json From 565e053ee8d19876e6db714d0d7e61e0b5c49fde Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 11 Feb 2026 14:59:17 -0800 Subject: [PATCH 21/29] Fix auth tab crash when template rendering fails (#392) --- crates-tauri/yaak-app/src/lib.rs | 9 +++++++-- crates/yaak-templates/src/renderer.rs | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates-tauri/yaak-app/src/lib.rs b/crates-tauri/yaak-app/src/lib.rs index 2b1710cf..ceb6c309 100644 --- a/crates-tauri/yaak-app/src/lib.rs +++ b/crates-tauri/yaak-app/src/lib.rs @@ -1095,8 +1095,13 @@ async fn cmd_get_http_authentication_config( // Convert HashMap to serde_json::Value for rendering let values_json: serde_json::Value = serde_json::to_value(&values)?; - let rendered_json = - render_json_value(values_json, environment_chain, &cb, &RenderOptions::throw()).await?; + let rendered_json = render_json_value( + values_json, + environment_chain, + &cb, + &RenderOptions::return_empty(), + ) + .await?; // Convert back to HashMap let rendered_values: HashMap = serde_json::from_value(rendered_json)?; diff --git a/crates/yaak-templates/src/renderer.rs b/crates/yaak-templates/src/renderer.rs index 495e1120..42fd0029 100644 --- a/crates/yaak-templates/src/renderer.rs +++ b/crates/yaak-templates/src/renderer.rs @@ -81,6 +81,10 @@ impl RenderOptions { pub fn throw() -> Self { Self { error_behavior: RenderErrorBehavior::Throw } } + + pub fn return_empty() -> Self { + Self { error_behavior: RenderErrorBehavior::ReturnEmpty } + } } impl RenderErrorBehavior { From 7d4d228236b58ee2a5a893456cd261f5e47c5e97 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 11 Feb 2026 17:11:35 -0800 Subject: [PATCH 22/29] Fix HTTP/2 requests failing with duplicate Content-Length (#391) Co-authored-by: Claude Opus 4.6 --- Cargo.lock | 1 + crates-tauri/yaak-app/src/http_request.rs | 4 +- crates/yaak-http/Cargo.toml | 1 + crates/yaak-http/src/sender.rs | 61 +++++++++++++++- crates/yaak-http/src/types.rs | 89 ++++++++++------------- 5 files changed, 98 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e86ab19..6897229a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8167,6 +8167,7 @@ dependencies = [ "cookie", "flate2", "futures-util", + "http-body", "hyper-util", "log", "mime_guess", diff --git a/crates-tauri/yaak-app/src/http_request.rs b/crates-tauri/yaak-app/src/http_request.rs index d2e060aa..4618bc87 100644 --- a/crates-tauri/yaak-app/src/http_request.rs +++ b/crates-tauri/yaak-app/src/http_request.rs @@ -414,7 +414,7 @@ async fn execute_transaction( sendable_request.body = Some(SendableBody::Bytes(bytes)); None } - Some(SendableBody::Stream(stream)) => { + Some(SendableBody::Stream { data: stream, content_length }) => { // Wrap stream with TeeReader to capture data as it's read // Use unbounded channel to ensure all data is captured without blocking the HTTP request let (body_chunk_tx, body_chunk_rx) = tokio::sync::mpsc::unbounded_channel::>(); @@ -448,7 +448,7 @@ async fn execute_transaction( None }; - sendable_request.body = Some(SendableBody::Stream(pinned)); + sendable_request.body = Some(SendableBody::Stream { data: pinned, content_length }); handle } None => { diff --git a/crates/yaak-http/Cargo.toml b/crates/yaak-http/Cargo.toml index 1df3b31e..20183503 100644 --- a/crates/yaak-http/Cargo.toml +++ b/crates/yaak-http/Cargo.toml @@ -12,6 +12,7 @@ bytes = "1.11.1" cookie = "0.18.1" flate2 = "1" futures-util = "0.3" +http-body = "1" url = "2" zstd = "0.13" hyper-util = { version = "0.1.17", default-features = false, features = ["client-legacy"] } diff --git a/crates/yaak-http/src/sender.rs b/crates/yaak-http/src/sender.rs index 940d0f31..911f2338 100644 --- a/crates/yaak-http/src/sender.rs +++ b/crates/yaak-http/src/sender.rs @@ -2,7 +2,9 @@ use crate::decompress::{ContentEncoding, streaming_decoder}; use crate::error::{Error, Result}; use crate::types::{SendableBody, SendableHttpRequest}; use async_trait::async_trait; +use bytes::Bytes; use futures_util::StreamExt; +use http_body::{Body as HttpBody, Frame, SizeHint}; use reqwest::{Client, Method, Version}; use std::fmt::Display; use std::pin::Pin; @@ -413,10 +415,16 @@ impl HttpSender for ReqwestSender { Some(SendableBody::Bytes(bytes)) => { req_builder = req_builder.body(bytes); } - Some(SendableBody::Stream(stream)) => { - // Convert AsyncRead stream to reqwest Body - let stream = tokio_util::io::ReaderStream::new(stream); - let body = reqwest::Body::wrap_stream(stream); + Some(SendableBody::Stream { data, content_length }) => { + // Convert AsyncRead stream to reqwest Body. If content length is + // known, wrap with a SizedBody so hyper can set Content-Length + // automatically (for both HTTP/1.1 and HTTP/2). + let stream = tokio_util::io::ReaderStream::new(data); + let body = if let Some(len) = content_length { + reqwest::Body::wrap(SizedBody::new(stream, len)) + } else { + reqwest::Body::wrap_stream(stream) + }; req_builder = req_builder.body(body); } } @@ -520,6 +528,51 @@ impl HttpSender for ReqwestSender { } } +/// A wrapper around a byte stream that reports a known content length via +/// `size_hint()`. This lets hyper set the `Content-Length` header +/// automatically based on the body size, without us having to add it as an +/// explicit header — which can cause duplicate `Content-Length` headers and +/// break HTTP/2. +struct SizedBody { + stream: std::sync::Mutex, + remaining: u64, +} + +impl SizedBody { + fn new(stream: S, content_length: u64) -> Self { + Self { stream: std::sync::Mutex::new(stream), remaining: content_length } + } +} + +impl HttpBody for SizedBody +where + S: futures_util::Stream> + Send + Unpin + 'static, +{ + type Data = Bytes; + type Error = std::io::Error; + + fn poll_frame( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + let this = self.get_mut(); + let mut stream = this.stream.lock().unwrap(); + match stream.poll_next_unpin(cx) { + Poll::Ready(Some(Ok(chunk))) => { + this.remaining = this.remaining.saturating_sub(chunk.len() as u64); + Poll::Ready(Some(Ok(Frame::data(chunk)))) + } + Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } + + fn size_hint(&self) -> SizeHint { + SizeHint::with_exact(self.remaining) + } +} + fn version_to_str(version: &Version) -> String { match *version { Version::HTTP_09 => "HTTP/0.9".to_string(), diff --git a/crates/yaak-http/src/types.rs b/crates/yaak-http/src/types.rs index 135ce8bb..aee41313 100644 --- a/crates/yaak-http/src/types.rs +++ b/crates/yaak-http/src/types.rs @@ -16,7 +16,13 @@ pub(crate) const MULTIPART_BOUNDARY: &str = "------YaakFormBoundary"; pub enum SendableBody { Bytes(Bytes), - Stream(Pin>), + Stream { + data: Pin>, + /// Known content length for the stream, if available. This is used by + /// the sender to set the body size hint so that hyper can set + /// Content-Length automatically for both HTTP/1.1 and HTTP/2. + content_length: Option, + }, } enum SendableBodyWithMeta { @@ -31,7 +37,10 @@ impl From for SendableBody { fn from(value: SendableBodyWithMeta) -> Self { match value { SendableBodyWithMeta::Bytes(b) => SendableBody::Bytes(b), - SendableBodyWithMeta::Stream { data, .. } => SendableBody::Stream(data), + SendableBodyWithMeta::Stream { data, content_length } => SendableBody::Stream { + data, + content_length: content_length.map(|l| l as u64), + }, } } } @@ -186,23 +195,11 @@ async fn build_body( } } - // Check if Transfer-Encoding: chunked is already set - let has_chunked_encoding = headers.iter().any(|h| { - h.0.to_lowercase() == "transfer-encoding" && h.1.to_lowercase().contains("chunked") - }); - - // Add a Content-Length header only if chunked encoding is not being used - if !has_chunked_encoding { - let content_length = match body { - Some(SendableBodyWithMeta::Bytes(ref bytes)) => Some(bytes.len()), - Some(SendableBodyWithMeta::Stream { content_length, .. }) => content_length, - None => None, - }; - - if let Some(cl) = content_length { - headers.push(("Content-Length".to_string(), cl.to_string())); - } - } + // NOTE: Content-Length is NOT set as an explicit header here. Instead, the + // body's content length is carried via SendableBody::Stream { content_length } + // and used by the sender to set the body size hint. This lets hyper handle + // Content-Length automatically for both HTTP/1.1 and HTTP/2, avoiding the + // duplicate Content-Length that breaks HTTP/2 servers. Ok((body.map(|b| b.into()), headers)) } @@ -928,7 +925,27 @@ mod tests { } #[tokio::test] - async fn test_no_content_length_with_chunked_encoding() -> Result<()> { + async fn test_no_content_length_header_added_by_build_body() -> Result<()> { + let mut body = BTreeMap::new(); + body.insert("text".to_string(), json!("Hello, World!")); + + let headers = vec![]; + + let (_, result_headers) = + build_body("POST", &Some("text/plain".to_string()), &body, headers).await?; + + // Content-Length should NOT be set as an explicit header. Instead, the + // sender uses the body's size_hint to let hyper set it automatically, + // which works correctly for both HTTP/1.1 and HTTP/2. + let has_content_length = + result_headers.iter().any(|h| h.0.to_lowercase() == "content-length"); + assert!(!has_content_length, "Content-Length should not be set as an explicit header"); + + Ok(()) + } + + #[tokio::test] + async fn test_chunked_encoding_header_preserved() -> Result<()> { let mut body = BTreeMap::new(); body.insert("text".to_string(), json!("Hello, World!")); @@ -938,11 +955,6 @@ mod tests { let (_, result_headers) = build_body("POST", &Some("text/plain".to_string()), &body, headers).await?; - // Verify that Content-Length is NOT present when Transfer-Encoding: chunked is set - let has_content_length = - result_headers.iter().any(|h| h.0.to_lowercase() == "content-length"); - assert!(!has_content_length, "Content-Length should not be present with chunked encoding"); - // Verify that the Transfer-Encoding header is still present let has_chunked = result_headers.iter().any(|h| { h.0.to_lowercase() == "transfer-encoding" && h.1.to_lowercase().contains("chunked") @@ -951,31 +963,4 @@ mod tests { Ok(()) } - - #[tokio::test] - async fn test_content_length_without_chunked_encoding() -> Result<()> { - let mut body = BTreeMap::new(); - body.insert("text".to_string(), json!("Hello, World!")); - - // Headers without Transfer-Encoding: chunked - let headers = vec![]; - - let (_, result_headers) = - build_body("POST", &Some("text/plain".to_string()), &body, headers).await?; - - // Verify that Content-Length IS present when Transfer-Encoding: chunked is NOT set - let content_length_header = - result_headers.iter().find(|h| h.0.to_lowercase() == "content-length"); - assert!( - content_length_header.is_some(), - "Content-Length should be present without chunked encoding" - ); - assert_eq!( - content_length_header.unwrap().1, - "13", - "Content-Length should match the body size" - ); - - Ok(()) - } } From 1127d7e3fa2546241d524a9fe7c3605add62e052 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 11 Feb 2026 17:38:13 -0800 Subject: [PATCH 23/29] Use consistent release title format in generate-release-notes command --- .claude/commands/release/generate-release-notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.claude/commands/release/generate-release-notes.md b/.claude/commands/release/generate-release-notes.md index 2ca790b5..6a42ea32 100644 --- a/.claude/commands/release/generate-release-notes.md +++ b/.claude/commands/release/generate-release-notes.md @@ -43,5 +43,7 @@ The skill generates markdown-formatted release notes following this structure: After outputting the release notes, ask the user if they would like to create a draft GitHub release with these notes. If they confirm, create the release using: ```bash -gh release create --draft --prerelease --title "" --notes '' +gh release create --draft --prerelease --title "Release " --notes '' ``` + +**IMPORTANT**: The release title format is "Release XXXX" where XXXX is the version WITHOUT the `v` prefix. For example, tag `v2026.2.1-beta.1` gets title "Release 2026.2.1-beta.1". From 52732e12ec54da8c2e946a1784bf54c8675a528a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 12 Feb 2026 14:38:53 -0800 Subject: [PATCH 24/29] Fix license activation and plugin requests ignoring proxy settings (#393) Co-authored-by: Claude Opus 4.6 ( app_handle: AppHandle, query: &str, ) -> Result { - let http_client = yaak_api_client(&app_handle)?; + let app_version = app_handle.package_info().version.to_string(); + let http_client = yaak_api_client(&app_version)?; Ok(search_plugins(&http_client, query).await?) } @@ -147,7 +149,8 @@ pub async fn cmd_plugins_install( version: Option, ) -> Result<()> { let plugin_manager = Arc::new((*window.state::()).clone()); - let http_client = yaak_api_client(window.app_handle())?; + let app_version = window.app_handle().package_info().version.to_string(); + let http_client = yaak_api_client(&app_version)?; let query_manager = window.state::(); let plugin_context = window.plugin_context(); download_and_install( @@ -177,7 +180,8 @@ pub async fn cmd_plugins_uninstall( pub async fn cmd_plugins_updates( app_handle: AppHandle, ) -> Result { - let http_client = yaak_api_client(&app_handle)?; + let app_version = app_handle.package_info().version.to_string(); + let http_client = yaak_api_client(&app_version)?; let plugins = app_handle.db().list_plugins()?; Ok(check_plugin_updates(&http_client, plugins).await?) } @@ -186,7 +190,8 @@ pub async fn cmd_plugins_updates( pub async fn cmd_plugins_update_all( window: WebviewWindow, ) -> Result> { - let http_client = yaak_api_client(window.app_handle())?; + let app_version = window.app_handle().package_info().version.to_string(); + let http_client = yaak_api_client(&app_version)?; let plugins = window.db().list_plugins()?; // Get list of available updates (already filtered to only registry plugins) diff --git a/crates-tauri/yaak-app/src/updates.rs b/crates-tauri/yaak-app/src/updates.rs index b8f64a5a..c068a813 100644 --- a/crates-tauri/yaak-app/src/updates.rs +++ b/crates-tauri/yaak-app/src/updates.rs @@ -15,6 +15,9 @@ use ts_rs::TS; use yaak_models::util::generate_id; use yaak_plugins::manager::PluginManager; +use url::Url; +use yaak_api::get_system_proxy_url; + use crate::error::Error::GenericError; use crate::is_dev; @@ -87,8 +90,13 @@ impl YaakUpdater { info!("Checking for updates mode={} autodl={}", mode, auto_download); let w = window.clone(); - let update_check_result = w - .updater_builder() + let mut updater_builder = w.updater_builder(); + if let Some(proxy_url) = get_system_proxy_url() { + if let Ok(url) = Url::parse(&proxy_url) { + updater_builder = updater_builder.proxy(url); + } + } + let update_check_result = updater_builder .on_before_exit(move || { // Kill plugin manager before exit or NSIS installer will fail to replace sidecar // while it's running. diff --git a/crates-tauri/yaak-app/src/uri_scheme.rs b/crates-tauri/yaak-app/src/uri_scheme.rs index 52119241..d186bbfc 100644 --- a/crates-tauri/yaak-app/src/uri_scheme.rs +++ b/crates-tauri/yaak-app/src/uri_scheme.rs @@ -12,7 +12,7 @@ use yaak_models::util::generate_id; use yaak_plugins::events::{Color, ShowToastRequest}; use yaak_plugins::install::download_and_install; use yaak_plugins::manager::PluginManager; -use yaak_tauri_utils::api_client::yaak_api_client; +use yaak_api::yaak_api_client; pub(crate) async fn handle_deep_link( app_handle: &AppHandle, @@ -46,7 +46,8 @@ pub(crate) async fn handle_deep_link( let plugin_manager = Arc::new((*window.state::()).clone()); let query_manager = app_handle.db_manager(); - let http_client = yaak_api_client(app_handle)?; + let app_version = app_handle.package_info().version.to_string(); + let http_client = yaak_api_client(&app_version)?; let plugin_context = window.plugin_context(); let pv = download_and_install( plugin_manager, @@ -86,7 +87,8 @@ pub(crate) async fn handle_deep_link( return Ok(()); } - let resp = yaak_api_client(app_handle)?.get(file_url).send().await?; + let app_version = app_handle.package_info().version.to_string(); + let resp = yaak_api_client(&app_version)?.get(file_url).send().await?; let json = resp.bytes().await?; let p = app_handle .path() diff --git a/crates-tauri/yaak-license/Cargo.toml b/crates-tauri/yaak-license/Cargo.toml index 3560040a..4eb04489 100644 --- a/crates-tauri/yaak-license/Cargo.toml +++ b/crates-tauri/yaak-license/Cargo.toml @@ -16,7 +16,7 @@ thiserror = { workspace = true } ts-rs = { workspace = true } yaak-common = { workspace = true } yaak-models = { workspace = true } -yaak-tauri-utils = { workspace = true } +yaak-api = { workspace = true } [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } diff --git a/crates-tauri/yaak-license/src/error.rs b/crates-tauri/yaak-license/src/error.rs index 823260fe..99e1292d 100644 --- a/crates-tauri/yaak-license/src/error.rs +++ b/crates-tauri/yaak-license/src/error.rs @@ -16,7 +16,7 @@ pub enum Error { ModelError(#[from] yaak_models::error::Error), #[error(transparent)] - TauriUtilsError(#[from] yaak_tauri_utils::error::Error), + ApiError(#[from] yaak_api::Error), #[error("Internal server error")] ServerError, diff --git a/crates-tauri/yaak-license/src/license.rs b/crates-tauri/yaak-license/src/license.rs index dc6c185e..3f2c4aa3 100644 --- a/crates-tauri/yaak-license/src/license.rs +++ b/crates-tauri/yaak-license/src/license.rs @@ -11,7 +11,7 @@ use yaak_common::platform::get_os_str; use yaak_models::db_context::DbContext; use yaak_models::query_manager::QueryManager; use yaak_models::util::UpdateSource; -use yaak_tauri_utils::api_client::yaak_api_client; +use yaak_api::yaak_api_client; /// Extension trait for accessing the QueryManager from Tauri Manager types. /// This is needed temporarily until all crates are refactored to not use Tauri. @@ -118,11 +118,12 @@ pub async fn activate_license( license_key: &str, ) -> Result<()> { info!("Activating license {}", license_key); - let client = reqwest::Client::new(); + let app_version = window.app_handle().package_info().version.to_string(); + let client = yaak_api_client(&app_version)?; let payload = ActivateLicenseRequestPayload { license_key: license_key.to_string(), app_platform: get_os_str().to_string(), - app_version: window.app_handle().package_info().version.to_string(), + app_version, }; let response = client.post(build_url("/licenses/activate")).json(&payload).send().await?; @@ -155,11 +156,12 @@ pub async fn deactivate_license(window: &WebviewWindow) -> Result let app_handle = window.app_handle(); let activation_id = get_activation_id(app_handle).await; - let client = reqwest::Client::new(); + let app_version = window.app_handle().package_info().version.to_string(); + let client = yaak_api_client(&app_version)?; let path = format!("/licenses/activations/{}/deactivate", activation_id); let payload = DeactivateLicenseRequestPayload { app_platform: get_os_str().to_string(), - app_version: window.app_handle().package_info().version.to_string(), + app_version, }; let response = client.post(build_url(&path)).json(&payload).send().await?; @@ -186,9 +188,10 @@ pub async fn deactivate_license(window: &WebviewWindow) -> Result } pub async fn check_license(window: &WebviewWindow) -> Result { + let app_version = window.app_handle().package_info().version.to_string(); let payload = CheckActivationRequestPayload { app_platform: get_os_str().to_string(), - app_version: window.package_info().version.to_string(), + app_version, }; let activation_id = get_activation_id(window.app_handle()).await; @@ -204,7 +207,7 @@ pub async fn check_license(window: &WebviewWindow) -> Result { info!("Checking license activation"); // A license has been activated, so let's check the license server - let client = yaak_api_client(window.app_handle())?; + let client = yaak_api_client(&payload.app_version)?; let path = format!("/licenses/activations/{activation_id}/check-v2"); let response = client.post(build_url(&path)).json(&payload).send().await?; diff --git a/crates-tauri/yaak-tauri-utils/Cargo.toml b/crates-tauri/yaak-tauri-utils/Cargo.toml index 11652f1a..74272621 100644 --- a/crates-tauri/yaak-tauri-utils/Cargo.toml +++ b/crates-tauri/yaak-tauri-utils/Cargo.toml @@ -6,8 +6,4 @@ publish = false [dependencies] tauri = { workspace = true } -reqwest = { workspace = true, features = ["gzip"] } -thiserror = { workspace = true } -serde = { workspace = true, features = ["derive"] } regex = "1.11.0" -yaak-common = { workspace = true } diff --git a/crates-tauri/yaak-tauri-utils/src/api_client.rs b/crates-tauri/yaak-tauri-utils/src/api_client.rs deleted file mode 100644 index cc308126..00000000 --- a/crates-tauri/yaak-tauri-utils/src/api_client.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::error::Result; -use reqwest::Client; -use std::time::Duration; -use tauri::http::{HeaderMap, HeaderValue}; -use tauri::{AppHandle, Runtime}; -use yaak_common::platform::{get_ua_arch, get_ua_platform}; - -pub fn yaak_api_client(app_handle: &AppHandle) -> Result { - let platform = get_ua_platform(); - let version = app_handle.package_info().version.clone(); - let arch = get_ua_arch(); - let ua = format!("Yaak/{version} ({platform}; {arch})"); - let mut default_headers = HeaderMap::new(); - default_headers.insert("Accept", HeaderValue::from_str("application/json").unwrap()); - - let client = reqwest::ClientBuilder::new() - .timeout(Duration::from_secs(20)) - .default_headers(default_headers) - .gzip(true) - .user_agent(ua) - .build()?; - - Ok(client) -} diff --git a/crates-tauri/yaak-tauri-utils/src/error.rs b/crates-tauri/yaak-tauri-utils/src/error.rs deleted file mode 100644 index 46a9c103..00000000 --- a/crates-tauri/yaak-tauri-utils/src/error.rs +++ /dev/null @@ -1,19 +0,0 @@ -use serde::{Serialize, Serializer}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error(transparent)] - ReqwestError(#[from] reqwest::Error), -} - -impl Serialize for Error { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - serializer.serialize_str(self.to_string().as_ref()) - } -} - -pub type Result = std::result::Result; diff --git a/crates-tauri/yaak-tauri-utils/src/lib.rs b/crates-tauri/yaak-tauri-utils/src/lib.rs index 719ee7f0..61b63f17 100644 --- a/crates-tauri/yaak-tauri-utils/src/lib.rs +++ b/crates-tauri/yaak-tauri-utils/src/lib.rs @@ -1,3 +1 @@ -pub mod api_client; -pub mod error; pub mod window; diff --git a/crates/yaak-api/Cargo.toml b/crates/yaak-api/Cargo.toml new file mode 100644 index 00000000..024ae852 --- /dev/null +++ b/crates/yaak-api/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "yaak-api" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +log = { workspace = true } +reqwest = { workspace = true, features = ["gzip"] } +sysproxy = "0.3" +thiserror = { workspace = true } +yaak-common = { workspace = true } diff --git a/crates/yaak-api/src/error.rs b/crates/yaak-api/src/error.rs new file mode 100644 index 00000000..2cdc6a11 --- /dev/null +++ b/crates/yaak-api/src/error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), +} + +pub type Result = std::result::Result; diff --git a/crates/yaak-api/src/lib.rs b/crates/yaak-api/src/lib.rs new file mode 100644 index 00000000..6e18de19 --- /dev/null +++ b/crates/yaak-api/src/lib.rs @@ -0,0 +1,70 @@ +mod error; + +pub use error::{Error, Result}; + +use log::{debug, warn}; +use reqwest::Client; +use reqwest::header::{HeaderMap, HeaderValue}; +use std::time::Duration; +use yaak_common::platform::{get_ua_arch, get_ua_platform}; + +/// Build a reqwest Client configured for Yaak's own API calls. +/// +/// Includes a custom User-Agent, JSON accept header, 20s timeout, gzip, +/// and automatic OS-level proxy detection via sysproxy. +pub fn yaak_api_client(version: &str) -> Result { + let platform = get_ua_platform(); + let arch = get_ua_arch(); + let ua = format!("Yaak/{version} ({platform}; {arch})"); + + let mut default_headers = HeaderMap::new(); + default_headers.insert("Accept", HeaderValue::from_str("application/json").unwrap()); + + let mut builder = reqwest::ClientBuilder::new() + .timeout(Duration::from_secs(20)) + .default_headers(default_headers) + .gzip(true) + .user_agent(ua); + + if let Some(sys) = get_enabled_system_proxy() { + let proxy_url = format!("http://{}:{}", sys.host, sys.port); + match reqwest::Proxy::all(&proxy_url) { + Ok(p) => { + let p = if !sys.bypass.is_empty() { + p.no_proxy(reqwest::NoProxy::from_string(&sys.bypass)) + } else { + p + }; + builder = builder.proxy(p); + } + Err(e) => { + warn!("Failed to configure system proxy: {e}"); + } + } + } + + Ok(builder.build()?) +} + +/// Returns the system proxy URL if one is enabled, e.g. `http://host:port`. +pub fn get_system_proxy_url() -> Option { + let sys = get_enabled_system_proxy()?; + Some(format!("http://{}:{}", sys.host, sys.port)) +} + +fn get_enabled_system_proxy() -> Option { + match sysproxy::Sysproxy::get_system_proxy() { + Ok(sys) if sys.enable => { + debug!("Detected system proxy: http://{}:{}", sys.host, sys.port); + Some(sys) + } + Ok(_) => { + debug!("System proxy detected but not enabled"); + None + } + Err(e) => { + debug!("Could not detect system proxy: {e}"); + None + } + } +} From 9e1a11de0b5357ef6d1dee6da13d8f5651fac51b Mon Sep 17 00:00:00 2001 From: winit Date: Fri, 13 Feb 2026 04:57:18 +0530 Subject: [PATCH 25/29] Add CodeMirror extension to display find match count in the editor. (#390) --- src-web/components/core/Editor/Editor.css | 45 ++++++- src-web/components/core/Editor/extensions.ts | 2 + .../core/Editor/searchMatchCount.ts | 115 ++++++++++++++++++ 3 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 src-web/components/core/Editor/searchMatchCount.ts diff --git a/src-web/components/core/Editor/Editor.css b/src-web/components/core/Editor/Editor.css index 34679684..b7183b10 100644 --- a/src-web/components/core/Editor/Editor.css +++ b/src-web/components/core/Editor/Editor.css @@ -434,11 +434,23 @@ input { @apply bg-surface border-border-subtle focus:border-border-focus; - @apply border outline-none cursor-text; + @apply border outline-none; } - label { - @apply focus-within:text-text; + input.cm-textfield { + @apply cursor-text; + } + + .cm-search label { + @apply inline-flex items-center h-6 px-1.5 rounded-sm border border-border-subtle cursor-default text-text-subtle text-xs; + + input[type="checkbox"] { + @apply hidden; + } + + &:has(:checked) { + @apply text-primary border-border; + } } /* Hide the "All" button */ @@ -446,4 +458,31 @@ button[name="select"] { @apply hidden; } + + /* Replace next/prev button text with chevron icons */ + + .cm-search button[name="next"], + .cm-search button[name="prev"] { + @apply text-[0px] w-7 h-6 inline-flex items-center justify-center border border-border-subtle mr-1; + } + + .cm-search button[name="prev"]::after, + .cm-search button[name="next"]::after { + @apply block w-3.5 h-3.5 bg-text; + content: ""; + } + + .cm-search button[name="prev"]::after { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E"); + } + + .cm-search button[name="next"]::after { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E"); + } + + .cm-search-match-count { + @apply text-text-subtle text-xs font-mono whitespace-nowrap px-1.5 py-0.5 self-center; + } } diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index 6919fe2e..1ec1eb07 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -67,6 +67,7 @@ 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([ { @@ -256,6 +257,7 @@ export const readonlyExtensions = [ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [ search({ top: true }), + searchMatchCount(), hideGutter ? [] : [ diff --git a/src-web/components/core/Editor/searchMatchCount.ts b/src-web/components/core/Editor/searchMatchCount.ts new file mode 100644 index 00000000..9f4e8aff --- /dev/null +++ b/src-web/components/core/Editor/searchMatchCount.ts @@ -0,0 +1,115 @@ +import { getSearchQuery, searchPanelOpen } from '@codemirror/search'; +import type { Extension } from '@codemirror/state'; +import { type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view'; + +/** + * A CodeMirror extension that displays the total number of search matches + * inside the built-in search panel. + */ +export function searchMatchCount(): Extension { + return ViewPlugin.fromClass( + class { + private countEl: HTMLElement | null = null; + + constructor(private view: EditorView) { + this.updateCount(); + } + + update(update: ViewUpdate) { + // Recompute when doc changes, search state changes, or selection moves + const query = getSearchQuery(update.state); + const prevQuery = getSearchQuery(update.startState); + const open = searchPanelOpen(update.state); + const prevOpen = searchPanelOpen(update.startState); + + if (update.docChanged || update.selectionSet || !query.eq(prevQuery) || open !== prevOpen) { + this.updateCount(); + } + } + + private updateCount() { + const state = this.view.state; + const open = searchPanelOpen(state); + const query = getSearchQuery(state); + + if (!open) { + this.removeCountEl(); + return; + } + + this.ensureCountEl(); + + if (!query.search) { + if (this.countEl) { + this.countEl.textContent = '0/0'; + } + return; + } + + const selection = state.selection.main; + let count = 0; + let currentIndex = 0; + const MAX_COUNT = 9999; + const cursor = query.getCursor(state); + while (!cursor.next().done) { + count++; + if (cursor.value.from <= selection.from && cursor.value.to >= selection.to) { + currentIndex = count; + } + if (count > MAX_COUNT) break; + } + + if (this.countEl) { + if (count > MAX_COUNT) { + this.countEl.textContent = `${MAX_COUNT}+`; + } else if (count === 0) { + this.countEl.textContent = '0/0'; + } else if (currentIndex > 0) { + this.countEl.textContent = `${currentIndex}/${count}`; + } else { + this.countEl.textContent = `0/${count}`; + } + } + } + + private ensureCountEl() { + // Find the search panel in the editor DOM + const panel = this.view.dom.querySelector('.cm-search'); + if (!panel) { + this.countEl = null; + return; + } + + if (this.countEl && this.countEl.parentElement === panel) { + return; // Already attached + } + + this.countEl = document.createElement('span'); + this.countEl.className = 'cm-search-match-count'; + + // Reorder: insert prev button, then next button, then count after the search input + const searchInput = panel.querySelector('input'); + const prevBtn = panel.querySelector('button[name="prev"]'); + const nextBtn = panel.querySelector('button[name="next"]'); + if (searchInput && searchInput.parentElement === panel) { + searchInput.after(this.countEl); + if (prevBtn) this.countEl.after(prevBtn); + if (nextBtn && prevBtn) prevBtn.after(nextBtn); + } else { + panel.prepend(this.countEl); + } + } + + private removeCountEl() { + if (this.countEl) { + this.countEl.remove(); + this.countEl = null; + } + } + + destroy() { + this.removeCountEl(); + } + }, + ); +} From ae943a5fd22fa275b1456376185a4badf403969e Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 12 Feb 2026 15:34:13 -0800 Subject: [PATCH 26/29] Fix lint: ignore flatpak in biome, fix search cursor iterator usage --- biome.json | 3 ++- src-web/components/core/Editor/searchMatchCount.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/biome.json b/biome.json index 13111ead..2a9cd936 100644 --- a/biome.json +++ b/biome.json @@ -47,7 +47,8 @@ "!src-web/vite.config.ts", "!src-web/routeTree.gen.ts", "!packages/plugin-runtime-types/lib", - "!**/bindings" + "!**/bindings", + "!flatpak" ] } } diff --git a/src-web/components/core/Editor/searchMatchCount.ts b/src-web/components/core/Editor/searchMatchCount.ts index 9f4e8aff..79b0937a 100644 --- a/src-web/components/core/Editor/searchMatchCount.ts +++ b/src-web/components/core/Editor/searchMatchCount.ts @@ -51,9 +51,10 @@ export function searchMatchCount(): Extension { let currentIndex = 0; const MAX_COUNT = 9999; const cursor = query.getCursor(state); - while (!cursor.next().done) { + for (let result = cursor.next(); !result.done; result = cursor.next()) { count++; - if (cursor.value.from <= selection.from && cursor.value.to >= selection.to) { + const match = result.value; + if (match.from <= selection.from && match.to >= selection.to) { currentIndex = count; } if (count > MAX_COUNT) break; From 65e91aec6b2ba9f8b3acf0badd9280524fce49eb Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 13 Feb 2026 14:46:47 -0800 Subject: [PATCH 27/29] Fix git pull conflicts with pull.ff=only and improve commit UX (#394) --- crates/yaak-git/src/pull.rs | 56 +++++++++++++------ src-web/components/git/GitCommitDialog.tsx | 12 ++-- src-web/components/git/GitDropdown.tsx | 2 - src-web/components/git/callbacks.tsx | 4 +- .../components/git/showAddRemoteDialog.tsx | 4 +- 5 files changed, 51 insertions(+), 27 deletions(-) diff --git a/crates/yaak-git/src/pull.rs b/crates/yaak-git/src/pull.rs index 0bf75474..4185350e 100644 --- a/crates/yaak-git/src/pull.rs +++ b/crates/yaak-git/src/pull.rs @@ -44,43 +44,65 @@ pub async fn git_pull(dir: &Path) -> Result { (branch_name, remote_name, remote_url) }; - let out = new_binary_command(dir) + // Step 1: fetch the specific branch + // NOTE: We use fetch + merge instead of `git pull` to avoid conflicts with + // global git config (e.g. pull.ff=only) and the background fetch --all. + let fetch_out = new_binary_command(dir) .await? - .args(["pull", &remote_name, &branch_name]) + .args(["fetch", &remote_name, &branch_name]) .env("GIT_TERMINAL_PROMPT", "0") .output() .await - .map_err(|e| GenericError(format!("failed to run git pull: {e}")))?; + .map_err(|e| GenericError(format!("failed to run git fetch: {e}")))?; - let stdout = String::from_utf8_lossy(&out.stdout); - let stderr = String::from_utf8_lossy(&out.stderr); - let combined = stdout + stderr; + let fetch_stdout = String::from_utf8_lossy(&fetch_out.stdout); + let fetch_stderr = String::from_utf8_lossy(&fetch_out.stderr); + let fetch_combined = format!("{fetch_stdout}{fetch_stderr}"); - info!("Pulled status={} {combined}", out.status); + info!("Fetched status={} {fetch_combined}", fetch_out.status); - if combined.to_lowercase().contains("could not read") { + if fetch_combined.to_lowercase().contains("could not read") { return Ok(PullResult::NeedsCredentials { url: remote_url.to_string(), error: None }); } - if combined.to_lowercase().contains("unable to access") { + if fetch_combined.to_lowercase().contains("unable to access") { return Ok(PullResult::NeedsCredentials { url: remote_url.to_string(), - error: Some(combined.to_string()), + error: Some(fetch_combined.to_string()), }); } - if !out.status.success() { - let combined_lower = combined.to_lowercase(); - if combined_lower.contains("cannot fast-forward") - || combined_lower.contains("not possible to fast-forward") - || combined_lower.contains("diverged") + if !fetch_out.status.success() { + return Err(GenericError(format!("Failed to fetch: {fetch_combined}"))); + } + + // Step 2: merge the fetched branch + let ref_name = format!("{}/{}", remote_name, branch_name); + let merge_out = new_binary_command(dir) + .await? + .args(["merge", "--ff-only", &ref_name]) + .output() + .await + .map_err(|e| GenericError(format!("failed to run git merge: {e}")))?; + + let merge_stdout = String::from_utf8_lossy(&merge_out.stdout); + let merge_stderr = String::from_utf8_lossy(&merge_out.stderr); + let merge_combined = format!("{merge_stdout}{merge_stderr}"); + + info!("Merged status={} {merge_combined}", merge_out.status); + + if !merge_out.status.success() { + let merge_lower = merge_combined.to_lowercase(); + if merge_lower.contains("cannot fast-forward") + || merge_lower.contains("not possible to fast-forward") + || merge_lower.contains("diverged") { return Ok(PullResult::Diverged { remote: remote_name, branch: branch_name }); } - return Err(GenericError(format!("Failed to pull {combined}"))); + return Err(GenericError(format!("Failed to merge: {merge_combined}"))); } - if combined.to_lowercase().contains("up to date") { + if merge_combined.to_lowercase().contains("up to date") { return Ok(PullResult::UpToDate); } diff --git a/src-web/components/git/GitCommitDialog.tsx b/src-web/components/git/GitCommitDialog.tsx index 7435f3b3..7de28a23 100644 --- a/src-web/components/git/GitCommitDialog.tsx +++ b/src-web/components/git/GitCommitDialog.tsx @@ -176,7 +176,11 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { } if (!hasAnythingToAdd) { - return No changes since last commit; + return ( +
+ No changes since last commit +
+ ); } return ( @@ -230,14 +234,14 @@ export function GitCommitDialog({ syncDir, onDone, workspace }: Props) { hideLabel /> {commitError && {commitError}} - + {status.data?.headRefShorthand}