@voquill/desktop-utils 0.3.7 → 0.3.10
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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/platform.d.ts +9 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +16 -0
- package/dist/updater.d.ts +52 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +197 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
@@ -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"}
|
package/dist/platform.js
ADDED
|
@@ -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"}
|
package/dist/updater.js
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.3.10",
|
|
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.
|
|
21
|
+
"@voquill/typescript-config": "0.3.10"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist"
|