feat: add version bumping script and i18n key generator

This commit is contained in:
Tonho committed 2026-04-13 15:39:10 -03:00
1 parent b06b7d1d35
commit f288acaa83
24 files changed
+1032 -69

No files matched your search

+32 -4
View File
@@ -145,9 +145,6 @@ jobs:
--arg d "$PUB_DATE" \
'{version: $v, notes: "", pub_date: $d, platforms: {}}')
ASSETS=$(gh release view "$TAG" --repo "$REPO" --json assets -q '.assets[].name')
TMPDIR=$(mktemp -d)
declare -A PLATFORM_MAP=(
["aarch64.app.tar.gz.sig"]="darwin-aarch64"
["x64.app.tar.gz.sig"]="darwin-x86_64"
@@ -156,13 +153,44 @@ jobs:
["x64-setup.exe.sig"]="windows-x86_64"
)
ASSETS=""
for attempt in $(seq 1 10); do
ASSETS=$(gh release view "$TAG" --repo "$REPO" --json assets -q '.assets[].name' || true)
MISSING=0
for SUFFIX in "${!PLATFORM_MAP[@]}"; do
if ! echo "$ASSETS" | grep -q "${SUFFIX}$"; then
MISSING=$((MISSING + 1))
fi
done
if [ "$MISSING" -eq 0 ]; then
echo "All expected .sig assets are visible (attempt $attempt)."
break
fi
echo "Attempt $attempt: $MISSING .sig asset(s) not yet visible, retrying in 30s..."
sleep 30
done
TMPDIR=$(mktemp -d)
for SUFFIX in "${!PLATFORM_MAP[@]}"; do
PLATFORM="${PLATFORM_MAP[$SUFFIX]}"
SIG_FILE=$(echo "$ASSETS" | grep "${SUFFIX}$" || true)
if [ -n "$SIG_FILE" ]; then
BUNDLE="${SIG_FILE%.sig}"
gh release download "$TAG" --repo "$REPO" --pattern "$SIG_FILE" --dir "$TMPDIR" --clobber
DOWNLOADED=0
for dl_attempt in $(seq 1 10); do
if gh release download "$TAG" --repo "$REPO" --pattern "$SIG_FILE" --dir "$TMPDIR" --clobber; then
DOWNLOADED=1
break
fi
echo "Download attempt $dl_attempt for $SIG_FILE failed, retrying in 30s..."
sleep 30
done
if [ "$DOWNLOADED" -ne 1 ]; then
echo "Failed to download $SIG_FILE after 10 attempts" >&2
exit 1
fi
SIG_CONTENT=$(cat "$TMPDIR/$SIG_FILE")
URL="https://github.com/${REPO}/releases/download/${TAG}/${BUNDLE}"
+1
View File
@@ -37,6 +37,7 @@ body {
}
.header-left { display: flex; align-items: center; gap: 8px; }
.header-toggles { display: flex; align-items: center; gap: 10px; }
.logo { width: 16px; height: 16px; }
.title { font-weight: 600; font-size: 14px; }
+10 -4
View File
@@ -12,10 +12,16 @@
<img src="../icons/active-16.png" alt="" class="logo">
<span class="title">OmniGet</span>
</div>
<label class="toggle" title="Media detection">
<input type="checkbox" id="sniffer-toggle" role="switch" aria-label="Media detection">
<span class="toggle-slider"></span>
</label>
<div class="header-toggles">
<label class="toggle" title="Open app when downloading">
<input type="checkbox" id="open-app-toggle" role="switch" aria-label="Open app when downloading">
<span class="toggle-slider"></span>
</label>
<label class="toggle" title="Media detection">
<input type="checkbox" id="sniffer-toggle" role="switch" aria-label="Media detection">
<span class="toggle-slider"></span>
</label>
</div>
</header>
<div id="content" class="content">
<div class="state-empty">
+14
View File
@@ -1,3 +1,5 @@
import { loadOpenAppState, isOpenAppEnabled, setOpenAppEnabled } from "../src/open-app-toggle.js";
const APP_URL = "https://github.com/tonhowtf/omniget/releases/latest";
const SVG = {
@@ -11,15 +13,24 @@ const SVG = {
let currentData = null;
let pageTitle = "";
let pageThumbnail = "";
async function init() {
const toggle = document.getElementById("sniffer-toggle");
const openAppToggle = document.getElementById("open-app-toggle");
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
pageTitle = tab?.title || "";
pageThumbnail = tab?.favIconUrl || "";
} catch {}
await loadOpenAppState();
openAppToggle.checked = isOpenAppEnabled();
openAppToggle.addEventListener("change", () => {
setOpenAppEnabled(openAppToggle.checked);
});
chrome.runtime.sendMessage({ type: "getDetectedMedia" }, (response) => {
if (!response) return;
currentData = response;
@@ -309,9 +320,11 @@ function sendToApp(url, platform, mediaEntry) {
const msg = { type: "sendToOmniGet", url, platform };
if (pageTitle) msg.title = pageTitle;
if (pageThumbnail) msg.thumbnail = pageThumbnail;
if (currentData?.tabUrl) msg.referer = currentData.tabUrl;
if (mediaEntry?.mediaType) msg.mediaType = mediaEntry.mediaType;
if (mediaEntry?.contentType) msg.contentType = mediaEntry.contentType;
msg.openApp = isOpenAppEnabled();
if (mediaEntry?.requestHeaders) {
const skip = ["host", "connection", "accept-encoding", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "upgrade-insecure-requests"];
@@ -344,6 +357,7 @@ async function sendBatch(groups) {
if (currentData?.tabUrl) msg.referer = currentData.tabUrl;
if (group.master.mediaType) msg.mediaType = group.master.mediaType;
if (group.master.contentType) msg.contentType = group.master.contentType;
msg.openApp = isOpenAppEnabled();
try {
await new Promise((resolve) => {
@@ -156,9 +156,11 @@ async function handleSendToApp(msg) {
const platform = msg.platform || "generic";
let pageTitle = "";
let pageThumbnail = "";
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
pageTitle = tab?.title || "";
pageThumbnail = tab?.favIconUrl || "";
} catch {}
let cookies = null;
@@ -204,9 +206,12 @@ async function handleSendToApp(msg) {
if (msg.referer) message.referer = msg.referer;
if (msg.title) message.title = msg.title;
else if (pageTitle) message.title = pageTitle;
if (msg.thumbnail) message.thumbnail = msg.thumbnail;
else if (pageThumbnail) message.thumbnail = pageThumbnail;
if (msg.mediaType) message.mediaType = msg.mediaType;
if (msg.contentType) message.contentType = msg.contentType;
if (msg.headers) message.headers = msg.headers;
if (typeof msg.openApp === "boolean") message.openApp = msg.openApp;
message.pageUrl = msg.referer || "";
message.userAgent = navigator.userAgent;
+30 -5
View File
@@ -71,6 +71,18 @@ const PLATFORM_CDN_HOSTS = [
"tiktokcdn-us.com",
];
const BLOCKED_PATH_PATTERNS = [
/\/analytics(\/|\?|$)/i,
/\/pixel(\/|\?|\.|$)/i,
/\/collect(\/|\?|$)/i,
/\/beacon(\/|\?|$)/i,
/\/telemetry(\/|\?|$)/i,
/\/track(\/|\?|$|er)/i,
/1x1\.gif$/i,
/\/spacer\.gif$/i,
/\/transparent\.gif$/i,
];
const MIN_CONTENT_LENGTH = 50 * 1024;
const detectedMedia = new Map();
@@ -96,6 +108,14 @@ function isBlockedHost(url) {
} catch { return false; }
}
function isBlockedPath(url) {
try {
const u = new URL(url);
const target = u.pathname + u.search;
return BLOCKED_PATH_PATTERNS.some(rx => rx.test(target));
} catch { return false; }
}
function isMediaByExtension(url) {
try {
const path = new URL(url).pathname.toLowerCase();
@@ -168,16 +188,18 @@ function isPlatformCdnFragment(url, contentType, contentLength) {
} catch { return false; }
}
const storageArea = chrome.storage.session ?? chrome.storage.local;
function persistMedia(tabId) {
const media = detectedMedia.get(tabId);
if (!media || media.size === 0) return;
const arr = Array.from(media.entries());
chrome.storage.session.set({ [`media_${tabId}`]: arr }).catch(() => {});
storageArea.set({ [`media_${tabId}`]: arr }).catch(() => {});
}
export async function restoreMedia() {
try {
const data = await chrome.storage.session.get(null);
const data = await storageArea.get(null);
for (const [key, value] of Object.entries(data)) {
if (!key.startsWith("media_")) continue;
const tabId = parseInt(key.replace("media_", ""));
@@ -214,7 +236,10 @@ export function registerSnifferListeners(onMediaDetected) {
if (details.statusCode < 200 || details.statusCode >= 300) return;
const url = details.url;
if (isBlockedHost(url)) return;
if (isBlockedHost(url) || isBlockedPath(url)) {
pendingRequests.delete(details.requestId);
return;
}
const contentType = getContentType(details.responseHeaders);
const contentLength = getContentLength(details.responseHeaders);
@@ -280,13 +305,13 @@ export function registerSnifferListeners(onMediaDetected) {
chrome.tabs.onRemoved.addListener((tabId) => {
detectedMedia.delete(tabId);
chrome.storage.session.remove(`media_${tabId}`).catch(() => {});
storageArea.remove(`media_${tabId}`).catch(() => {});
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.url) {
detectedMedia.delete(tabId);
chrome.storage.session.remove(`media_${tabId}`).catch(() => {});
storageArea.remove(`media_${tabId}`).catch(() => {});
}
});
}
@@ -0,0 +1,18 @@
const STORAGE_KEY = "omniget_open_app_on_download";
let openApp = true;
export async function loadOpenAppState() {
const result = await chrome.storage.local.get(STORAGE_KEY);
openApp = result[STORAGE_KEY] !== false;
return openApp;
}
export function isOpenAppEnabled() {
return openApp;
}
export async function setOpenAppEnabled(value) {
openApp = value;
await chrome.storage.local.set({ [STORAGE_KEY]: value });
}
@@ -37,6 +37,7 @@ body {
}
.header-left { display: flex; align-items: center; gap: 8px; }
.header-toggles { display: flex; align-items: center; gap: 10px; }
.logo { width: 16px; height: 16px; }
.title { font-weight: 600; font-size: 14px; }
+10 -4
View File
@@ -12,10 +12,16 @@
<img src="../icons/active-16.png" alt="" class="logo">
<span class="title">OmniGet</span>
</div>
<label class="toggle" title="Media detection">
<input type="checkbox" id="sniffer-toggle" role="switch" aria-label="Media detection">
<span class="toggle-slider"></span>
</label>
<div class="header-toggles">
<label class="toggle" title="Open app when downloading">
<input type="checkbox" id="open-app-toggle" role="switch" aria-label="Open app when downloading">
<span class="toggle-slider"></span>
</label>
<label class="toggle" title="Media detection">
<input type="checkbox" id="sniffer-toggle" role="switch" aria-label="Media detection">
<span class="toggle-slider"></span>
</label>
</div>
</header>
<div id="content" class="content">
<div class="state-empty">
+14
View File
@@ -1,3 +1,5 @@
import { loadOpenAppState, isOpenAppEnabled, setOpenAppEnabled } from "../src/open-app-toggle.js";
const APP_URL = "https://github.com/tonhowtf/omniget/releases/latest";
const SVG = {
@@ -11,15 +13,24 @@ const SVG = {
let currentData = null;
let pageTitle = "";
let pageThumbnail = "";
async function init() {
const toggle = document.getElementById("sniffer-toggle");
const openAppToggle = document.getElementById("open-app-toggle");
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
pageTitle = tab?.title || "";
pageThumbnail = tab?.favIconUrl || "";
} catch {}
await loadOpenAppState();
openAppToggle.checked = isOpenAppEnabled();
openAppToggle.addEventListener("change", () => {
setOpenAppEnabled(openAppToggle.checked);
});
chrome.runtime.sendMessage({ type: "getDetectedMedia" }, (response) => {
if (!response) return;
currentData = response;
@@ -309,9 +320,11 @@ function sendToApp(url, platform, mediaEntry) {
const msg = { type: "sendToOmniGet", url, platform };
if (pageTitle) msg.title = pageTitle;
if (pageThumbnail) msg.thumbnail = pageThumbnail;
if (currentData?.tabUrl) msg.referer = currentData.tabUrl;
if (mediaEntry?.mediaType) msg.mediaType = mediaEntry.mediaType;
if (mediaEntry?.contentType) msg.contentType = mediaEntry.contentType;
msg.openApp = isOpenAppEnabled();
if (mediaEntry?.requestHeaders) {
const skip = ["host", "connection", "accept-encoding", "sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform", "sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site", "upgrade-insecure-requests"];
@@ -344,6 +357,7 @@ async function sendBatch(groups) {
if (currentData?.tabUrl) msg.referer = currentData.tabUrl;
if (group.master.mediaType) msg.mediaType = group.master.mediaType;
if (group.master.contentType) msg.contentType = group.master.contentType;
msg.openApp = isOpenAppEnabled();
try {
await new Promise((resolve) => {
@@ -156,9 +156,11 @@ async function handleSendToApp(msg) {
const platform = msg.platform || "generic";
let pageTitle = "";
let pageThumbnail = "";
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
pageTitle = tab?.title || "";
pageThumbnail = tab?.favIconUrl || "";
} catch {}
let cookies = null;
@@ -204,9 +206,12 @@ async function handleSendToApp(msg) {
if (msg.referer) message.referer = msg.referer;
if (msg.title) message.title = msg.title;
else if (pageTitle) message.title = pageTitle;
if (msg.thumbnail) message.thumbnail = msg.thumbnail;
else if (pageThumbnail) message.thumbnail = pageThumbnail;
if (msg.mediaType) message.mediaType = msg.mediaType;
if (msg.contentType) message.contentType = msg.contentType;
if (msg.headers) message.headers = msg.headers;
if (typeof msg.openApp === "boolean") message.openApp = msg.openApp;
message.pageUrl = msg.referer || "";
message.userAgent = navigator.userAgent;
+30 -5
View File
@@ -71,6 +71,18 @@ const PLATFORM_CDN_HOSTS = [
"tiktokcdn-us.com",
];
const BLOCKED_PATH_PATTERNS = [
/\/analytics(\/|\?|$)/i,
/\/pixel(\/|\?|\.|$)/i,
/\/collect(\/|\?|$)/i,
/\/beacon(\/|\?|$)/i,
/\/telemetry(\/|\?|$)/i,
/\/track(\/|\?|$|er)/i,
/1x1\.gif$/i,
/\/spacer\.gif$/i,
/\/transparent\.gif$/i,
];
const MIN_CONTENT_LENGTH = 50 * 1024;
const detectedMedia = new Map();
@@ -96,6 +108,14 @@ function isBlockedHost(url) {
} catch { return false; }
}
function isBlockedPath(url) {
try {
const u = new URL(url);
const target = u.pathname + u.search;
return BLOCKED_PATH_PATTERNS.some(rx => rx.test(target));
} catch { return false; }
}
function isMediaByExtension(url) {
try {
const path = new URL(url).pathname.toLowerCase();
@@ -168,16 +188,18 @@ function isPlatformCdnFragment(url, contentType, contentLength) {
} catch { return false; }
}
const storageArea = chrome.storage.session ?? chrome.storage.local;
function persistMedia(tabId) {
const media = detectedMedia.get(tabId);
if (!media || media.size === 0) return;
const arr = Array.from(media.entries());
chrome.storage.local.set({ [`media_${tabId}`]: arr }).catch(() => {});
storageArea.set({ [`media_${tabId}`]: arr }).catch(() => {});
}
export async function restoreMedia() {
try {
const data = await chrome.storage.local.get(null);
const data = await storageArea.get(null);
for (const [key, value] of Object.entries(data)) {
if (!key.startsWith("media_")) continue;
const tabId = parseInt(key.replace("media_", ""));
@@ -214,7 +236,10 @@ export function registerSnifferListeners(onMediaDetected) {
if (details.statusCode < 200 || details.statusCode >= 300) return;
const url = details.url;
if (isBlockedHost(url)) return;
if (isBlockedHost(url) || isBlockedPath(url)) {
pendingRequests.delete(details.requestId);
return;
}
const contentType = getContentType(details.responseHeaders);
const contentLength = getContentLength(details.responseHeaders);
@@ -280,13 +305,13 @@ export function registerSnifferListeners(onMediaDetected) {
chrome.tabs.onRemoved.addListener((tabId) => {
detectedMedia.delete(tabId);
chrome.storage.local.remove(`media_${tabId}`).catch(() => {});
storageArea.remove(`media_${tabId}`).catch(() => {});
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.url) {
detectedMedia.delete(tabId);
chrome.storage.local.remove(`media_${tabId}`).catch(() => {});
storageArea.remove(`media_${tabId}`).catch(() => {});
}
});
}
@@ -0,0 +1,18 @@
const STORAGE_KEY = "omniget_open_app_on_download";
let openApp = true;
export async function loadOpenAppState() {
const result = await chrome.storage.local.get(STORAGE_KEY);
openApp = result[STORAGE_KEY] !== false;
return openApp;
}
export function isOpenAppEnabled() {
return openApp;
}
export async function setOpenAppEnabled(value) {
openApp = value;
await chrome.storage.local.set({ [STORAGE_KEY]: value });
}
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CHROME = path.join(__dirname, "chrome");
const FIREFOX = path.join(__dirname, "firefox");
const SHARED = [
"src/action-click.js",
"src/action-feedback.js",
"src/action-title.js",
"src/background.js",
"src/context-menu.js",
"src/cookies.js",
"src/detect.js",
"src/media-sniffer.js",
"src/open-app-toggle.js",
"src/sniffer-toggle.js",
"popup/popup.js",
"popup/popup.css",
"popup/popup.html",
"pages/error.html",
"pages/error.css",
"pages/error.js",
"pages/error-content.js",
];
const SHARED_DIRS = ["_locales", "icons"];
function copyFile(rel) {
const from = path.join(CHROME, rel);
const to = path.join(FIREFOX, rel);
if (!fs.existsSync(from)) {
console.warn(`[skip] missing in chrome: ${rel}`);
return { changed: false, same: false, skipped: true };
}
fs.mkdirSync(path.dirname(to), { recursive: true });
const a = fs.readFileSync(from);
if (fs.existsSync(to)) {
const b = fs.readFileSync(to);
if (a.equals(b)) return { changed: false, same: true, skipped: false };
}
fs.writeFileSync(to, a);
return { changed: true, same: false, skipped: false };
}
function copyDirRecursive(relDir) {
const from = path.join(CHROME, relDir);
const to = path.join(FIREFOX, relDir);
if (!fs.existsSync(from)) return { changed: 0, same: 0 };
let changed = 0;
let same = 0;
const walk = (srcDir, dstDir) => {
fs.mkdirSync(dstDir, { recursive: true });
for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
const srcPath = path.join(srcDir, entry.name);
const dstPath = path.join(dstDir, entry.name);
if (entry.isDirectory()) {
walk(srcPath, dstPath);
} else if (entry.isFile()) {
const a = fs.readFileSync(srcPath);
if (fs.existsSync(dstPath)) {
const b = fs.readFileSync(dstPath);
if (a.equals(b)) {
same++;
continue;
}
}
fs.writeFileSync(dstPath, a);
changed++;
}
}
};
walk(from, to);
return { changed, same };
}
let totalChanged = 0;
let totalSame = 0;
for (const rel of SHARED) {
const r = copyFile(rel);
if (r.changed) {
console.log(`[copy] ${rel}`);
totalChanged++;
} else if (r.same) {
totalSame++;
}
}
for (const rel of SHARED_DIRS) {
const r = copyDirRecursive(rel);
if (r.changed > 0) console.log(`[copy] ${rel}/ (${r.changed} file(s))`);
totalChanged += r.changed;
totalSame += r.same;
}
console.log(`\nSynced chrome → firefox: ${totalChanged} changed, ${totalSame} already identical.`);
console.log(`Note: manifest.json is intentionally per-browser and not synced.`);
+9 -2
View File
@@ -10,7 +10,12 @@
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "tauri"
"tauri": "tauri",
"version:set": "node scripts/bump-version.js",
"generate:i18n-keys": "node scripts/generate-i18n-keys.js",
"extension:sync": "node browser-extension/sync.mjs",
"test": "vitest run",
"test:watch": "vitest"
},
"license": "GPL-3.0",
"dependencies": {
@@ -29,9 +34,11 @@
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tauri-apps/cli": "^2",
"@types/node": "^25.6.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
"vite": "^6.0.3"
"vite": "^6.0.3",
"vitest": "^4.1.4"
}
}
+276 -19
View File
@@ -38,16 +38,19 @@ importers:
devDependencies:
'@sveltejs/adapter-static':
specifier: ^3.0.6
version: 3.0.10(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1))
version: 3.0.10(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0)))
'@sveltejs/kit':
specifier: ^2.9.0
version: 2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1)
version: 2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0))
'@sveltejs/vite-plugin-svelte':
specifier: ^5.0.0
version: 5.1.1(svelte@5.51.0)(vite@6.4.1)
version: 5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0))
'@tauri-apps/cli':
specifier: ^2
version: 2.10.0
'@types/node':
specifier: ^25.6.0
version: 25.6.0
svelte:
specifier: ^5.0.0
version: 5.51.0
@@ -59,7 +62,10 @@ importers:
version: 5.6.3
vite:
specifier: ^6.0.3
version: 6.4.1
version: 6.4.1(@types/node@25.6.0)
vitest:
specifier: ^4.1.4
version: 4.1.4(@types/node@25.6.0)(vite@6.4.1(@types/node@25.6.0))
packages:
@@ -528,15 +534,53 @@ packages:
'@tauri-apps/plugin-updater@2.10.0':
resolution: {integrity: sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@25.6.0':
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
'@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
'@vitest/expect@4.1.4':
resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==}
'@vitest/mocker@4.1.4':
resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@4.1.4':
resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==}
'@vitest/runner@4.1.4':
resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==}
'@vitest/snapshot@4.1.4':
resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==}
'@vitest/spy@4.1.4':
resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==}
'@vitest/utils@4.1.4':
resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
@@ -546,10 +590,18 @@ packages:
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
engines: {node: '>= 0.4'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
engines: {node: '>= 0.4'}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -558,6 +610,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
cookie@0.6.0:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
@@ -578,6 +633,9 @@ packages:
devalue@5.6.2:
resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==}
es-module-lexer@2.0.0:
resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==}
esbuild@0.25.12:
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
engines: {node: '>=18'}
@@ -589,6 +647,13 @@ packages:
esrap@2.2.3:
resolution: {integrity: sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@@ -632,6 +697,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
obug@2.1.1:
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -659,6 +730,9 @@ packages:
set-cookie-parser@3.0.1:
resolution: {integrity: sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
sirv@3.0.2:
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
engines: {node: '>=18'}
@@ -667,6 +741,12 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@4.0.0:
resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==}
svelte-check@4.4.0:
resolution: {integrity: sha512-gB3FdEPb8tPO3Y7Dzc6d/Pm/KrXAhK+0Fk+LkcysVtupvAh6Y/IrBCEZNupq57oh0hcwlxCUamu/rq7GtvfSEg==}
engines: {node: '>= 18.0.0'}
@@ -684,10 +764,21 @@ packages:
peerDependencies:
svelte: '>=3.49.0'
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@1.1.1:
resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==}
engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
tinyrainbow@3.1.0:
resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==}
engines: {node: '>=14.0.0'}
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -697,6 +788,9 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
undici-types@7.19.2:
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
vite@6.4.1:
resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -745,6 +839,52 @@ packages:
vite:
optional: true
vitest@4.1.4:
resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
'@vitest/browser-playwright': 4.1.4
'@vitest/browser-preview': 4.1.4
'@vitest/browser-webdriverio': 4.1.4
'@vitest/coverage-istanbul': 4.1.4
'@vitest/coverage-v8': 4.1.4
'@vitest/ui': 4.1.4
happy-dom: '*'
jsdom: '*'
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@opentelemetry/api':
optional: true
'@types/node':
optional: true
'@vitest/browser-playwright':
optional: true
'@vitest/browser-preview':
optional: true
'@vitest/browser-webdriverio':
optional: true
'@vitest/coverage-istanbul':
optional: true
'@vitest/coverage-v8':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
zimmerframe@1.1.4:
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
@@ -934,15 +1074,15 @@ snapshots:
dependencies:
acorn: 8.15.0
'@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1))':
'@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0)))':
dependencies:
'@sveltejs/kit': 2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1)
'@sveltejs/kit': 2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0))
'@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1)':
'@sveltejs/kit@2.51.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(typescript@5.6.3)(vite@6.4.1(@types/node@25.6.0))':
dependencies:
'@standard-schema/spec': 1.1.0
'@sveltejs/acorn-typescript': 1.0.9(acorn@8.15.0)
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.51.0)(vite@6.4.1)
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0))
'@types/cookie': 0.6.0
acorn: 8.15.0
cookie: 0.6.0
@@ -955,29 +1095,29 @@ snapshots:
set-cookie-parser: 3.0.1
sirv: 3.0.2
svelte: 5.51.0
vite: 6.4.1
vite: 6.4.1(@types/node@25.6.0)
optionalDependencies:
typescript: 5.6.3
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(vite@6.4.1)':
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0))':
dependencies:
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.51.0)(vite@6.4.1)
'@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0))
debug: 4.4.3
svelte: 5.51.0
vite: 6.4.1
vite: 6.4.1(@types/node@25.6.0)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1)':
'@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1))(svelte@5.51.0)(vite@6.4.1)
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0)))(svelte@5.51.0)(vite@6.4.1(@types/node@25.6.0))
debug: 4.4.3
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.21
svelte: 5.51.0
vite: 6.4.1
vitefu: 1.1.1(vite@6.4.1)
vite: 6.4.1(@types/node@25.6.0)
vitefu: 1.1.1(vite@6.4.1(@types/node@25.6.0))
transitivePeerDependencies:
- supports-color
@@ -1056,24 +1196,82 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.10.1
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/cookie@0.6.0': {}
'@types/deep-eql@4.0.2': {}
'@types/estree@1.0.8': {}
'@types/node@25.6.0':
dependencies:
undici-types: 7.19.2
'@types/trusted-types@2.0.7': {}
'@vitest/expect@4.1.4':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@vitest/spy': 4.1.4
'@vitest/utils': 4.1.4
chai: 6.2.2
tinyrainbow: 3.1.0
'@vitest/mocker@4.1.4(vite@6.4.1(@types/node@25.6.0))':
dependencies:
'@vitest/spy': 4.1.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 6.4.1(@types/node@25.6.0)
'@vitest/pretty-format@4.1.4':
dependencies:
tinyrainbow: 3.1.0
'@vitest/runner@4.1.4':
dependencies:
'@vitest/utils': 4.1.4
pathe: 2.0.3
'@vitest/snapshot@4.1.4':
dependencies:
'@vitest/pretty-format': 4.1.4
'@vitest/utils': 4.1.4
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@4.1.4': {}
'@vitest/utils@4.1.4':
dependencies:
'@vitest/pretty-format': 4.1.4
convert-source-map: 2.0.0
tinyrainbow: 3.1.0
acorn@8.15.0: {}
aria-query@5.3.2: {}
assertion-error@2.0.1: {}
axobject-query@4.1.0: {}
chai@6.2.2: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
clsx@2.1.1: {}
convert-source-map@2.0.0: {}
cookie@0.6.0: {}
debug@4.4.3:
@@ -1084,6 +1282,8 @@ snapshots:
devalue@5.6.2: {}
es-module-lexer@2.0.0: {}
esbuild@0.25.12:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.12
@@ -1119,6 +1319,12 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
expect-type@1.3.0: {}
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
@@ -1146,6 +1352,10 @@ snapshots:
nanoid@3.3.11: {}
obug@2.1.1: {}
pathe@2.0.3: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
@@ -1195,6 +1405,8 @@ snapshots:
set-cookie-parser@3.0.1: {}
siginfo@2.0.0: {}
sirv@3.0.2:
dependencies:
'@polka/url': 1.0.0-next.29
@@ -1203,6 +1415,10 @@ snapshots:
source-map-js@1.2.1: {}
stackback@0.0.2: {}
std-env@4.0.0: {}
svelte-check@4.4.0(picomatch@4.0.3)(svelte@5.51.0)(typescript@5.6.3):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@@ -1240,16 +1456,24 @@ snapshots:
'@sveltekit-i18n/parser-default': 1.1.1
svelte: 5.51.0
tinybench@2.9.0: {}
tinyexec@1.1.1: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
tinyrainbow@3.1.0: {}
totalist@3.0.1: {}
typescript@5.6.3: {}
vite@6.4.1:
undici-types@7.19.2: {}
vite@6.4.1(@types/node@25.6.0):
dependencies:
esbuild: 0.25.12
fdir: 6.5.0(picomatch@4.0.3)
@@ -1258,10 +1482,43 @@ snapshots:
rollup: 4.57.1
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 25.6.0
fsevents: 2.3.3
vitefu@1.1.1(vite@6.4.1):
vitefu@1.1.1(vite@6.4.1(@types/node@25.6.0)):
optionalDependencies:
vite: 6.4.1
vite: 6.4.1(@types/node@25.6.0)
vitest@4.1.4(@types/node@25.6.0)(vite@6.4.1(@types/node@25.6.0)):
dependencies:
'@vitest/expect': 4.1.4
'@vitest/mocker': 4.1.4(vite@6.4.1(@types/node@25.6.0))
'@vitest/pretty-format': 4.1.4
'@vitest/runner': 4.1.4
'@vitest/snapshot': 4.1.4
'@vitest/spy': 4.1.4
'@vitest/utils': 4.1.4
es-module-lexer: 2.0.0
expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.1
pathe: 2.0.3
picomatch: 4.0.3
std-env: 4.0.0
tinybench: 2.9.0
tinyexec: 1.1.1
tinyglobby: 0.2.15
tinyrainbow: 3.1.0
vite: 6.4.1(@types/node@25.6.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 25.6.0
transitivePeerDependencies:
- msw
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
zimmerframe@1.1.4: {}
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { execSync } from "child_process";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const arg = process.argv[2];
if (!arg) {
console.error("Usage: node scripts/bump-version.js <major|minor|patch|X.Y.Z[-suffix]>");
process.exit(1);
}
function readCurrentVersion() {
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
return pkg.version;
}
function bump(current, kind) {
const match = current.match(/^(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?$/);
if (!match) {
console.error(`Cannot parse current version: ${current}`);
process.exit(1);
}
let [, maj, min, patch] = match;
maj = Number(maj);
min = Number(min);
patch = Number(patch);
if (kind === "major") {
maj += 1;
min = 0;
patch = 0;
} else if (kind === "minor") {
min += 1;
patch = 0;
} else if (kind === "patch") {
patch += 1;
} else {
console.error(`Unknown bump kind: ${kind}`);
process.exit(1);
}
return `${maj}.${min}.${patch}`;
}
const current = readCurrentVersion();
let version;
if (["major", "minor", "patch"].includes(arg)) {
version = bump(current, arg);
} else if (/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(arg)) {
version = arg;
} else {
console.error(`Invalid version: ${arg}`);
console.error("Expected: major | minor | patch | X.Y.Z[-suffix]");
process.exit(1);
}
console.log(`Bumping ${current} → ${version}`);
const changed = [];
function writeJson(filePath, mutator) {
const raw = fs.readFileSync(filePath, "utf8");
const data = JSON.parse(raw);
mutator(data);
const out = JSON.stringify(data, null, 2) + (raw.endsWith("\n") ? "\n" : "");
fs.writeFileSync(filePath, out);
changed.push(path.relative(root, filePath));
}
function writeText(filePath, mutator) {
const original = fs.readFileSync(filePath, "utf8");
const updated = mutator(original);
if (updated !== original) {
fs.writeFileSync(filePath, updated);
changed.push(path.relative(root, filePath));
}
}
writeJson(path.join(root, "package.json"), (p) => {
p.version = version;
});
const pkgLockPath = path.join(root, "package-lock.json");
if (fs.existsSync(pkgLockPath)) {
writeJson(pkgLockPath, (p) => {
p.version = version;
if (p.packages && p.packages[""]) {
p.packages[""].version = version;
}
});
}
const cargoTomlPath = path.join(root, "src-tauri", "Cargo.toml");
writeText(cargoTomlPath, (content) => {
const lines = content.split("\n");
let inPackage = false;
let replaced = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const sectionMatch = line.match(/^\[([^\]]+)\]\s*$/);
if (sectionMatch) {
inPackage = sectionMatch[1].trim() === "package";
continue;
}
if (inPackage && /^version\s*=\s*"[^"]+"/.test(line)) {
lines[i] = line.replace(/^version\s*=\s*"[^"]+"/, `version = "${version}"`);
replaced = true;
break;
}
}
if (!replaced) {
console.error("Failed to update version in Cargo.toml");
process.exit(1);
}
return lines.join("\n");
});
const cargoLockPath = path.join(root, "src-tauri", "Cargo.lock");
if (fs.existsSync(cargoLockPath)) {
writeText(cargoLockPath, (content) => {
return content.replace(
/(name = "omniget"\nversion = ")([^"]+)(")/,
(_, a, _b, c) => `${a}${version}${c}`
);
});
}
const tauriConfPath = path.join(root, "src-tauri", "tauri.conf.json");
writeJson(tauriConfPath, (conf) => {
conf.version = version;
});
const metainfoPath = path.join(root, "flatpak", "wtf.tonho.omniget.metainfo.xml");
writeText(metainfoPath, (content) => {
let updated = content.replace(
/(raw\.githubusercontent\.com\/tonhowtf\/omniget\/v)(\d+\.\d+\.\d+(?:-[\w.]+)?)(\/)/g,
(_, a, _b, c) => `${a}${version}${c}`
);
return updated;
});
const changelogStorePath = path.join(root, "src", "lib", "stores", "changelog-store.svelte.ts");
if (fs.existsSync(changelogStorePath)) {
writeText(changelogStorePath, (content) =>
content.replace(
/(currentVersion\s*=\s*")[^"]+(";)/g,
(_, a, c) => `${a}${version}${c}`
)
);
}
const aboutProjectPath = path.join(root, "src", "routes", "about", "project", "+page.svelte");
if (fs.existsSync(aboutProjectPath)) {
writeText(aboutProjectPath, (content) =>
content.replace(
/(const\s+APP_VERSION\s*=\s*")[^"]+(";)/,
(_, a, c) => `${a}${version}${c}`
)
);
}
if (changed.length === 0) {
console.error("No files changed — aborting.");
process.exit(1);
}
console.log("Updated files:");
for (const file of changed) {
console.log(` - ${file}`);
}
function hasBinary(name) {
const probe = process.platform === "win32" ? "where" : "which";
try {
execSync(`${probe} ${name}`, { stdio: "ignore" });
return true;
} catch {
return false;
}
}
const metainfoAbs = path.join(root, "flatpak", "wtf.tonho.omniget.metainfo.xml");
if (hasBinary("appstreamcli")) {
try {
execSync(`appstreamcli validate "${metainfoAbs}"`, { stdio: "inherit" });
console.log("appstreamcli validate: OK");
} catch {
console.error("appstreamcli validate failed — aborting.");
process.exit(1);
}
} else {
console.warn("appstreamcli not found in PATH — skipping metainfo validation.");
}
console.log(`v${version}`);
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const i18nDir = path.join(__dirname, "..", "src", "lib", "i18n");
const enPath = path.join(i18nDir, "en.json");
const outputFile = path.join(i18nDir, "keys.ts");
function extractKeys(obj, prefix = "") {
const keys = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
keys.push(...extractKeys(value, fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}
function generateKeysFile() {
if (!fs.existsSync(enPath)) {
console.error("Error: en.json not found at", enPath);
process.exit(1);
}
const enContent = JSON.parse(fs.readFileSync(enPath, "utf-8"));
const keys = extractKeys(enContent).sort();
const grouped = {};
for (const key of keys) {
const category = key.split(".")[0];
if (!grouped[category]) grouped[category] = [];
grouped[category].push(key);
}
let output = `/**
* i18n Translation Keys Reference
*
* AUTO-GENERATED FILE
* Run: pnpm generate:i18n-keys
*
* This file provides autocomplete hints for translation keys.
* Import the \`t\` function from '$lib/i18n' to use translations.
*
* Usage:
* \`\`\`svelte
* <script>
* import { t } from '$lib/i18n';
* </script>
*
* <h1>{\$t('app.name')}</h1>
* <p>{\$t('queue.items', { count: 5 })}</p>
* \`\`\`
*/
export type TranslationKeys =\n`;
const categories = Object.keys(grouped).sort();
for (let i = 0; i < categories.length; i++) {
const category = categories[i];
const categoryKeys = grouped[category];
for (let j = 0; j < categoryKeys.length; j++) {
const key = categoryKeys[j];
output += ` | '${key}'\n`;
}
if (i < categories.length - 1) {
output += "\n";
}
}
output += " // Allows dynamic/computed keys while preserving autocomplete for known keys\n";
output += " | (string & {});\n";
fs.writeFileSync(outputFile, output, "utf-8");
console.log(`Generated ${keys.length} translation keys in keys.ts`);
const otherLocales = fs
.readdirSync(i18nDir)
.filter((f) => f.endsWith(".json") && f !== "en.json");
let hadMismatch = false;
const enKeySet = new Set(keys);
for (const file of otherLocales) {
const p = path.join(i18nDir, file);
try {
const content = JSON.parse(fs.readFileSync(p, "utf-8"));
const localeKeys = new Set(extractKeys(content));
const missing = [...enKeySet].filter((k) => !localeKeys.has(k));
const extra = [...localeKeys].filter((k) => !enKeySet.has(k));
if (missing.length > 0 || extra.length > 0) {
hadMismatch = true;
console.warn(`[${file}] missing: ${missing.length}, extra: ${extra.length}`);
}
} catch (err) {
hadMismatch = true;
console.warn(`[${file}] failed to parse: ${err.message}`);
}
}
if (!hadMismatch) {
console.log(`All ${otherLocales.length} other locales in sync with en.json`);
}
}
generateKeysFile();
+6
View File
@@ -8,6 +8,12 @@
--padding: 10px;
--border-radius: 11px;
--sidebar-width: 80px;
--bottom-nav-height: 44px;
--mobile-nav-clearance: calc(var(--bottom-nav-height) + env(safe-area-inset-bottom, 0px) + var(--padding));
--safe-area-top: env(safe-area-inset-top, 0px);
--safe-area-right: env(safe-area-inset-right, 0px);
--safe-area-bottom: env(safe-area-inset-bottom, 0px);
--safe-area-left: env(safe-area-inset-left, 0px);
--font-system: 'DM Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: 'IBM Plex Mono', 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono', 'Ubuntu Mono', 'Courier New', monospace;
+6
View File
@@ -0,0 +1,6 @@
declare const __COMMIT_HASH__: string;
declare const __GIT_BRANCH__: string;
declare const __APP_VERSION__: string;
declare const __BUILD_DATE__: string;
declare namespace App {}
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>OmniGet</title>
%sveltekit.head%
</head>
+58
View File
@@ -0,0 +1,58 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
const invokeMock = vi.fn();
vi.mock("@tauri-apps/api/core", () => ({
invoke: (cmd: string, args: unknown) => invokeMock(cmd, args),
}));
import { pluginInvoke } from "./plugin-invoke";
describe("pluginInvoke", () => {
beforeEach(() => {
invokeMock.mockReset();
});
it("routes through the plugin_command Tauri handler", async () => {
invokeMock.mockResolvedValueOnce({ ok: true });
await pluginInvoke("courses", "hotmart_login", { email: "e", password: "p" });
expect(invokeMock).toHaveBeenCalledTimes(1);
expect(invokeMock.mock.calls[0][0]).toBe("plugin_command");
});
it("passes pluginId, command, and args in the payload", async () => {
invokeMock.mockResolvedValueOnce(undefined);
await pluginInvoke("telegram", "list_chats", { limit: 20 });
expect(invokeMock.mock.calls[0][1]).toEqual({
pluginId: "telegram",
command: "list_chats",
args: { limit: 20 },
});
});
it("defaults args to an empty object when not provided", async () => {
invokeMock.mockResolvedValueOnce("ok");
await pluginInvoke("convert", "list_formats");
expect(invokeMock.mock.calls[0][1]).toEqual({
pluginId: "convert",
command: "list_formats",
args: {},
});
});
it("propagates the resolved value", async () => {
invokeMock.mockResolvedValueOnce({ success: true, count: 3 });
const result = await pluginInvoke<{ success: boolean; count: number }>(
"courses",
"list",
);
expect(result).toEqual({ success: true, count: 3 });
});
it("propagates rejection from invoke", async () => {
invokeMock.mockRejectedValueOnce(new Error("plugin not loaded"));
await expect(pluginInvoke("missing", "cmd")).rejects.toThrow(
"plugin not loaded",
);
});
});
+56 -25
View File
@@ -1,32 +1,63 @@
import { defineConfig } from "vite";
import { sveltekit } from "@sveltejs/kit/vite";
import { execSync } from "child_process";
import { readFileSync } from "fs";
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(async () => ({
plugins: [sveltekit()],
function getGitInfo() {
try {
return {
hash: execSync("git rev-parse HEAD").toString().trim(),
branch: execSync("git rev-parse --abbrev-ref HEAD").toString().trim(),
};
} catch {
return { hash: "unknown", branch: "unknown" };
}
}
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent Vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
function getVersion() {
try {
return JSON.parse(readFileSync("package.json", "utf8")).version;
} catch {
return "0.0.0";
}
}
// https://vite.dev/config/
export default defineConfig(async () => {
const gitInfo = getGitInfo();
return {
plugins: [sveltekit()],
define: {
__COMMIT_HASH__: JSON.stringify(gitInfo.hash),
__GIT_BRANCH__: JSON.stringify(gitInfo.branch),
__APP_VERSION__: JSON.stringify(getVersion()),
__BUILD_DATE__: JSON.stringify(new Date().toISOString().split("T")[0]),
},
},
}));
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent Vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell Vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
};
});
+22
View File
@@ -0,0 +1,22 @@
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
include: ["src/**/*.test.ts"],
environment: "node",
globals: false,
},
resolve: {
alias: {
$lib: path.resolve(__dirname, "src/lib"),
$components: path.resolve(__dirname, "src/components"),
},
},
define: {
__COMMIT_HASH__: JSON.stringify("test-commit"),
__GIT_BRANCH__: JSON.stringify("test-branch"),
__APP_VERSION__: JSON.stringify("0.0.0-test"),
__BUILD_DATE__: JSON.stringify("2026-04-13"),
},
});