@voquill/desktop-utils 0.3.7 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./activation";
2
2
  export * from "./hotkey";
3
3
  export * from "./keys";
4
+ export * from "./platform";
4
5
  export * from "./tauri-events";
5
6
  export * from "./tauri-listen";
7
+ export * from "./updater";
6
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,QAAQ,CAAC;AACvB,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./activation";
2
2
  export * from "./hotkey";
3
3
  export * from "./keys";
4
+ export * from "./platform";
4
5
  export * from "./tauri-events";
5
6
  export * from "./tauri-listen";
7
+ export * from "./updater";
@@ -0,0 +1,9 @@
1
+ export type DesktopPlatform = "darwin" | "win32" | "linux";
2
+ /**
3
+ * Best-effort platform detection from `navigator.userAgent`. Works in any
4
+ * browser or Tauri webview. `navigator.platform` is deprecated so we avoid
5
+ * it. Consumers that need a build-time override should layer one on top —
6
+ * this helper has no knowledge of environment variables or build tooling.
7
+ */
8
+ export declare const detectDesktopPlatform: () => DesktopPlatform;
9
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAO,eASxC,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Best-effort platform detection from `navigator.userAgent`. Works in any
3
+ * browser or Tauri webview. `navigator.platform` is deprecated so we avoid
4
+ * it. Consumers that need a build-time override should layer one on top —
5
+ * this helper has no knowledge of environment variables or build tooling.
6
+ */
7
+ export const detectDesktopPlatform = () => {
8
+ const userAgent = navigator.userAgent.toLowerCase();
9
+ if (userAgent.includes("mac")) {
10
+ return "darwin";
11
+ }
12
+ if (userAgent.includes("win")) {
13
+ return "win32";
14
+ }
15
+ return "linux";
16
+ };
@@ -0,0 +1,52 @@
1
+ import type { DesktopPlatform } from "./platform";
2
+ export type AvailableUpdateInfo = {
3
+ currentVersion: string;
4
+ version: string;
5
+ releaseDate: string | null;
6
+ releaseNotes: string | null;
7
+ manualInstallerUrl: string | null;
8
+ requiresManualInstall: boolean;
9
+ };
10
+ export type UpdateDownloadCallbacks = {
11
+ onDownloadStarted?: (totalBytes: number | null) => void;
12
+ onDownloadProgress?: (downloadedBytes: number, totalBytes: number | null) => void;
13
+ onInstalling?: () => void;
14
+ };
15
+ export declare const shouldSurfaceUpdate: (releaseDate: string | null, optInToBetaUpdates: boolean) => boolean;
16
+ export declare const isReadOnlyFilesystemInstallError: (message: string | null | undefined) => boolean;
17
+ export declare const buildManualMacInstallerUrl: (version: string, rawJson: Record<string, unknown>) => string | null;
18
+ /**
19
+ * Probes the app install directory for writability via the
20
+ * `check_app_location_writable` Tauri command. Non-macOS always returns
21
+ * `true`. Probe failures are swallowed and default to `true` so a flaky
22
+ * probe doesn't block the update flow.
23
+ */
24
+ export declare const checkAppLocationWritable: (platform: DesktopPlatform) => Promise<boolean>;
25
+ /**
26
+ * Checks for an update. On success the underlying `Update` handle is
27
+ * retained internally so a follow-up `installAvailableUpdate()` call can
28
+ * drive the install; returns `null` when the app is already current. Throws
29
+ * on network / plugin failures — callers should catch to surface.
30
+ */
31
+ export declare const checkForUpdate: (platform: DesktopPlatform) => Promise<AvailableUpdateInfo | null>;
32
+ /** Releases the stored `Update` handle, if any. */
33
+ export declare const closeAvailableUpdate: () => Promise<void>;
34
+ /** True when `checkForUpdate` found and retained an update. */
35
+ export declare const hasAvailableUpdate: () => boolean;
36
+ /**
37
+ * Downloads and installs using the `Update` handle retained from the last
38
+ * `checkForUpdate()`. Callbacks drive the caller's own progress UI. Throws
39
+ * when no handle is available or the install fails — on macOS, callers
40
+ * should catch `isReadOnlyFilesystemInstallError(err)` and fall back to
41
+ * `downloadAndOpenMacInstaller`.
42
+ */
43
+ export declare const installAvailableUpdate: (callbacks?: UpdateDownloadCallbacks) => Promise<void>;
44
+ /**
45
+ * Downloads a `.pkg` installer to a temp directory and opens it via macOS
46
+ * Installer.app. Used as a fallback when the in-place updater cannot write
47
+ * to the app's install location.
48
+ */
49
+ export declare const downloadAndOpenMacInstaller: (url: string) => Promise<void>;
50
+ /** Relaunches the app via Tauri's process plugin. */
51
+ export declare const relaunchApp: () => Promise<void>;
52
+ //# sourceMappingURL=updater.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updater.d.ts","sourceRoot":"","sources":["../src/updater.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA+DlD,MAAM,MAAM,mBAAmB,GAAG;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,kBAAkB,CAAC,EAAE,CACnB,eAAe,EAAE,MAAM,EACvB,UAAU,EAAE,MAAM,GAAG,IAAI,KACtB,IAAI,CAAC;IACV,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAIF,eAAO,MAAM,mBAAmB,GAC9B,aAAa,MAAM,GAAG,IAAI,EAC1B,oBAAoB,OAAO,KAC1B,OAeF,CAAC;AAEF,eAAO,MAAM,gCAAgC,GAC3C,SAAS,MAAM,GAAG,IAAI,GAAG,SAAS,KACjC,OAYF,CAAC;AA+BF,eAAO,MAAM,0BAA0B,GACrC,SAAS,MAAM,EACf,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC/B,MAAM,GAAG,IAQX,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GACnC,UAAU,eAAe,KACxB,OAAO,CAAC,OAAO,CAUjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,eAAe,KACxB,OAAO,CAAC,mBAAmB,GAAG,IAAI,CA+BpC,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,oBAAoB,QAAa,OAAO,CAAC,IAAI,CAWzD,CAAC;AAEF,+DAA+D;AAC/D,eAAO,MAAM,kBAAkB,QAAO,OAAmC,CAAC;AAE1E;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GACjC,YAAY,uBAAuB,KAClC,OAAO,CAAC,IAAI,CA4Bd,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,GACtC,KAAK,MAAM,KACV,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,IAAI,CAEhD,CAAC"}
@@ -0,0 +1,197 @@
1
+ import { Channel, invoke, Resource } from "@tauri-apps/api/core";
2
+ class Update extends Resource {
3
+ constructor(metadata) {
4
+ super(metadata.rid);
5
+ this.currentVersion = metadata.currentVersion;
6
+ this.version = metadata.version;
7
+ this.date = metadata.date;
8
+ this.body = metadata.body;
9
+ this.rawJson = metadata.rawJson;
10
+ }
11
+ async downloadAndInstall(onEvent) {
12
+ const channel = new Channel();
13
+ if (onEvent) {
14
+ channel.onmessage = onEvent;
15
+ }
16
+ await invoke("plugin:updater|download_and_install", {
17
+ onEvent: channel,
18
+ rid: this.rid,
19
+ });
20
+ }
21
+ }
22
+ const check = async () => {
23
+ const metadata = await invoke("plugin:updater|check");
24
+ return metadata ? new Update(metadata) : null;
25
+ };
26
+ const relaunch = async () => {
27
+ await invoke("plugin:process|restart");
28
+ };
29
+ const GITHUB_RELEASE_DOWNLOAD_BASE = "https://github.com/voquill/voquill/releases/download";
30
+ const RELEASE_TAG_REGEX = /\/releases\/download\/([^/]+)\//;
31
+ const BETA_UPDATE_DELAY_MS = 3 * 24 * 60 * 60 * 1000;
32
+ let availableUpdate = null;
33
+ export const shouldSurfaceUpdate = (releaseDate, optInToBetaUpdates) => {
34
+ if (optInToBetaUpdates) {
35
+ return true;
36
+ }
37
+ if (!releaseDate) {
38
+ return true;
39
+ }
40
+ const parsed = new Date(releaseDate).getTime();
41
+ if (Number.isNaN(parsed)) {
42
+ return true;
43
+ }
44
+ return Date.now() - parsed >= BETA_UPDATE_DELAY_MS;
45
+ };
46
+ export const isReadOnlyFilesystemInstallError = (message) => {
47
+ if (!message) {
48
+ return false;
49
+ }
50
+ const normalized = message.toLowerCase();
51
+ return (normalized.includes("read-only file system") ||
52
+ normalized.includes("os error 30") ||
53
+ normalized.includes("cross-device link") ||
54
+ normalized.includes("os error 18"));
55
+ };
56
+ const isRecord = (value) => typeof value === "object" && value !== null;
57
+ const extractReleaseTagFromRawJson = (rawJson) => {
58
+ const platforms = rawJson.platforms;
59
+ if (!isRecord(platforms)) {
60
+ return null;
61
+ }
62
+ for (const platform of Object.values(platforms)) {
63
+ if (!isRecord(platform)) {
64
+ continue;
65
+ }
66
+ const url = platform.url;
67
+ if (typeof url !== "string") {
68
+ continue;
69
+ }
70
+ const match = RELEASE_TAG_REGEX.exec(url);
71
+ if (match?.[1]) {
72
+ return match[1];
73
+ }
74
+ }
75
+ return null;
76
+ };
77
+ export const buildManualMacInstallerUrl = (version, rawJson) => {
78
+ const releaseTag = extractReleaseTagFromRawJson(rawJson);
79
+ if (!releaseTag) {
80
+ return null;
81
+ }
82
+ const fileName = `Voquill_${version}_universal.pkg`;
83
+ return `${GITHUB_RELEASE_DOWNLOAD_BASE}/${encodeURIComponent(releaseTag)}/${encodeURIComponent(fileName)}`;
84
+ };
85
+ /**
86
+ * Probes the app install directory for writability via the
87
+ * `check_app_location_writable` Tauri command. Non-macOS always returns
88
+ * `true`. Probe failures are swallowed and default to `true` so a flaky
89
+ * probe doesn't block the update flow.
90
+ */
91
+ export const checkAppLocationWritable = async (platform) => {
92
+ if (platform !== "darwin") {
93
+ return true;
94
+ }
95
+ try {
96
+ return await invoke("check_app_location_writable");
97
+ }
98
+ catch (error) {
99
+ console.error("Failed to check app location writability", error);
100
+ return true;
101
+ }
102
+ };
103
+ /**
104
+ * Checks for an update. On success the underlying `Update` handle is
105
+ * retained internally so a follow-up `installAvailableUpdate()` call can
106
+ * drive the install; returns `null` when the app is already current. Throws
107
+ * on network / plugin failures — callers should catch to surface.
108
+ */
109
+ export const checkForUpdate = async (platform) => {
110
+ const update = await check();
111
+ if (!update) {
112
+ await closeAvailableUpdate();
113
+ return null;
114
+ }
115
+ if (availableUpdate && availableUpdate !== update) {
116
+ try {
117
+ await availableUpdate.close();
118
+ }
119
+ catch (error) {
120
+ console.error("Failed to close previous update resource", error);
121
+ }
122
+ }
123
+ availableUpdate = update;
124
+ const requiresManualInstall = platform === "darwin" ? !(await checkAppLocationWritable(platform)) : false;
125
+ return {
126
+ currentVersion: update.currentVersion,
127
+ version: update.version,
128
+ releaseDate: update.date ?? null,
129
+ releaseNotes: update.body ?? null,
130
+ manualInstallerUrl: platform === "darwin"
131
+ ? buildManualMacInstallerUrl(update.version, update.rawJson)
132
+ : null,
133
+ requiresManualInstall,
134
+ };
135
+ };
136
+ /** Releases the stored `Update` handle, if any. */
137
+ export const closeAvailableUpdate = async () => {
138
+ if (!availableUpdate) {
139
+ return;
140
+ }
141
+ try {
142
+ await availableUpdate.close();
143
+ }
144
+ catch (error) {
145
+ console.error("Failed to close update resource", error);
146
+ }
147
+ finally {
148
+ availableUpdate = null;
149
+ }
150
+ };
151
+ /** True when `checkForUpdate` found and retained an update. */
152
+ export const hasAvailableUpdate = () => availableUpdate !== null;
153
+ /**
154
+ * Downloads and installs using the `Update` handle retained from the last
155
+ * `checkForUpdate()`. Callbacks drive the caller's own progress UI. Throws
156
+ * when no handle is available or the install fails — on macOS, callers
157
+ * should catch `isReadOnlyFilesystemInstallError(err)` and fall back to
158
+ * `downloadAndOpenMacInstaller`.
159
+ */
160
+ export const installAvailableUpdate = async (callbacks) => {
161
+ const update = availableUpdate;
162
+ if (!update) {
163
+ throw new Error("No available update to install");
164
+ }
165
+ let downloadedBytes = 0;
166
+ let totalBytes = null;
167
+ const handleEvent = (event) => {
168
+ switch (event.event) {
169
+ case "Started":
170
+ totalBytes = event.data.contentLength ?? null;
171
+ callbacks?.onDownloadStarted?.(totalBytes);
172
+ break;
173
+ case "Progress":
174
+ downloadedBytes += event.data.chunkLength;
175
+ callbacks?.onDownloadProgress?.(downloadedBytes, totalBytes);
176
+ break;
177
+ case "Finished":
178
+ callbacks?.onInstalling?.();
179
+ break;
180
+ default:
181
+ break;
182
+ }
183
+ };
184
+ await update.downloadAndInstall(handleEvent);
185
+ };
186
+ /**
187
+ * Downloads a `.pkg` installer to a temp directory and opens it via macOS
188
+ * Installer.app. Used as a fallback when the in-place updater cannot write
189
+ * to the app's install location.
190
+ */
191
+ export const downloadAndOpenMacInstaller = async (url) => {
192
+ await invoke("download_and_open_mac_installer", { url });
193
+ };
194
+ /** Relaunches the app via Tauri's process plugin. */
195
+ export const relaunchApp = async () => {
196
+ await relaunch();
197
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voquill/desktop-utils",
3
- "version": "0.3.7",
3
+ "version": "0.3.11",
4
4
  "description": "Shared React hooks and logic for Voquill desktop-style clients.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,7 +18,7 @@
18
18
  "@types/react": "^19.0.0",
19
19
  "react": "^19.2.4",
20
20
  "typescript": "5.9.2",
21
- "@voquill/typescript-config": "0.3.7"
21
+ "@voquill/typescript-config": "0.3.11"
22
22
  },
23
23
  "files": [
24
24
  "dist"