Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
- Do not lower coverage thresholds without explicit maintainer approval.
- Keep durable domain and business behavior out of UI components where applicable, without adding DDD ceremony.
- Use Bun workspace commands from the repo root: `bun run typecheck`, `bun run test`, `bun run test:coverage`, and `bun run build`.

## Web Components

- Stateful DOM nodes (dialog, live regions) must stay stable across renders — full innerHTML rebuilds on state ticks replay showModal/animations and break assistive-tech announcements.

## Workspace policy

For substantial work, read `../AGENTS.md` (workspace root) and use the `plan-issue` workflow — GitHub Issues are the canonical plan/progress tracker.
Expand Down
21 changes: 21 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"packages/*"
],
"scripts": {
"build": "bun run --cwd packages/tauri-release build && bun run --cwd packages/auth build && bun run --cwd packages/brand build && bun run --cwd packages/flags build && bun run --cwd packages/billing build && bun run --cwd packages/edge-shared build && bun run --cwd packages/sync build",
"build": "bun run --cwd packages/tauri-release build && bun run --cwd packages/tauri-updater build && bun run --cwd packages/auth build && bun run --cwd packages/brand build && bun run --cwd packages/flags build && bun run --cwd packages/billing build && bun run --cwd packages/edge-shared build && bun run --cwd packages/sync build",
"test": "vitest run",
"test:supabase": "supabase test db supabase/tests/database --local && bun run supabase/tests/welcome-credits-concurrency.ts && bun test packages/billing/test/checkout-lifecycle.contract.test.ts && bun test packages/sync/test/lww.contract.test.ts && bun test packages/edge-shared/test/router-attempt.contract.test.ts",
"test:coverage": "vitest run --coverage",
Expand All @@ -19,6 +19,7 @@
"@types/bun": "^1.3.5",
"@types/node": "^24.10.1",
"@vitest/coverage-v8": "^4.0.14",
"happy-dom": "^20.8.4",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"vitest": "^4.0.14"
Expand Down
31 changes: 31 additions & 0 deletions packages/tauri-updater/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# @pickforge/tauri-updater

Framework-neutral update control and a branded `<pickforge-update-dialog>` Web Component for Tauri desktop apps.

```ts
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import {
createTauriUpdaterAdapter,
createUpdateController,
definePickforgeUpdaterElement,
} from "@pickforge/tauri-updater";

const controller = createUpdateController({
adapter: createTauriUpdaterAdapter({ check, relaunch }),
eligibility: {
// Resolve only after the packaged app's focused, visible main window is ready.
whenEligible: async () => true,
},
});

definePickforgeUpdaterElement();
const dialog = document.querySelector("pickforge-update-dialog");
dialog.metadata = { productName: "PickForge", currentVersion: "1.0.0" };
dialog.controller = controller;
void controller.start();
```

`start()` silently ignores unavailable feeds and no-update responses, checking once per process through the shared gate. `check({ manual: true })` is for a Settings-style "check for updates" action: it clears a `dismissed` state, always performs a fresh `adapter.check()` bypassing the process gate's cached result, and reports failures as a retryable `error` state instead of staying silent. A `dismissed` state from `dismiss()` only blocks the automatic `start()`/`check()` path; a late-resolving automatic check that finds an update after the user has since dismissed keeps the `dismissed` status (retaining the update for bookkeeping) rather than reopening the dialog. The default process gate shares the first check across controllers; inject `createProcessCheckGate()` to isolate deterministic tests.

Build the package and serve `fixture/` from the repository root to inspect the standalone states.
93 changes: 93 additions & 0 deletions packages/tauri-updater/fixture/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pickforge updater fixture</title>
<link rel="stylesheet" href="../../brand/src/tokens.css" />
<link rel="stylesheet" href="../../brand/src/fonts.css" />
<style>
body { background: var(--pf-surface); color: var(--pf-text-hi); font-family: var(--pf-font-sans); margin: 0; min-height: 100vh; }
main { padding: 32px; }
nav { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
nav a { color: var(--pf-text-med); font-family: var(--pf-font-mono); font-size: 12px; }
nav a[aria-current="page"] { color: var(--pf-ember); }
</style>
</head>
<body>
<main>
<h1>Updater fixture</h1>
<p id="note">The app remains usable behind the modal.</p>
<nav>
<a href="?scenario=available">Available</a>
<a href="?scenario=indeterminate">Indeterminate download</a>
<a href="?scenario=check-error">Check fails</a>
<a href="?scenario=install-error">Install fails</a>
<a href="?scenario=no-update">No update</a>
</nav>
</main>
<pickforge-update-dialog></pickforge-update-dialog>
<script type="module">
import {
createEligibility,
createProcessCheckGate,
createUpdateController,
definePickforgeUpdaterElement,
} from "../dist/index.js";

const scenario = new URLSearchParams(location.search).get("scenario") ?? "available";
for (const link of document.querySelectorAll("nav a")) {
if (new URL(link.href).searchParams.get("scenario") === scenario) {
link.setAttribute("aria-current", "page");
}
}

const notes = {
available: "Click “Update & restart” to run the determinate download.",
indeterminate: "Click “Update & restart” to run a download with no content length.",
"check-error": "The startup check stays silent; a manual check surfaces the retryable error.",
"install-error": "Click “Update & restart”; the install fails after some progress.",
"no-update": "The startup check found nothing, so no dialog opens.",
};
document.querySelector("#note").textContent = notes[scenario] ?? notes.available;

const adapter = {
async check() {
if (scenario === "check-error") throw new Error("Could not reach the update feed.");
if (scenario === "no-update") return null;
return {
version: "1.4.0",
notes: "Faster startup\nMore reliable signed updates\nImproved keyboard navigation",
};
},
async downloadAndInstall(onEvent) {
onEvent(
scenario === "indeterminate"
? { type: "started" }
: { type: "started", contentLength: 100 },
);
for (const downloaded of [18, 24, 31, 27]) {
await new Promise((resolve) => setTimeout(resolve, 300));
onEvent({ type: "progress", chunkLength: downloaded });
}
if (scenario === "install-error") throw new Error("Signature verification failed.");
onEvent({ type: "finished" });
},
async relaunch() {},
};
const controller = createUpdateController({
adapter,
eligibility: createEligibility({ packaged: true, mainWindow: true, visible: true, focused: true }),
gate: createProcessCheckGate(),
});
definePickforgeUpdaterElement();
const element = document.querySelector("pickforge-update-dialog");
element.metadata = { productName: "PickForge", currentVersion: "1.3.2", productMark: "PF" };
element.controller = controller;
await controller.start();
if (scenario === "check-error") {
await controller.check({ manual: true });
}
</script>
</body>
</html>
25 changes: 25 additions & 0 deletions packages/tauri-updater/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@pickforge/tauri-updater",
"version": "0.12.0",
"description": "Framework-neutral Tauri update controller and Pickforge update dialog.",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/pickforge/pickforge-platform.git",
"directory": "packages/tauri-updater"
},
"type": "module",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts --clean --splitting false --out-dir dist"
},
"publishConfig": {
"access": "public"
}
}
Loading