papilot 0.8.5 → 0.8.6
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/assets/{index-DSWizoaI.js → index-Dm7AYM9e.js} +5 -5
- package/dist/assets/{math-DHvVou9A.js → math-C7ENJr-a.js} +1 -1
- package/dist/assets/{pdf-view-AYTS7kdA.js → pdf-view-B2xQuHmI.js} +1 -1
- package/dist/index.html +1 -1
- package/engine/src/core/build.ts +24 -13
- package/package.json +1 -1
- package/src/server/update-install.ts +5 -1
- package/src/server/updates.ts +24 -49
package/dist/index.html
CHANGED
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
|
|
9
9
|
document.querySelector('meta[name="theme-color"]').content = dark ? '#26272b' : '#ffffff';
|
|
10
10
|
})();
|
|
11
|
-
</script><style>html{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;background:#fff}html[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#26272b}</style> <script type="module" crossorigin src="/assets/index-
|
|
11
|
+
</script><style>html{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;background:#fff}html[data-theme=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;background:#26272b}</style> <script type="module" crossorigin src="/assets/index-Dm7AYM9e.js"></script>
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-PFQDdu53.css">
|
|
13
13
|
</head><body><div id="root"></div></body></html>
|
package/engine/src/core/build.ts
CHANGED
|
@@ -263,12 +263,13 @@ export class Builder {
|
|
|
263
263
|
fs.utimesSync(folder, new Date(), new Date());
|
|
264
264
|
return cached;
|
|
265
265
|
}
|
|
266
|
-
// A
|
|
267
|
-
// author's own latexmk output, as
|
|
268
|
-
// compiled again: unchanged files are
|
|
269
|
-
// so it reruns only as often as the
|
|
270
|
-
// starts clean, and so does a retry
|
|
271
|
-
|
|
266
|
+
// A build continues from the latest completed one with the same
|
|
267
|
+
// targets, or a full build from the author's own latexmk output, as
|
|
268
|
+
// latexmk does when a folder is compiled again: unchanged files are
|
|
269
|
+
// cloned and LaTeX keeps its state, so it reruns only as often as the
|
|
270
|
+
// edit requires. Manual recompilation starts clean, and so does a retry
|
|
271
|
+
// when leftover state broke a build.
|
|
272
|
+
const start = options.force ? null : this.latest(context, targets) ?? (partial ? null : "author");
|
|
272
273
|
let result: BuildInfo;
|
|
273
274
|
try {
|
|
274
275
|
result = await this.compile(rev, tree, key, targets, seed, date, start, options.progress);
|
|
@@ -276,15 +277,21 @@ export class Builder {
|
|
|
276
277
|
if (!start || (error instanceof BuildError && !error.details.some(line => /(?:^|\s|\()(?:\.\/)?build\//.test(line)))) throw error;
|
|
277
278
|
result = await this.compile(rev, tree, key, targets, seed, date, null, options.progress);
|
|
278
279
|
}
|
|
279
|
-
|
|
280
|
+
writeJson(this.latestFile(targets), { key, context, date: date.SOURCE_DATE_EPOCH });
|
|
280
281
|
return result;
|
|
281
282
|
});
|
|
282
283
|
}
|
|
283
|
-
|
|
284
|
-
|
|
284
|
+
// Review rounds build the same chapters again and again (\includeonly), so
|
|
285
|
+
// each set of targets continues from its own latest build.
|
|
286
|
+
private latestFile(targets: string[]) {
|
|
287
|
+
const name = JSON.stringify(targets) === '["full"]' ? "latest.json" : `latest-${hash(JSON.stringify(targets)).slice(0, 16)}.json`;
|
|
288
|
+
return path.join(this.cache, name);
|
|
289
|
+
}
|
|
290
|
+
private latest(context: string, targets: string[]): { info: BuildInfo; date: unknown } | null {
|
|
291
|
+
const latest = readJson<{ key?: unknown; context?: unknown; date?: unknown } | null>(this.latestFile(targets), null);
|
|
285
292
|
if (typeof latest?.key !== "string" || !/^[0-9a-f]{32}$/.test(latest.key) || latest.context !== context) return null;
|
|
286
293
|
const info = readJson<BuildInfo | null>(path.join(this.cache, latest.key, "result.json"), null);
|
|
287
|
-
return info && JSON.stringify(info.targets) ===
|
|
294
|
+
return info && JSON.stringify(info.targets) === JSON.stringify(targets) && fs.existsSync(info.pdf) && fs.existsSync(info.root) ? { info, date: latest.date } : null;
|
|
288
295
|
}
|
|
289
296
|
private async compile(
|
|
290
297
|
rev: string,
|
|
@@ -353,12 +360,16 @@ export class Builder {
|
|
|
353
360
|
"-file-line-error",
|
|
354
361
|
];
|
|
355
362
|
if (partial) {
|
|
356
|
-
|
|
363
|
+
// Chapters outside \includeonly keep the seed's auxiliary files, also
|
|
364
|
+
// when this build continues from an earlier one built on another seed.
|
|
365
|
+
// latexmk compares contents, so an unchanged chapter causes no rerun.
|
|
366
|
+
const seedOut = path.dirname(seed!.pdf),
|
|
367
|
+
others = new Set(included.filter((target) => !targets.includes(target)).map((target) => path.join(...target.split("/")) + ".aux"));
|
|
357
368
|
for (const file of listFiles(seedOut))
|
|
358
369
|
if (AUXILIARY.has(path.extname(file))) {
|
|
359
|
-
const
|
|
370
|
+
const relative = path.relative(seedOut, file), target = path.join(out, relative);
|
|
360
371
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
361
|
-
if (!fs.existsSync(target)) fs.copyFileSync(file, target);
|
|
372
|
+
if (!fs.existsSync(target) || others.has(relative)) fs.copyFileSync(file, target);
|
|
362
373
|
}
|
|
363
374
|
const preload = Object.entries(p.preload)
|
|
364
375
|
.filter(
|
package/package.json
CHANGED
|
@@ -106,7 +106,10 @@ async function installDev(data: string, version: string) {
|
|
|
106
106
|
const INSTALL_FAILED = 'The new version could not be installed. Check the connection and available disk space, then retry.';
|
|
107
107
|
// Release assets redirect from github.com to GitHub's download hosts. Each hop
|
|
108
108
|
// stays on HTTPS and on those hosts (or on the smoke test's local source).
|
|
109
|
-
|
|
109
|
+
// With missing, a file that does not exist resolves to null.
|
|
110
|
+
export async function download(url: string, fetcher: typeof fetch, limit: number, missing: true): Promise<Buffer | null>;
|
|
111
|
+
export async function download(url: string, fetcher: typeof fetch, limit: number): Promise<Buffer>;
|
|
112
|
+
export async function download(url: string, fetcher: typeof fetch, limit: number, missing = false): Promise<Buffer | null> {
|
|
110
113
|
const source = process.env.PAPILOT_DESKTOP_UPDATE_SOURCE ? new URL(process.env.PAPILOT_DESKTOP_UPDATE_SOURCE).origin : null;
|
|
111
114
|
const trusted = (address: URL) => address.origin === source
|
|
112
115
|
|| address.protocol === 'https:' && (address.hostname === 'github.com' || address.hostname.endsWith('.githubusercontent.com'));
|
|
@@ -116,6 +119,7 @@ export async function download(url: string, fetcher: typeof fetch, limit: number
|
|
|
116
119
|
const response = await fetcher(address.href, { signal: AbortSignal.timeout(300000), redirect: 'manual', headers: { Accept: 'application/octet-stream', 'User-Agent': 'Papilot' } });
|
|
117
120
|
const location = response.headers.get('location');
|
|
118
121
|
if (response.status >= 300 && response.status < 400 && location) { address = new URL(location, address); continue; }
|
|
122
|
+
if (missing && response.status === 404) return null;
|
|
119
123
|
if (!response.ok || Number(response.headers.get('content-length') || 0) > limit) throw new Error(INSTALL_FAILED);
|
|
120
124
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
121
125
|
if (bytes.length > limit) throw new Error(INSTALL_FAILED);
|
package/src/server/updates.ts
CHANGED
|
@@ -8,9 +8,8 @@ import type { UpdateChannel, UpdateStatus } from '../shared.ts';
|
|
|
8
8
|
|
|
9
9
|
const activePhases = new Set(['installing', 'waiting', 'restarting']);
|
|
10
10
|
export const alive = (pid: number) => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
|
11
|
-
async function releaseInfo(url: string, fetcher: typeof fetch, limit = 256000
|
|
11
|
+
async function releaseInfo(url: string, fetcher: typeof fetch, limit = 256000) {
|
|
12
12
|
const response = await fetcher(url, { signal: AbortSignal.timeout(10000), redirect: 'error', headers: { Accept: 'application/json', 'User-Agent': 'Papilot' } });
|
|
13
|
-
if (response.status === 404 && missing === null) return null;
|
|
14
13
|
if (!response.ok) throw new Error('Could not check for updates.');
|
|
15
14
|
const text = await response.text();
|
|
16
15
|
if (text.length > limit) throw new Error('Invalid release information.');
|
|
@@ -34,53 +33,29 @@ export const latestRelease = async (channel: UpdateChannel = 'stable', fetcher =
|
|
|
34
33
|
export const DESKTOP_RELEASES = 'https://github.com/PapilotAI/papilot/releases';
|
|
35
34
|
// Nightly builds of main replace the assets of this one prerelease.
|
|
36
35
|
export const DESKTOP_DEV_TAG = 'desktop-dev';
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
const
|
|
36
|
+
// Release downloads, not the rate-limited REST API. The smoke test serves
|
|
37
|
+
// them locally.
|
|
38
|
+
const desktopDownloads = () => (process.env.PAPILOT_DESKTOP_UPDATE_SOURCE ? process.env.PAPILOT_DESKTOP_UPDATE_SOURCE.replace(/\/+$/, '') + '/PapilotAI/papilot/releases' : DESKTOP_RELEASES);
|
|
40
39
|
export interface DesktopRelease { version: string; downloadUrl: string; layer?: DesktopLayer }
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (channel === 'dev') {
|
|
61
|
-
const release = await releaseInfo(`${desktopApi()}/releases/tags/${DESKTOP_DEV_TAG}`, fetcher, 4 * 1024 * 1024, null);
|
|
62
|
-
if (!release) return null;
|
|
63
|
-
if (!Array.isArray(release.assets) || release.draft !== false) throw new Error('Invalid release information.');
|
|
64
|
-
const manifest = await desktopManifest(release, fetcher);
|
|
65
|
-
if (!manifest) return null;
|
|
66
|
-
if (!devRevision(manifest.version)) throw new Error('Invalid release information.');
|
|
67
|
-
const layer = desktopLayer(release, manifest, manifest.version, fingerprint);
|
|
68
|
-
return { version: manifest.version, downloadUrl: `${DESKTOP_RELEASES}/tag/${DESKTOP_DEV_TAG}`, ...(layer ? { layer } : {}) };
|
|
69
|
-
}
|
|
70
|
-
const releases = await releaseInfo(`${desktopApi()}/releases?per_page=100`, fetcher, 4 * 1024 * 1024);
|
|
71
|
-
if (!Array.isArray(releases)) throw new Error('Invalid release information.');
|
|
72
|
-
let latest: { version: string; downloadUrl: string; release: any } | null = null;
|
|
73
|
-
for (const release of releases) {
|
|
74
|
-
const version = typeof release.tag_name === 'string' ? release.tag_name.replace(/^v/, '') : '';
|
|
75
|
-
if (release.draft !== false || release.prerelease !== false || !stableVersion(version) || !Array.isArray(release.assets)) continue;
|
|
76
|
-
const names = platform === 'darwin' && ['arm64', 'x64'].includes(arch) ? [`Papilot-${version}-mac-${arch}.dmg`, `Papilot-${version}-mac-${arch}.zip`]
|
|
77
|
-
: platform === 'linux' && ['arm64', 'x64'].includes(arch) ? [`Papilot-${version}-linux-${arch === 'x64' ? 'x86_64' : arch}.AppImage`, `Papilot-${version}-linux-${arch === 'x64' ? 'amd64' : arch}.deb`] : [];
|
|
78
|
-
if (!release.assets.some((asset: any) => asset.state === 'uploaded' && asset.size > 0 && names.includes(asset.name))) continue;
|
|
79
|
-
if (!latest || newerVersion(version, latest.version)) latest = { version, downloadUrl: `${DESKTOP_RELEASES}/tag/${release.tag_name}`, release };
|
|
80
|
-
}
|
|
81
|
-
if (!latest) return null;
|
|
82
|
-
const layer = fingerprint ? desktopLayer(latest.release, await desktopManifest(latest.release, fetcher), latest.version, fingerprint) : undefined;
|
|
83
|
-
return { version: latest.version, downloadUrl: latest.downloadUrl, ...(layer ? { layer } : {}) };
|
|
40
|
+
// desktop.json (written by scripts/desktop-layer.mjs, uploaded after every
|
|
41
|
+
// other asset of its release) names the version, its update layer and the
|
|
42
|
+
// installer shell that layer runs on. Stable: the latest release. Dev: the
|
|
43
|
+
// latest nightly build of main. The layer is offered only to that shell.
|
|
44
|
+
export async function latestDesktopRelease(fetcher = fetch, channel: UpdateChannel = 'stable', fingerprint = ''): Promise<DesktopRelease | null> {
|
|
45
|
+
const folder = channel === 'dev' ? `download/${DESKTOP_DEV_TAG}` : 'latest/download';
|
|
46
|
+
let manifest: any;
|
|
47
|
+
try {
|
|
48
|
+
const bytes = await download(`${desktopDownloads()}/${folder}/desktop.json`, fetcher, 64 * 1024, true);
|
|
49
|
+
if (!bytes) return null;
|
|
50
|
+
manifest = JSON.parse(bytes.toString('utf8'));
|
|
51
|
+
} catch { throw new Error('Could not check for updates.'); }
|
|
52
|
+
const version = manifest?.version;
|
|
53
|
+
if (manifest?.format !== 1 || (channel === 'dev' ? !devRevision(version) : !stableVersion(version))) throw new Error('Invalid release information.');
|
|
54
|
+
const tag = channel === 'dev' ? DESKTOP_DEV_TAG : `v${version}`, layer = manifest.layer;
|
|
55
|
+
const usable = fingerprint && manifest.fingerprint === fingerprint && typeof layer?.name === 'string' && /^papilot-app-[\w.-]+\.tgz$/.test(layer.name)
|
|
56
|
+
&& Number.isSafeInteger(layer.size) && layer.size > 0 && /^[a-f0-9]{64}$/.test(layer.sha256);
|
|
57
|
+
return { version, downloadUrl: `${DESKTOP_RELEASES}/tag/${tag}`,
|
|
58
|
+
...(usable ? { layer: { url: `${desktopDownloads()}/download/${tag}/${layer.name}`, size: layer.size, sha256: layer.sha256 } } : {}) };
|
|
84
59
|
}
|
|
85
60
|
const validTarget = (version: unknown, channel: UpdateChannel): version is string => channel === 'dev' ? Boolean(devRevision(version)) : stableVersion(version);
|
|
86
61
|
const available = (next: string, current: string, channel: UpdateChannel) => validTarget(next, channel)
|
|
@@ -197,7 +172,7 @@ export class Updates {
|
|
|
197
172
|
return this.checking;
|
|
198
173
|
}
|
|
199
174
|
private desktopRelease(channel: UpdateChannel) {
|
|
200
|
-
return (this.options.latestDesktop || (channel => latestDesktopRelease(
|
|
175
|
+
return (this.options.latestDesktop || (channel => latestDesktopRelease(fetch, channel, this.options.bundle?.fingerprint)))(channel);
|
|
201
176
|
}
|
|
202
177
|
// A retry after a restart finds the release again; the check that offered it
|
|
203
178
|
// happened in the previous process.
|