tabby-rdp 0.5.1 → 0.5.2
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/CHANGELOG.md +26 -0
- package/README.md +40 -11
- package/TROUBLESHOOTING.md +77 -0
- package/dist/accountRevisions.js +46 -0
- package/dist/accounts.js +2 -1
- package/dist/desktop.service.js +59 -27
- package/dist/desktopForm.js +2 -1
- package/dist/diskStorage.js +212 -0
- package/dist/fileTransfer.js +6 -53
- package/dist/help.js +10 -62
- package/dist/hostCompat.js +86 -0
- package/dist/index.js +5 -2
- package/dist/packageChangeDialog.js +186 -0
- package/dist/releaseSettings.js +72 -0
- package/dist/releases.js +408 -0
- package/dist/settingsPage.js +91 -101
- package/dist/signin.js +73 -10
- package/dist/targets.js +9 -1
- package/dist/updates.js +77 -89
- package/docs/desk.md +157 -0
- package/package.json +11 -2
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PackageChangeDialog = void 0;
|
|
4
|
+
const STYLE = `
|
|
5
|
+
.trd-package-dialog { width: min(460px, calc(100vw - 40px)); padding: 24px; border-radius: 8px; outline: none;
|
|
6
|
+
border: 1px solid rgba(128,128,128,.35); background: var(--bs-body-bg, #1b1b1b); color: var(--bs-body-color, #ddd);
|
|
7
|
+
box-shadow: 0 16px 60px rgba(0,0,0,.45); }
|
|
8
|
+
.trd-package-dialog::backdrop { background: rgba(0,0,0,.55); }
|
|
9
|
+
.trd-package-dialog h4 { font-size: 18px; margin: 0 0 16px; }
|
|
10
|
+
.trd-package-dialog p { white-space: pre-line; overflow-wrap: anywhere; }
|
|
11
|
+
.trd-package-dialog [data-detail] { font-size: 13px; opacity: .8; }
|
|
12
|
+
.trd-package-dialog p:empty { display: none; }
|
|
13
|
+
.trd-package-dialog progress { display: block; width: 100%; height: 8px; margin: 18px 0 8px; accent-color: #589cf0; }
|
|
14
|
+
.trd-package-dialog [hidden] { display: none !important; }
|
|
15
|
+
.trd-package-dialog [data-elapsed] { font-size: 12px; opacity: .7; }
|
|
16
|
+
.trd-package-dialog [data-phase=error] { color: var(--bs-danger, #f08080); }
|
|
17
|
+
.trd-package-dialog [data-phase=success] { color: var(--bs-success, #79c99b); }
|
|
18
|
+
.trd-package-dialog .trd-package-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 22px; }
|
|
19
|
+
`;
|
|
20
|
+
/** One modal remains open from confirmation through the installer's verified result. */
|
|
21
|
+
class PackageChangeDialog {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.dialog = null;
|
|
24
|
+
this.previousFocus = null;
|
|
25
|
+
this.started = 0;
|
|
26
|
+
}
|
|
27
|
+
update(state) {
|
|
28
|
+
if (state.phase === 'cancelled') {
|
|
29
|
+
this.close();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.open();
|
|
33
|
+
this.field('[data-title]').textContent = state.action === 'install'
|
|
34
|
+
? `Install tabby-rdp ${state.version}` : 'Uninstall tabby-rdp';
|
|
35
|
+
const result = this.field('[data-result]');
|
|
36
|
+
result.dataset.phase = state.phase;
|
|
37
|
+
result.setAttribute('role', state.phase === 'error' ? 'alert' : 'status');
|
|
38
|
+
const complete = state.phase === 'success' || state.phase === 'error';
|
|
39
|
+
this.field('progress').hidden = complete;
|
|
40
|
+
this.field('[data-elapsed]').hidden = complete;
|
|
41
|
+
this.field('[data-cancel]').hidden = true;
|
|
42
|
+
this.field('[data-confirm]').hidden = true;
|
|
43
|
+
this.field('[data-close]').hidden = !complete;
|
|
44
|
+
this.field('[data-detail]').textContent = '';
|
|
45
|
+
let message = state.message ?? '';
|
|
46
|
+
if (state.phase === 'preparing') {
|
|
47
|
+
message = state.action === 'install' ? 'Checking compatibility…' : 'Checking installation…';
|
|
48
|
+
}
|
|
49
|
+
if (state.phase === 'installing') {
|
|
50
|
+
message = `Installing tabby-rdp ${state.version}…`;
|
|
51
|
+
}
|
|
52
|
+
if (state.phase === 'uninstalling') {
|
|
53
|
+
message = 'Uninstalling tabby-rdp…';
|
|
54
|
+
}
|
|
55
|
+
if (state.phase === 'verifying') {
|
|
56
|
+
message = state.action === 'install' ? 'Verifying installation…' : 'Verifying removal…';
|
|
57
|
+
}
|
|
58
|
+
if (state.phase === 'success') {
|
|
59
|
+
this.field('[data-title]').textContent = state.action === 'install' ? 'Installation complete' : 'Uninstall complete';
|
|
60
|
+
if (!state.message) {
|
|
61
|
+
message = state.action === 'install' ? `tabby-rdp ${state.version} is installed.` : 'tabby-rdp is uninstalled.';
|
|
62
|
+
this.field('[data-detail]').textContent = 'Close remote desktops, then restart Tabby.';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (state.phase === 'error') {
|
|
66
|
+
this.field('[data-title]').textContent = state.action === 'install' ? 'Installation failed' : 'Uninstall failed';
|
|
67
|
+
}
|
|
68
|
+
this.field('[data-message]').textContent = message;
|
|
69
|
+
if (complete) {
|
|
70
|
+
this.stopTimer();
|
|
71
|
+
this.field('[data-close]').focus();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.dialog.focus();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
confirm(message, detail, action) {
|
|
78
|
+
this.open();
|
|
79
|
+
this.stopTimer();
|
|
80
|
+
this.field('[data-title]').textContent = message;
|
|
81
|
+
this.field('[data-message]').textContent = detail;
|
|
82
|
+
this.field('[data-detail]').textContent = '';
|
|
83
|
+
this.field('progress').hidden = true;
|
|
84
|
+
this.field('[data-elapsed]').hidden = true;
|
|
85
|
+
this.field('[data-close]').hidden = true;
|
|
86
|
+
const cancel = this.field('[data-cancel]');
|
|
87
|
+
const accept = this.field('[data-confirm]');
|
|
88
|
+
cancel.hidden = false;
|
|
89
|
+
accept.hidden = false;
|
|
90
|
+
accept.textContent = action;
|
|
91
|
+
this.field('[data-result]').dataset.phase = 'confirming';
|
|
92
|
+
cancel.focus();
|
|
93
|
+
return new Promise(resolve => { this.confirmation = resolve; });
|
|
94
|
+
}
|
|
95
|
+
field(selector) { return this.dialog.querySelector(selector); }
|
|
96
|
+
open() {
|
|
97
|
+
if (this.dialog) {
|
|
98
|
+
if (!this.dialog.open) {
|
|
99
|
+
this.dialog.showModal();
|
|
100
|
+
}
|
|
101
|
+
if (!this.timer) {
|
|
102
|
+
this.startTimer();
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!document.getElementById('trd-package-dialog-style')) {
|
|
107
|
+
const style = document.createElement('style');
|
|
108
|
+
style.id = 'trd-package-dialog-style';
|
|
109
|
+
style.textContent = STYLE;
|
|
110
|
+
document.head.appendChild(style);
|
|
111
|
+
}
|
|
112
|
+
this.previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
113
|
+
const dialog = document.createElement('dialog');
|
|
114
|
+
this.dialog = dialog;
|
|
115
|
+
dialog.className = 'trd-package-dialog';
|
|
116
|
+
dialog.setAttribute('aria-labelledby', 'trd-package-dialog-title');
|
|
117
|
+
dialog.setAttribute('tabindex', '-1');
|
|
118
|
+
dialog.setAttribute('closedby', 'none');
|
|
119
|
+
dialog.innerHTML = `<h4 id="trd-package-dialog-title" data-title></h4>
|
|
120
|
+
<div data-result role="status" aria-live="polite"><p data-message></p><p data-detail></p></div>
|
|
121
|
+
<progress aria-label="Package change progress"></progress><div data-elapsed></div>
|
|
122
|
+
<div class="trd-package-actions"><button class="btn btn-secondary" data-cancel hidden>Cancel</button>
|
|
123
|
+
<button class="btn btn-primary" data-confirm hidden></button><button class="btn btn-primary" data-close hidden>Close</button></div>`;
|
|
124
|
+
this.field('[data-confirm]').addEventListener('click', () => this.decide(true));
|
|
125
|
+
this.field('[data-cancel]').addEventListener('click', () => this.decide(false));
|
|
126
|
+
this.field('[data-close]').addEventListener('click', () => this.close());
|
|
127
|
+
dialog.addEventListener('cancel', event => {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
this.dismiss();
|
|
130
|
+
});
|
|
131
|
+
// Keep package-dialog keystrokes out of Tabby's terminal and shortcut handlers.
|
|
132
|
+
dialog.addEventListener('keydown', event => {
|
|
133
|
+
event.stopPropagation();
|
|
134
|
+
if (event.key === 'Escape') {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
this.dismiss();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
dialog.addEventListener('keyup', event => event.stopPropagation());
|
|
140
|
+
document.body.appendChild(dialog);
|
|
141
|
+
dialog.showModal();
|
|
142
|
+
this.startTimer();
|
|
143
|
+
}
|
|
144
|
+
decide(accepted) {
|
|
145
|
+
const resolve = this.confirmation;
|
|
146
|
+
if (!resolve) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.confirmation = undefined;
|
|
150
|
+
this.field('[data-confirm]').hidden = true;
|
|
151
|
+
this.field('[data-cancel]').hidden = true;
|
|
152
|
+
resolve(accepted);
|
|
153
|
+
if (!accepted) {
|
|
154
|
+
this.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
dismiss() {
|
|
158
|
+
if (this.confirmation) {
|
|
159
|
+
this.decide(false);
|
|
160
|
+
}
|
|
161
|
+
else if (!this.field('[data-close]').hidden) {
|
|
162
|
+
this.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
startTimer() {
|
|
166
|
+
this.started = Date.now();
|
|
167
|
+
const tick = () => {
|
|
168
|
+
const seconds = Math.floor((Date.now() - this.started) / 1000);
|
|
169
|
+
this.field('[data-elapsed]').textContent = `Elapsed ${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
|
|
170
|
+
};
|
|
171
|
+
tick();
|
|
172
|
+
this.timer = setInterval(tick, 1000);
|
|
173
|
+
}
|
|
174
|
+
stopTimer() { clearInterval(this.timer); this.timer = undefined; }
|
|
175
|
+
close() {
|
|
176
|
+
this.stopTimer();
|
|
177
|
+
this.dialog?.close();
|
|
178
|
+
this.dialog?.remove();
|
|
179
|
+
this.dialog = null;
|
|
180
|
+
if (this.previousFocus?.isConnected) {
|
|
181
|
+
this.previousFocus.focus();
|
|
182
|
+
}
|
|
183
|
+
this.previousFocus = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.PackageChangeDialog = PackageChangeDialog;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderReleaseSettings = renderReleaseSettings;
|
|
4
|
+
const help_1 = require("./help");
|
|
5
|
+
/** Rebuilt from manager state; every package mutation stays behind the manager's confirmation and validation. */
|
|
6
|
+
function renderReleaseSettings(root, releases, actions) {
|
|
7
|
+
const oldSelection = root.dataset.releaseChannel === releases.state.channel ? root.querySelector('[data-version]')?.value : undefined;
|
|
8
|
+
root.dataset.releaseChannel = releases.state.channel;
|
|
9
|
+
const selected = releases.versions.some(p => p.version === oldSelection) ? oldSelection : releases.recommended ?? '';
|
|
10
|
+
const blocked = releases.busy || releases.checking || !!releases.pending || releases.removed;
|
|
11
|
+
const disable = (condition) => condition ? 'disabled' : '';
|
|
12
|
+
const reason = selected ? releases.reason(selected) : null;
|
|
13
|
+
const stable = releases.catalog?.stable;
|
|
14
|
+
const previous = releases.state.previous;
|
|
15
|
+
const status = releases.checking ? 'Checking…' : releases.checkedAt !== null ? `Last checked ${new Date(releases.checkedAt).toLocaleString()}.` : 'Not checked';
|
|
16
|
+
root.innerHTML = `
|
|
17
|
+
<div class="trd-release-summary">
|
|
18
|
+
<div><span class="trd-sub">Running version</span><h4>tabby-rdp ${(0, help_1.esc)(releases.running)}</h4></div>
|
|
19
|
+
<button class="btn btn-link btn-sm" data-release-notes="${(0, help_1.esc)(releases.running)}">Release notes</button>
|
|
20
|
+
</div>
|
|
21
|
+
${releases.pending ? `<div class="trd-release-notice" role="status"><b>Restart required to use ${(0, help_1.esc)(releases.pending)}.</b> Close remote desktops and restart Tabby.</div>` : ''}
|
|
22
|
+
${releases.removed ? '<div class="trd-release-notice" role="status"><b>Plugin uninstalled.</b> Close remote desktops and restart Tabby.</div>' : ''}
|
|
23
|
+
${releases.notice ? `<p role="status">${(0, help_1.esc)(releases.notice)}</p>` : ''}
|
|
24
|
+
${releases.storageWarning ? `<p role="alert">${(0, help_1.esc)(releases.storageWarning)}</p>` : ''}
|
|
25
|
+
<div class="form-line">
|
|
26
|
+
<div class="header"><label class="title" for="trd-release-channel">Release channel</label><div class="description">Preview includes beta releases and release candidates.</div></div>
|
|
27
|
+
<select id="trd-release-channel" class="form-control" data-channel ${disable(blocked)}><option value="stable" ${releases.state.channel === 'stable' ? 'selected' : ''}>Stable</option><option value="preview" ${releases.state.channel === 'preview' ? 'selected' : ''}>Preview</option></select>
|
|
28
|
+
</div>
|
|
29
|
+
<label class="trd-release-pause"><input type="checkbox" data-pause ${disable(releases.busy)} ${!releases.state.paused ? 'checked' : ''}> Check for updates automatically</label>
|
|
30
|
+
<div class="trd-release-actions"><button class="btn btn-secondary btn-sm" data-check ${disable(releases.checking || releases.busy)}>Check now</button><span class="trd-sub" role="status">${(0, help_1.esc)(status)}</span></div>
|
|
31
|
+
${releases.error ? `<p class="trd-release-error" role="alert">${(0, help_1.esc)(releases.error)}${releases.catalog ? ' Showing the last successful check.' : ''}</p>` : ''}
|
|
32
|
+
${releases.state.channel === 'preview' && releases.catalog && !releases.catalog.preview ? '<p>No preview releases available.</p>' : ''}
|
|
33
|
+
${releases.recommended ? `<p>Latest: <b>${(0, help_1.esc)(releases.recommended)}</b> <button class="btn btn-link btn-sm" data-release-notes="${(0, help_1.esc)(releases.recommended)}">Release notes</button></p>` : ''}
|
|
34
|
+
<h5><label for="trd-release-version">Version</label></h5>
|
|
35
|
+
<div class="trd-release-actions">
|
|
36
|
+
<select id="trd-release-version" class="form-control" data-version ${disable(blocked || !releases.versions.length)}>
|
|
37
|
+
${!releases.versions.length ? '<option>No versions loaded</option>' : releases.versions.map(p => {
|
|
38
|
+
const why = releases.reason(p.version);
|
|
39
|
+
return `<option value="${(0, help_1.esc)(p.version)}" ${p.version === selected ? 'selected' : ''} ${disable(!!why)}>${(0, help_1.esc)(p.version)}${why ? ` — ${(0, help_1.esc)(why)}` : ''}</option>`;
|
|
40
|
+
}).join('')}
|
|
41
|
+
</select>
|
|
42
|
+
<button class="btn btn-primary btn-sm" data-install ${disable(blocked || !!releases.error || !selected || !!reason || selected === releases.running)}>Install selected version…</button>
|
|
43
|
+
<button class="btn btn-link btn-sm" data-selected-notes ${disable(!selected)}>Release notes</button>
|
|
44
|
+
</div>
|
|
45
|
+
<p class="trd-sub" data-version-reason ${reason ? '' : 'hidden'}>${(0, help_1.esc)(reason ?? '')}</p>
|
|
46
|
+
<div class="trd-release-actions">
|
|
47
|
+
${stable && stable !== releases.running ? `<button class="btn btn-secondary btn-sm" data-stable ${disable(blocked || !!releases.error || !!releases.reason(stable))}>Return to stable ${(0, help_1.esc)(stable)}…</button>` : ''}
|
|
48
|
+
<button class="btn btn-secondary btn-sm" data-restore ${disable(blocked || !!releases.error || !previous || !!releases.reason(previous) || previous === releases.running)}>Restore previous version${previous ? ` (${(0, help_1.esc)(previous)})` : ''}…</button>
|
|
49
|
+
</div>
|
|
50
|
+
${!previous ? '<p class="trd-sub">No previous version saved.</p>' : releases.reason(previous) ? `<p class="trd-sub">Previous version unavailable: ${(0, help_1.esc)(releases.reason(previous))}</p>` : ''}
|
|
51
|
+
<h5>Disable or uninstall</h5>
|
|
52
|
+
<p class="trd-sub">Saved connections, settings, passwords and remote setup are kept.</p>
|
|
53
|
+
<div class="trd-release-actions"><button class="btn btn-secondary btn-sm" data-plugins>Manage in Tabby…</button><button class="btn btn-outline-danger btn-sm" data-uninstall ${disable(blocked)}>Uninstall plugin…</button><a href="#" data-recovery>Cleanup instructions</a></div>`;
|
|
54
|
+
root.querySelector('[data-channel]').addEventListener('change', e => {
|
|
55
|
+
void releases.setChannel(e.target.value);
|
|
56
|
+
});
|
|
57
|
+
root.querySelector('[data-pause]').addEventListener('change', e => { void releases.setPaused(!e.target.checked); });
|
|
58
|
+
root.querySelector('[data-check]').addEventListener('click', () => { void releases.refreshCatalog(true); });
|
|
59
|
+
root.querySelector('[data-version]').addEventListener('change', () => renderReleaseSettings(root, releases, actions));
|
|
60
|
+
root.querySelector('[data-install]').addEventListener('click', () => { void releases.install(selected); });
|
|
61
|
+
root.querySelector('[data-stable]')?.addEventListener('click', () => { if (stable) {
|
|
62
|
+
void releases.install(stable, true);
|
|
63
|
+
} });
|
|
64
|
+
root.querySelector('[data-restore]').addEventListener('click', () => { if (previous) {
|
|
65
|
+
void releases.install(previous);
|
|
66
|
+
} });
|
|
67
|
+
root.querySelector('[data-uninstall]').addEventListener('click', () => { void releases.uninstall(); });
|
|
68
|
+
root.querySelector('[data-plugins]').addEventListener('click', actions.plugins);
|
|
69
|
+
root.querySelector('[data-recovery]').addEventListener('click', e => { e.preventDefault(); actions.link(`${help_1.TROUBLESHOOTING_URL}#recovery`); });
|
|
70
|
+
root.querySelector('[data-selected-notes]').addEventListener('click', () => actions.notes(selected));
|
|
71
|
+
root.querySelectorAll('[data-release-notes]').forEach(el => el.addEventListener('click', () => actions.notes(el.dataset.releaseNotes)));
|
|
72
|
+
}
|
package/dist/releases.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ReleaseManager = exports.RELEASE_STATE_KEY = exports.RELEASES = exports.REGISTRY = void 0;
|
|
4
|
+
exports.releaseVersion = releaseVersion;
|
|
5
|
+
exports.newer = newer;
|
|
6
|
+
exports.registryJSON = registryJSON;
|
|
7
|
+
exports.parseCatalog = parseCatalog;
|
|
8
|
+
exports.publishedVersions = publishedVersions;
|
|
9
|
+
const semver = require("semver");
|
|
10
|
+
const rxjs_1 = require("rxjs");
|
|
11
|
+
exports.REGISTRY = 'https://registry.npmjs.org/tabby-rdp';
|
|
12
|
+
exports.RELEASES = 'https://github.com/meanaverage/tabby-rdp/releases';
|
|
13
|
+
exports.RELEASE_STATE_KEY = 'tabby-rdp.releases.v1';
|
|
14
|
+
const BASELINE = '0.5.1';
|
|
15
|
+
const CACHE_MS = 15 * 60 * 1000;
|
|
16
|
+
/** Exact, canonical versions only: never an npm selector, alias, URL or tag. */
|
|
17
|
+
function releaseVersion(value) {
|
|
18
|
+
return typeof value === 'string' && semver.valid(value) === value && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:beta|rc)\.(0|[1-9]\d*))?$/.test(value);
|
|
19
|
+
}
|
|
20
|
+
function newer(a, b) {
|
|
21
|
+
return releaseVersion(a) && releaseVersion(b) && semver.gt(a, b);
|
|
22
|
+
}
|
|
23
|
+
const greatest = (a, b) => semver.gt(a, b) ? a : b;
|
|
24
|
+
function policy(value) {
|
|
25
|
+
return value && typeof value.minimumTabbyVersion === 'string' && semver.valid(value.minimumTabbyVersion) &&
|
|
26
|
+
releaseVersion(value.rollbackFloor) ? { minimumTabbyVersion: value.minimumTabbyVersion, rollbackFloor: value.rollbackFloor } : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Bound both the response body and elapsed time, including slow streaming responses. */
|
|
29
|
+
async function registryJSON(url, timeoutMs = 15000, maxBytes = 4 * 1024 * 1024) {
|
|
30
|
+
const abort = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => abort.abort(), timeoutMs);
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch(url, { cache: 'no-store', signal: abort.signal });
|
|
34
|
+
if (!response.ok || !response.body) {
|
|
35
|
+
throw new Error(`The registry answered ${response.status}.`);
|
|
36
|
+
}
|
|
37
|
+
const reader = response.body.getReader();
|
|
38
|
+
const chunks = [];
|
|
39
|
+
let size = 0;
|
|
40
|
+
for (;;) {
|
|
41
|
+
const { done, value } = await reader.read();
|
|
42
|
+
if (done) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
size += value.byteLength;
|
|
46
|
+
if (size > maxBytes) {
|
|
47
|
+
throw new Error('The registry\'s answer is too large.');
|
|
48
|
+
}
|
|
49
|
+
chunks.push(value);
|
|
50
|
+
}
|
|
51
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
abort.abort();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function parseCatalog(data) {
|
|
59
|
+
if (data?.name !== 'tabby-rdp' || !data.versions || typeof data.versions !== 'object' || Array.isArray(data.versions)) {
|
|
60
|
+
throw new Error('The registry returned an invalid version list.');
|
|
61
|
+
}
|
|
62
|
+
const entries = Object.entries(data.versions);
|
|
63
|
+
if (entries.length > 5000) {
|
|
64
|
+
throw new Error('The registry\'s version list is too large.');
|
|
65
|
+
}
|
|
66
|
+
const versions = [];
|
|
67
|
+
for (const [version, raw] of entries) {
|
|
68
|
+
const p = raw;
|
|
69
|
+
if (!releaseVersion(version) || p?.name !== 'tabby-rdp' || p.version !== version) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// 0.5.1 predates this field. Its requirements and irreversible credential migration were reviewed explicitly.
|
|
73
|
+
let compatibility = policy(p.tabbyRdp) ?? (version === BASELINE ? { minimumTabbyVersion: '1.0.236', rollbackFloor: BASELINE } : undefined);
|
|
74
|
+
if (compatibility && semver.gt(compatibility.rollbackFloor, version)) {
|
|
75
|
+
compatibility = undefined;
|
|
76
|
+
}
|
|
77
|
+
versions.push({ version, policy: compatibility, node: typeof p.engines?.node === 'string' ? p.engines.node : undefined,
|
|
78
|
+
os: Array.isArray(p.os) ? p.os.filter((x) => typeof x === 'string') : undefined,
|
|
79
|
+
cpu: Array.isArray(p.cpu) ? p.cpu.filter((x) => typeof x === 'string') : undefined,
|
|
80
|
+
deprecated: !!p.deprecated });
|
|
81
|
+
}
|
|
82
|
+
versions.sort((a, b) => semver.rcompare(a.version, b.version));
|
|
83
|
+
const tag = (name, preview) => {
|
|
84
|
+
const v = data['dist-tags']?.[name];
|
|
85
|
+
return releaseVersion(v) && !!semver.prerelease(v) === preview && versions.some(p => p.version === v) ? v : null;
|
|
86
|
+
};
|
|
87
|
+
return { versions, stable: tag('latest', false), preview: tag('beta', true) };
|
|
88
|
+
}
|
|
89
|
+
async function publishedVersions() {
|
|
90
|
+
return parseCatalog(await registryJSON(exports.REGISTRY));
|
|
91
|
+
}
|
|
92
|
+
/** No config or credential backups: only local preferences and successful package-change history. */
|
|
93
|
+
class ReleaseManager {
|
|
94
|
+
constructor(env, initiallyPaused = false) {
|
|
95
|
+
this.env = env;
|
|
96
|
+
this.changed$ = new rxjs_1.Subject();
|
|
97
|
+
this.catalog = null;
|
|
98
|
+
this.checking = false;
|
|
99
|
+
this.busy = false;
|
|
100
|
+
this.error = '';
|
|
101
|
+
this.catalogError = '';
|
|
102
|
+
this.notice = '';
|
|
103
|
+
this.storageWarning = '';
|
|
104
|
+
this.checkedAt = null;
|
|
105
|
+
this.lastAttempt = -Infinity;
|
|
106
|
+
this.running = env.running;
|
|
107
|
+
this.now = env.now ?? Date.now;
|
|
108
|
+
this.state = { channel: 'stable', paused: initiallyPaused, rollbackFloor: greatest(BASELINE, env.policy.rollbackFloor) };
|
|
109
|
+
this.syncFromStorage();
|
|
110
|
+
this.ready = this.lock(async () => {
|
|
111
|
+
this.syncFromStorage();
|
|
112
|
+
this.reconcileDisk();
|
|
113
|
+
this.persist();
|
|
114
|
+
}).catch(e => { this.error = this.message(e); }).finally(() => this.changed$.next());
|
|
115
|
+
}
|
|
116
|
+
message(error) { return error instanceof Error ? error.message : 'Use Tabby Plugins to manage this installation.'; }
|
|
117
|
+
async lock(run) {
|
|
118
|
+
if (this.env.withLock) {
|
|
119
|
+
await this.env.withLock(run);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
await run();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Storage events call this too. Reading never clears another window's successful package change. */
|
|
126
|
+
syncFromStorage() {
|
|
127
|
+
if (!this.env.storage) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const saved = JSON.parse(this.env.storage.getItem(exports.RELEASE_STATE_KEY) ?? 'null');
|
|
132
|
+
if (!saved) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.state.channel = saved.channel === 'preview' ? 'preview' : 'stable';
|
|
136
|
+
if (typeof saved.paused === 'boolean') {
|
|
137
|
+
this.state.paused = saved.paused;
|
|
138
|
+
}
|
|
139
|
+
this.state.previous = releaseVersion(saved.previous) ? saved.previous : undefined;
|
|
140
|
+
this.state.pending = releaseVersion(saved.pending) ? saved.pending : undefined;
|
|
141
|
+
this.state.removed = saved.removed === true;
|
|
142
|
+
if (releaseVersion(saved.rollbackFloor)) {
|
|
143
|
+
this.state.rollbackFloor = greatest(this.state.rollbackFloor, saved.rollbackFloor);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch { /* Keep the known state if local storage is temporarily unavailable. */ }
|
|
147
|
+
this.changed$.next();
|
|
148
|
+
}
|
|
149
|
+
/** Under the shared lock, so a second window never observes our installer's intermediate files. */
|
|
150
|
+
reconcileDisk() {
|
|
151
|
+
if (!this.env.diskVersion) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const disk = this.env.diskVersion();
|
|
155
|
+
if (disk !== null && !releaseVersion(disk)) {
|
|
156
|
+
throw new Error('The installed package version cannot be verified. Use Tabby Plugins.');
|
|
157
|
+
}
|
|
158
|
+
if (disk === null) {
|
|
159
|
+
this.state.removed = true;
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
if ((this.state.pending && this.state.pending !== disk) || this.state.removed) {
|
|
163
|
+
this.notice = `The package changed outside this page. Tabby is running ${this.running}; the package on disk is ${disk}.`;
|
|
164
|
+
this.state.previous = undefined;
|
|
165
|
+
}
|
|
166
|
+
this.state.pending = disk;
|
|
167
|
+
this.state.removed = false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
get pending() { return !this.removed && this.state.pending && this.state.pending !== this.running ? this.state.pending : null; }
|
|
171
|
+
get removed() { return this.state.removed === true; }
|
|
172
|
+
persist() {
|
|
173
|
+
try {
|
|
174
|
+
if (!this.env.storage) {
|
|
175
|
+
throw new Error('Local storage unavailable');
|
|
176
|
+
}
|
|
177
|
+
this.env.storage.setItem(exports.RELEASE_STATE_KEY, JSON.stringify(this.state));
|
|
178
|
+
this.storageWarning = '';
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
this.storageWarning = 'Version preferences could not be saved on this computer. They apply to this window only. Manage installations in Tabby Plugins.';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async preference(change) {
|
|
185
|
+
if (this.busy) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
await this.ready;
|
|
189
|
+
try {
|
|
190
|
+
await this.lock(async () => {
|
|
191
|
+
this.syncFromStorage();
|
|
192
|
+
Object.assign(this.state, change);
|
|
193
|
+
this.persist();
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
this.error = this.message(e);
|
|
198
|
+
}
|
|
199
|
+
this.changed$.next();
|
|
200
|
+
}
|
|
201
|
+
async setChannel(value) {
|
|
202
|
+
if (value === 'stable' || value === 'preview') {
|
|
203
|
+
await this.preference({ channel: value });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async setPaused(paused) { await this.preference({ paused }); }
|
|
207
|
+
get recommended() {
|
|
208
|
+
const stable = this.catalog?.stable ?? null;
|
|
209
|
+
const preview = this.state.channel === 'preview' ? this.catalog?.preview : null;
|
|
210
|
+
return preview && (!stable || newer(preview, stable)) ? preview : stable;
|
|
211
|
+
}
|
|
212
|
+
get available() {
|
|
213
|
+
const version = this.recommended;
|
|
214
|
+
return !this.state.paused && !this.catalogError && !this.pending && !this.removed && version && newer(version, this.running) && !this.reason(version) ? version : null;
|
|
215
|
+
}
|
|
216
|
+
get versions() {
|
|
217
|
+
return (this.catalog?.versions ?? []).filter(p => this.state.channel === 'preview' || !semver.prerelease(p.version));
|
|
218
|
+
}
|
|
219
|
+
/** A reason to refuse a version, independent of whether it is newer. Return-to-stable may downgrade. */
|
|
220
|
+
reason(version) {
|
|
221
|
+
if (!releaseVersion(version)) {
|
|
222
|
+
return 'Invalid version.';
|
|
223
|
+
}
|
|
224
|
+
const p = this.catalog?.versions.find(x => x.version === version);
|
|
225
|
+
if (!p) {
|
|
226
|
+
return 'This version is not in the published version list.';
|
|
227
|
+
}
|
|
228
|
+
if (p.deprecated) {
|
|
229
|
+
return 'This version has been withdrawn from recommendations.';
|
|
230
|
+
}
|
|
231
|
+
if (semver.lt(version, this.state.rollbackFloor)) {
|
|
232
|
+
return `Saved data requires version ${this.state.rollbackFloor} or newer.`;
|
|
233
|
+
}
|
|
234
|
+
if (!p.policy) {
|
|
235
|
+
return 'Compatibility has not been confirmed for this version.';
|
|
236
|
+
}
|
|
237
|
+
if (!semver.valid(this.env.tabby) || semver.lt(this.env.tabby, p.policy.minimumTabbyVersion)) {
|
|
238
|
+
return `Requires Tabby ${p.policy.minimumTabbyVersion} or newer.`;
|
|
239
|
+
}
|
|
240
|
+
if (p.node && (!semver.validRange(p.node) || !semver.satisfies(this.env.node, p.node, { includePrerelease: true }))) {
|
|
241
|
+
return 'Requires a different Node.js version. Update Tabby first.';
|
|
242
|
+
}
|
|
243
|
+
const supports = (list, value) => !list?.length ||
|
|
244
|
+
(!list.includes(`!${value}`) && (list.includes('any') || !list.some(x => !x.startsWith('!')) || list.includes(value)));
|
|
245
|
+
if (!supports(p.os, this.env.os) || !supports(p.cpu, this.env.cpu)) {
|
|
246
|
+
return 'Not available for this computer.';
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
/** One shared request; ordinary reads cache for 15 minutes, explicit checks retry at most every 10 seconds. */
|
|
251
|
+
async refreshCatalog(force = false) {
|
|
252
|
+
if (this.inFlight) {
|
|
253
|
+
return this.inFlight;
|
|
254
|
+
}
|
|
255
|
+
const elapsed = this.now() - this.lastAttempt;
|
|
256
|
+
if (elapsed < (force ? 10000 : CACHE_MS)) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
this.lastAttempt = this.now();
|
|
260
|
+
this.checking = true;
|
|
261
|
+
this.error = '';
|
|
262
|
+
this.changed$.next();
|
|
263
|
+
this.inFlight = (async () => {
|
|
264
|
+
try {
|
|
265
|
+
this.catalog = await Promise.resolve().then(() => this.env.fetchCatalog());
|
|
266
|
+
this.catalogError = '';
|
|
267
|
+
this.checkedAt = this.now();
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
this.error = this.catalogError = `Could not check published versions. ${e instanceof Error ? e.message : 'Try again later.'}`;
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
this.checking = false;
|
|
274
|
+
this.inFlight = undefined;
|
|
275
|
+
this.changed$.next();
|
|
276
|
+
}
|
|
277
|
+
})();
|
|
278
|
+
return this.inFlight;
|
|
279
|
+
}
|
|
280
|
+
async install(version, returnToStable = false) {
|
|
281
|
+
if (this.busy) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
this.busy = true;
|
|
285
|
+
this.error = '';
|
|
286
|
+
this.changed$.next();
|
|
287
|
+
const progress = (phase, message) => this.env.progress?.({ action: 'install', phase, version, message });
|
|
288
|
+
try {
|
|
289
|
+
progress('preparing');
|
|
290
|
+
await this.ready;
|
|
291
|
+
await this.lock(async () => {
|
|
292
|
+
this.syncFromStorage();
|
|
293
|
+
this.reconcileDisk();
|
|
294
|
+
this.persist();
|
|
295
|
+
if (this.storageWarning) {
|
|
296
|
+
throw new Error('Version history cannot be saved. Use Tabby Plugins to install.');
|
|
297
|
+
}
|
|
298
|
+
if (this.pending) {
|
|
299
|
+
progress('success', `tabby-rdp ${this.pending} is already installed. Restart Tabby to use it.`);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (this.removed) {
|
|
303
|
+
throw new Error('The plugin has been uninstalled. Restart Tabby before changing versions.');
|
|
304
|
+
}
|
|
305
|
+
await this.refreshCatalog();
|
|
306
|
+
if (this.catalogError) {
|
|
307
|
+
throw new Error('Check published versions successfully before installing.');
|
|
308
|
+
}
|
|
309
|
+
const reason = this.reason(version);
|
|
310
|
+
if (reason) {
|
|
311
|
+
throw new Error(reason);
|
|
312
|
+
}
|
|
313
|
+
if (version === this.running) {
|
|
314
|
+
progress('success', `tabby-rdp ${version} is already running.`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (semver.prerelease(version) && this.state.channel !== 'preview') {
|
|
318
|
+
throw new Error('Choose Preview before installing a preview version.');
|
|
319
|
+
}
|
|
320
|
+
if (returnToStable && version !== this.catalog?.stable) {
|
|
321
|
+
throw new Error('Check the current stable version again.');
|
|
322
|
+
}
|
|
323
|
+
const downgrade = semver.lt(version, this.running);
|
|
324
|
+
if (!await this.env.confirm(`Install tabby-rdp ${version}?`, `Currently running ${this.running}. ${downgrade ? 'This is a downgrade. ' : ''}Close remote desktops and restart Tabby after installation.`, 'Install')) {
|
|
325
|
+
progress('cancelled');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
progress('installing');
|
|
329
|
+
await this.env.install('tabby-rdp', version);
|
|
330
|
+
progress('verifying');
|
|
331
|
+
if (this.env.diskVersion) {
|
|
332
|
+
const installed = this.env.diskVersion();
|
|
333
|
+
if (installed !== version) {
|
|
334
|
+
throw new Error(installed === null ? 'The installed plugin package is missing.' :
|
|
335
|
+
`The installed version is ${installed}; expected ${version}.`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// History is committed only after the supported installer succeeds.
|
|
339
|
+
this.state.previous = this.running;
|
|
340
|
+
this.state.pending = version;
|
|
341
|
+
this.state.removed = false;
|
|
342
|
+
if (returnToStable) {
|
|
343
|
+
this.state.channel = 'stable';
|
|
344
|
+
}
|
|
345
|
+
this.persist();
|
|
346
|
+
progress('success');
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
this.error = `Installation did not complete. ${this.message(e)}`;
|
|
351
|
+
progress('error', this.error);
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
this.busy = false;
|
|
355
|
+
this.changed$.next();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async uninstall() {
|
|
359
|
+
if (this.busy) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.busy = true;
|
|
363
|
+
this.error = '';
|
|
364
|
+
this.changed$.next();
|
|
365
|
+
const progress = (phase, message) => this.env.progress?.({ action: 'uninstall', phase, message });
|
|
366
|
+
try {
|
|
367
|
+
progress('preparing');
|
|
368
|
+
await this.ready;
|
|
369
|
+
await this.lock(async () => {
|
|
370
|
+
this.syncFromStorage();
|
|
371
|
+
this.reconcileDisk();
|
|
372
|
+
this.persist();
|
|
373
|
+
if (this.storageWarning) {
|
|
374
|
+
throw new Error('Version history cannot be saved. Use Tabby Plugins to uninstall.');
|
|
375
|
+
}
|
|
376
|
+
if (this.pending) {
|
|
377
|
+
throw new Error('A version change is pending. Restart Tabby before uninstalling.');
|
|
378
|
+
}
|
|
379
|
+
if (this.removed) {
|
|
380
|
+
progress('success', 'tabby-rdp is already uninstalled. Restart Tabby to finish.');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!await this.env.confirm('Uninstall tabby-rdp?', 'Close remote desktops before uninstalling, then restart Tabby. Saved connections, settings, passwords and remote setup are kept.', 'Uninstall')) {
|
|
384
|
+
progress('cancelled');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
progress('uninstalling');
|
|
388
|
+
await this.env.uninstall('tabby-rdp');
|
|
389
|
+
progress('verifying');
|
|
390
|
+
if (this.env.diskVersion && this.env.diskVersion() !== null) {
|
|
391
|
+
throw new Error('The plugin package is still installed.');
|
|
392
|
+
}
|
|
393
|
+
this.state.removed = true;
|
|
394
|
+
this.persist();
|
|
395
|
+
progress('success');
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
this.error = `Uninstall did not complete. ${this.message(e)}`;
|
|
400
|
+
progress('error', this.error);
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
this.busy = false;
|
|
404
|
+
this.changed$.next();
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
exports.ReleaseManager = ReleaseManager;
|