papilot 0.8.2 → 0.8.3
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/README.md +2 -2
- package/bin/doctor.mjs +1 -1
- package/dist/assets/{_virtual_papilot-licenses-CMfCtEYQ.js → _virtual_papilot-licenses-YJbh9c1G.js} +1 -1
- package/dist/assets/index-0siHHtfz.js +267 -0
- package/dist/assets/index-CxdYm81a.css +1 -0
- package/dist/assets/{math-CEU4m4-H.js → math-ibBpTslk.js} +1 -1
- package/dist/assets/{pdf-view-HWRGBLsZ.js → pdf-view-CD1kNKYH.js} +1 -1
- package/dist/index.html +2 -2
- package/engine/dist/web/assets/FullPaper-DUgLb01O.js +54 -0
- package/engine/dist/web/assets/index-D3_ZFdT8.css +1 -0
- package/engine/dist/web/assets/index-DJywe0wU.js +19 -0
- package/engine/dist/web/assets/pdf.worker.min-CjEcRF4W.mjs +28 -0
- package/{vendor/latex-review → engine}/dist/web/index.html +2 -2
- package/engine/package-lock.json +3390 -0
- package/engine/package.json +50 -0
- package/{vendor/latex-review → engine}/src/core/build.ts +22 -6
- package/engine/src/core/progress.ts +67 -0
- package/{vendor/latex-review → engine}/src/core/review.ts +32 -0
- package/{vendor/latex-review → engine}/src/render/inline.ts +1 -0
- package/{vendor/latex-review → engine}/src/render/locate.ts +26 -16
- package/engine/src/render/positions.ts +34 -0
- package/engine/src/render/synctex.ts +188 -0
- package/engine/src/render/synctex.wasm +0 -0
- package/engine/src/render/synctex.wasm.LICENSE +50 -0
- package/{vendor/latex-review → engine}/src/server.ts +5 -3
- package/{vendor/latex-review → engine}/src/types.ts +13 -0
- package/engine/src/web/FullPaper.tsx +318 -0
- package/{vendor/latex-review → engine}/src/web/locales/de.json +3 -0
- package/{vendor/latex-review → engine}/src/web/locales/es.json +3 -0
- package/{vendor/latex-review → engine}/src/web/locales/fr.json +3 -0
- package/{vendor/latex-review → engine}/src/web/locales/it.json +3 -0
- package/{vendor/latex-review → engine}/src/web/locales/ja.json +3 -0
- package/{vendor/latex-review → engine}/src/web/locales/ko.json +3 -0
- package/{vendor/latex-review → engine}/src/web/locales/pt.json +3 -0
- package/{vendor/latex-review → engine}/src/web/locales/sv.json +3 -0
- package/{vendor/latex-review → engine}/src/web/locales/zh-Hant.json +3 -0
- package/engine/src/web/paper.ts +13 -0
- package/{vendor/latex-review → engine}/src/web/review.tsx +98 -1
- package/{vendor/latex-review → engine}/src/web/style.css +70 -0
- package/engine/src/web/view-preferences.ts +13 -0
- package/{vendor/latex-review → engine}/src/web/zh-CN.ts +3 -0
- package/package.json +10 -8
- package/src/conversation.ts +1 -1
- package/src/server/app.ts +16 -0
- package/src/server/changes.ts +12 -3
- package/src/server/imports.ts +9 -4
- package/src/server/main.ts +3 -2
- package/src/server/manuscript.ts +5 -4
- package/src/server/paths.ts +1 -1
- package/src/server/start.ts +3 -10
- package/src/server/update-install.ts +8 -25
- package/src/server/updates.ts +43 -25
- package/src/shared.ts +6 -3
- package/vendor/latex-review/package-lock.json +4 -3106
- package/vendor/latex-review/package.json +3 -46
- package/README.de.md +0 -110
- package/README.es.md +0 -110
- package/README.fr.md +0 -110
- package/README.it.md +0 -110
- package/README.ja.md +0 -110
- package/README.ko.md +0 -110
- package/README.pt.md +0 -110
- package/README.sv.md +0 -110
- package/README.zh-CN.md +0 -110
- package/README.zh-Hant.md +0 -110
- package/dist/assets/index-oAIkxhfp.js +0 -265
- package/dist/assets/index-t_qCOF_B.css +0 -1
- package/vendor/latex-review/dist/web/assets/index-DVs7cTBl.js +0 -19
- package/vendor/latex-review/dist/web/assets/index-ysOpu05D.css +0 -1
- /package/{vendor/latex-review → engine}/LICENSE +0 -0
- /package/{vendor/latex-review → engine}/dist/web/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf +0 -0
- /package/{vendor/latex-review → engine}/dist/web/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf +0 -0
- /package/{vendor/latex-review → engine}/src/cli.ts +0 -0
- /package/{vendor/latex-review → engine}/src/client.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/bib.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/changes.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/config.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/current-commit.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/delivery.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/diff.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/files.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/git.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/outline.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/process.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/publish.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/reconcile.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/store.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/tasks.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/verify.ts +0 -0
- /package/{vendor/latex-review → engine}/src/core/view.ts +0 -0
- /package/{vendor/latex-review → engine}/src/locales.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/effects.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/pdf.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/pdfdiff.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/round.ts +0 -0
- /package/{vendor/latex-review → engine}/src/render/worker.ts +0 -0
- /package/{vendor/latex-review → engine}/src/runtime.ts +0 -0
- /package/{vendor/latex-review → engine}/src/web/ChangeMap.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/PdfStrip.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/effect-preferences.ts +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/GUST-FONT-LICENSE.txt +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/LPPL.txt +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/MANIFEST-newpx.txt +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/README-newpx.txt +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/README.md +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/TeXGyrePagellaX-Bold.otf +0 -0
- /package/{vendor/latex-review → engine}/src/web/fonts/TeXGyrePagellaX-Regular.otf +0 -0
- /package/{vendor/latex-review → engine}/src/web/i18n.ts +0 -0
- /package/{vendor/latex-review → engine}/src/web/index.html +0 -0
- /package/{vendor/latex-review → engine}/src/web/locales/index.ts +0 -0
- /package/{vendor/latex-review → engine}/src/web/main.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/model.ts +0 -0
- /package/{vendor/latex-review → engine}/src/web/overview.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/references.tsx +0 -0
- /package/{vendor/latex-review → engine}/src/web/shortcuts.ts +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Fragment,
|
|
3
|
+
lazy,
|
|
4
|
+
Suspense,
|
|
3
5
|
useEffect,
|
|
4
6
|
useMemo,
|
|
5
7
|
useRef,
|
|
@@ -18,7 +20,11 @@ import type {
|
|
|
18
20
|
} from "../types.ts";
|
|
19
21
|
import { PdfStrip } from "./PdfStrip.tsx";
|
|
20
22
|
import { ChangeMap } from "./ChangeMap.tsx";
|
|
23
|
+
import { placeable, type PlacedChange } from "./paper.ts";
|
|
24
|
+
// pdf.js loads only when the full paper is shown.
|
|
25
|
+
const FullPaper = lazy(() => import("./FullPaper.tsx").then((module) => ({ default: module.FullPaper })));
|
|
21
26
|
import { EFFECTS_EVENT, EFFECTS_STORAGE, readExpandedEffects } from "./effect-preferences.ts";
|
|
27
|
+
import { readView, VIEW_EVENT, VIEW_STORAGE, type ReviewView } from "./view-preferences.ts";
|
|
22
28
|
import { STATUS } from "./overview.tsx";
|
|
23
29
|
import { language, locale, t, tn, withLanguage } from "./i18n.ts";
|
|
24
30
|
import {
|
|
@@ -167,6 +173,22 @@ export function ReviewApp({
|
|
|
167
173
|
scrollUntil = useRef(0),
|
|
168
174
|
snapshot = useRef({ state, detail, busy, composer, current });
|
|
169
175
|
const [shortcuts, setShortcuts] = useState(readShortcuts);
|
|
176
|
+
// The whole paper with the changes in place, or the changed sections only.
|
|
177
|
+
// A host may fix the view in the address (Papilot's manuscript chooses its
|
|
178
|
+
// PDF with its own switch); otherwise it follows the stored choice.
|
|
179
|
+
const [pinnedView] = useState<ReviewView | null>(() => {
|
|
180
|
+
const value = new URLSearchParams(location.search).get("view");
|
|
181
|
+
return value === "paper" || value === "changes" ? value : null;
|
|
182
|
+
});
|
|
183
|
+
const [view, setView] = useState<ReviewView>(() => pinnedView ?? readView());
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (pinnedView) return;
|
|
186
|
+
const chosen = (event: Event) => setView((event as CustomEvent).detail === "changes" ? "changes" : "paper");
|
|
187
|
+
const stored = (event: StorageEvent) => { if (event.key === VIEW_STORAGE || event.key === null) setView(readView()); };
|
|
188
|
+
window.addEventListener(VIEW_EVENT, chosen);
|
|
189
|
+
window.addEventListener("storage", stored);
|
|
190
|
+
return () => { window.removeEventListener(VIEW_EVENT, chosen); window.removeEventListener("storage", stored); };
|
|
191
|
+
}, [pinnedView]);
|
|
170
192
|
const [expandedEffects, setExpandedEffects] = useState(readExpandedEffects);
|
|
171
193
|
// Per-edit disclosure is temporary and scoped to its round. Changing the
|
|
172
194
|
// global preference resets those overrides without reloading the review.
|
|
@@ -195,6 +217,16 @@ export function ReviewApp({
|
|
|
195
217
|
[detail],
|
|
196
218
|
),
|
|
197
219
|
stops = useMemo(() => (detail ? buildStops(detail) : []), [detail]),
|
|
220
|
+
// The change blocks in the order and numbering of the changes view.
|
|
221
|
+
placed = useMemo(() => {
|
|
222
|
+
const list: PlacedChange[] = [];
|
|
223
|
+
if (!detail) return list;
|
|
224
|
+
for (const section of detail.sections)
|
|
225
|
+
for (const block of section.blocks)
|
|
226
|
+
if (block.kind === "change") list.push({ block, index: list.length });
|
|
227
|
+
for (const block of detail.loose) list.push({ block, index: list.length });
|
|
228
|
+
return list;
|
|
229
|
+
}, [detail]),
|
|
198
230
|
// An edit's places show under the first change that includes it.
|
|
199
231
|
firstStop = useMemo(() => {
|
|
200
232
|
const found = new Map<string, number>();
|
|
@@ -1094,6 +1126,32 @@ export function ReviewApp({
|
|
|
1094
1126
|
</article>
|
|
1095
1127
|
);
|
|
1096
1128
|
}
|
|
1129
|
+
const fullPaper = !pinnedView && Boolean(detail?.pdf) && placeable(placed),
|
|
1130
|
+
paperView = fullPaper && view === "paper";
|
|
1131
|
+
// A host shows its own switch while the whole paper can be shown.
|
|
1132
|
+
useEffect(() => {
|
|
1133
|
+
const root = document.documentElement;
|
|
1134
|
+
if (fullPaper) root.dataset.fullPaper = "available";
|
|
1135
|
+
else delete root.dataset.fullPaper;
|
|
1136
|
+
}, [fullPaper]);
|
|
1137
|
+
// Keep the selected change in view when the view changes, from here or the host.
|
|
1138
|
+
const shownView = useRef(paperView);
|
|
1139
|
+
useEffect(() => {
|
|
1140
|
+
if (shownView.current === paperView) return;
|
|
1141
|
+
shownView.current = paperView;
|
|
1142
|
+
requestAnimationFrame(() => {
|
|
1143
|
+
const article = paper.current?.querySelector<HTMLElement>(`[data-stop="${snapshot.current.current}"]`);
|
|
1144
|
+
if (article) reveal(article);
|
|
1145
|
+
});
|
|
1146
|
+
}, [paperView]);
|
|
1147
|
+
function chooseView(next: ReviewView) {
|
|
1148
|
+
setView(next);
|
|
1149
|
+
try {
|
|
1150
|
+
localStorage.setItem(VIEW_STORAGE, next);
|
|
1151
|
+
} catch {
|
|
1152
|
+
/* The choice lasts for this page. */
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1097
1155
|
let index = 0;
|
|
1098
1156
|
return (
|
|
1099
1157
|
<>
|
|
@@ -1233,6 +1291,24 @@ export function ReviewApp({
|
|
|
1233
1291
|
{t("Undo revert")}
|
|
1234
1292
|
</button>
|
|
1235
1293
|
)}
|
|
1294
|
+
{fullPaper && (
|
|
1295
|
+
<div className="view-switch" role="group" aria-label={t("Review view")}>
|
|
1296
|
+
<button
|
|
1297
|
+
className={`ghost${view === "paper" ? " active" : ""}`}
|
|
1298
|
+
aria-pressed={view === "paper"}
|
|
1299
|
+
onClick={() => chooseView("paper")}
|
|
1300
|
+
>
|
|
1301
|
+
PDF
|
|
1302
|
+
</button>
|
|
1303
|
+
<button
|
|
1304
|
+
className={`ghost${view === "changes" ? " active" : ""}`}
|
|
1305
|
+
aria-pressed={view === "changes"}
|
|
1306
|
+
onClick={() => chooseView("changes")}
|
|
1307
|
+
>
|
|
1308
|
+
{t("Changes")}
|
|
1309
|
+
</button>
|
|
1310
|
+
</div>
|
|
1311
|
+
)}
|
|
1236
1312
|
{/* History is a toolbar action; its own row appears only while
|
|
1237
1313
|
browsing earlier decisions. */}
|
|
1238
1314
|
{detail && !history && !changes && (
|
|
@@ -1462,7 +1538,28 @@ export function ReviewApp({
|
|
|
1462
1538
|
/>
|
|
1463
1539
|
)}
|
|
1464
1540
|
<div className="document">
|
|
1465
|
-
{detail ? (
|
|
1541
|
+
{detail && paperView ? (
|
|
1542
|
+
<>
|
|
1543
|
+
<Suspense fallback={<div className="paper-status" role="status"><div className="pulse" /></div>}>
|
|
1544
|
+
<FullPaper
|
|
1545
|
+
url={detail.pdf!}
|
|
1546
|
+
changes={placed}
|
|
1547
|
+
render={(block, at) => change(block, at, true)}
|
|
1548
|
+
scrollRoot={paper}
|
|
1549
|
+
onReady={() => {
|
|
1550
|
+
const article = paper.current?.querySelector<HTMLElement>(
|
|
1551
|
+
`[data-stop="${snapshot.current.current}"]`,
|
|
1552
|
+
);
|
|
1553
|
+
if (article) reveal(article);
|
|
1554
|
+
}}
|
|
1555
|
+
onUnavailable={() => chooseView("changes")}
|
|
1556
|
+
/>
|
|
1557
|
+
</Suspense>
|
|
1558
|
+
{!stops.length && !changes && (
|
|
1559
|
+
<p className="empty">{t("No changes to review.")}</p>
|
|
1560
|
+
)}
|
|
1561
|
+
</>
|
|
1562
|
+
) : detail ? (
|
|
1466
1563
|
<>
|
|
1467
1564
|
{detail.sections.map((section) => (
|
|
1468
1565
|
<section className="part" key={section.node}>
|
|
@@ -247,6 +247,33 @@ kbd {
|
|
|
247
247
|
border-color: var(--line);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/* Full paper or changes only, as one segmented control (a host such as
|
|
251
|
+
Papilot shows its own instead). */
|
|
252
|
+
.view-switch {
|
|
253
|
+
display: inline-grid;
|
|
254
|
+
grid-template-columns: repeat(2, 1fr);
|
|
255
|
+
flex: none;
|
|
256
|
+
gap: 2px;
|
|
257
|
+
padding: 2px;
|
|
258
|
+
border: 1px solid var(--line);
|
|
259
|
+
border-radius: 9px;
|
|
260
|
+
background: var(--head);
|
|
261
|
+
}
|
|
262
|
+
.view-switch .ghost {
|
|
263
|
+
padding: 3px 10px;
|
|
264
|
+
border-radius: 6px;
|
|
265
|
+
font-weight: 500;
|
|
266
|
+
}
|
|
267
|
+
.view-switch .ghost:hover {
|
|
268
|
+
text-decoration: none;
|
|
269
|
+
border-color: transparent;
|
|
270
|
+
}
|
|
271
|
+
.view-switch .ghost.active {
|
|
272
|
+
color: var(--ink);
|
|
273
|
+
background: var(--paper);
|
|
274
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.06), 0 0 0 1px var(--line);
|
|
275
|
+
}
|
|
276
|
+
|
|
250
277
|
/* Document */
|
|
251
278
|
#paper {
|
|
252
279
|
overflow: auto;
|
|
@@ -898,6 +925,49 @@ kbd {
|
|
|
898
925
|
text-align: center;
|
|
899
926
|
padding: 90px 16px;
|
|
900
927
|
}
|
|
928
|
+
/* The whole paper: the new PDF's pages, opened where each change is shown
|
|
929
|
+
as in the changes view. A host's narrow-screen crop of the strips
|
|
930
|
+
(--pdf-crop-scale, --pdf-crop-shift) applies to the pages too. */
|
|
931
|
+
.paper-page {
|
|
932
|
+
background: var(--pdf-page, var(--paper));
|
|
933
|
+
box-shadow: var(--shadow);
|
|
934
|
+
border-radius: 3px;
|
|
935
|
+
margin-bottom: 14px;
|
|
936
|
+
}
|
|
937
|
+
.paper-context {
|
|
938
|
+
line-height: 0;
|
|
939
|
+
}
|
|
940
|
+
.paper-slice {
|
|
941
|
+
position: relative;
|
|
942
|
+
width: 100%;
|
|
943
|
+
}
|
|
944
|
+
.paper-slice canvas {
|
|
945
|
+
position: absolute;
|
|
946
|
+
inset: 0;
|
|
947
|
+
width: 100%;
|
|
948
|
+
height: 100%;
|
|
949
|
+
filter: var(--pdf-filter, none);
|
|
950
|
+
}
|
|
951
|
+
@media (max-width: 680px) {
|
|
952
|
+
.paper-slice {
|
|
953
|
+
width: calc(100% * var(--pdf-crop-scale, 1));
|
|
954
|
+
margin-left: calc(-100% * var(--pdf-crop-scale, 1) * var(--pdf-crop-shift, 0));
|
|
955
|
+
}
|
|
956
|
+
.paper-context {
|
|
957
|
+
overflow-x: clip;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
.paper-status {
|
|
961
|
+
text-align: center;
|
|
962
|
+
padding: 90px 16px;
|
|
963
|
+
color: var(--muted);
|
|
964
|
+
}
|
|
965
|
+
.paper-status p {
|
|
966
|
+
margin: 6px 0 14px;
|
|
967
|
+
}
|
|
968
|
+
.paper-clock {
|
|
969
|
+
font-variant-numeric: tabular-nums;
|
|
970
|
+
}
|
|
901
971
|
.waiting p {
|
|
902
972
|
margin: 6px 0;
|
|
903
973
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Whole paper or changed sections only. A host such as Papilot shows its own
|
|
2
|
+
// switch: it stores the choice and tells this frame; storage events also
|
|
3
|
+
// update other tabs.
|
|
4
|
+
export const VIEW_STORAGE = "latex-review:view";
|
|
5
|
+
export const VIEW_EVENT = "latex-review:view-changed";
|
|
6
|
+
export type ReviewView = "paper" | "changes";
|
|
7
|
+
export function readView(): ReviewView {
|
|
8
|
+
try {
|
|
9
|
+
return localStorage.getItem(VIEW_STORAGE) === "changes" ? "changes" : "paper";
|
|
10
|
+
} catch {
|
|
11
|
+
return "paper";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -66,6 +66,9 @@ export const zhCN: Record<string, string> = {
|
|
|
66
66
|
"Review history": "审阅历史",
|
|
67
67
|
"Keyboard shortcuts": "键盘快捷键",
|
|
68
68
|
"Review view": "审阅视图",
|
|
69
|
+
"Changes": "改动",
|
|
70
|
+
"Preparing the full PDF…": "正在准备完整 PDF…",
|
|
71
|
+
"The full PDF could not be loaded.": "无法加载完整 PDF。",
|
|
69
72
|
"Undo {action} · {changes}": "撤销{action} · {changes}",
|
|
70
73
|
"Change {list}": "修改 {list}",
|
|
71
74
|
"Changes {list}": "修改 {list}",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "papilot",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "A Cursor-like workspace for your papers: annotate and review PDFs directly without ever touching LaTeX.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"latex",
|
|
@@ -48,23 +48,25 @@
|
|
|
48
48
|
"scripts/demo.ts",
|
|
49
49
|
"scripts/service.ts",
|
|
50
50
|
"examples/papilot-paper",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
51
|
+
"engine/src",
|
|
52
|
+
"engine/dist",
|
|
53
|
+
"engine/package.json",
|
|
54
|
+
"engine/package-lock.json",
|
|
55
|
+
"engine/LICENSE",
|
|
53
56
|
"vendor/latex-review/package.json",
|
|
54
|
-
"vendor/latex-review/package-lock.json"
|
|
55
|
-
"vendor/latex-review/LICENSE"
|
|
57
|
+
"vendor/latex-review/package-lock.json"
|
|
56
58
|
],
|
|
57
59
|
"engines": {
|
|
58
60
|
"node": ">=22.22.0"
|
|
59
61
|
},
|
|
60
|
-
"desktopShell":
|
|
62
|
+
"desktopShell": 2,
|
|
61
63
|
"scripts": {
|
|
62
64
|
"dev": "tsx watch src/server/main.ts",
|
|
63
65
|
"start": "tsx src/server/main.ts",
|
|
64
66
|
"build": "tsc --noEmit && vite build",
|
|
65
67
|
"typecheck": "tsc --noEmit && tsc -p desktop/tsconfig.json --noEmit",
|
|
66
68
|
"test": "tsx --test tests/*.test.ts",
|
|
67
|
-
"setup:review": "npm --prefix
|
|
69
|
+
"setup:review": "npm --prefix engine ci && npm --prefix engine run build",
|
|
68
70
|
"demo": "tsx scripts/demo.ts",
|
|
69
71
|
"doctor": "node bin/papilot.mjs doctor",
|
|
70
72
|
"service:install": "tsx scripts/service.ts install",
|
|
@@ -79,7 +81,7 @@
|
|
|
79
81
|
"test:desktop": "node scripts/check-desktop.mjs",
|
|
80
82
|
"i18n": "tsx scripts/i18n.ts",
|
|
81
83
|
"i18n:check-app": "tsx scripts/i18n/app-locales.ts",
|
|
82
|
-
"test:locales": "npm run build && npm --prefix
|
|
84
|
+
"test:locales": "npm run build && npm --prefix engine run build && node --import tsx scripts/check-locales.mjs"
|
|
83
85
|
},
|
|
84
86
|
"dependencies": {
|
|
85
87
|
"@fastify/compress": "^9.2.2",
|
package/src/conversation.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function skillPrompt(skill: { name: string; body: string; folder?: string
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
interface FeedbackItem { kind?: string; round?: number | null; text?: string; changes?: (string | { file?: string; lines?: [number, number] })[] }
|
|
36
|
-
// The review engine's feedback (see
|
|
36
|
+
// The review engine's feedback (see engine FeedbackItem) as the chat shows it.
|
|
37
37
|
export function summarizeFeedback(feedback: { feedback?: FeedbackItem[]; build_error?: unknown }): FeedbackSummary | undefined {
|
|
38
38
|
const items = feedback.feedback || [];
|
|
39
39
|
if (!items.length && !feedback.build_error) return undefined;
|
package/src/server/app.ts
CHANGED
|
@@ -473,6 +473,10 @@ export async function buildApp(options: AppOptions) {
|
|
|
473
473
|
if (typeof b?.id !== 'string' || typeof b?.confirmed !== 'boolean') throw new Error('Choose an entry to confirm.');
|
|
474
474
|
return changes.confirmReference(project.id, project.path, Number(request.params.number), b.id, b.confirmed);
|
|
475
475
|
});
|
|
476
|
+
app.get<{ Params: { projectId: string; number: string } }>('/review/:projectId/changes/pdf/:number', changesAccess, (request, reply) => {
|
|
477
|
+
const pdf = changes.pdf(store.project(request.params.projectId).id, Number(request.params.number));
|
|
478
|
+
return reply.type('application/pdf').sendFile(path.basename(pdf), path.dirname(pdf), { cacheControl: false });
|
|
479
|
+
});
|
|
476
480
|
app.get<{ Params: { projectId: string; number: string; file: string } }>('/review/:projectId/changes/files/:number/:file', changesAccess, (request, reply) => {
|
|
477
481
|
const file = changes.file(store.project(request.params.projectId).id, Number(request.params.number), request.params.file);
|
|
478
482
|
return reply.header('Cache-Control', 'private, max-age=31536000, immutable').type(file.endsWith('.svg') ? 'image/svg+xml' : 'image/png').send(fs.createReadStream(file));
|
|
@@ -491,6 +495,14 @@ export async function buildApp(options: AppOptions) {
|
|
|
491
495
|
return serialGit(project.id, () => workspace.exclusiveCommit(project.id, () => changes.undo(project.id, project.path, csrfFor(request.viewer!), id)));
|
|
492
496
|
});
|
|
493
497
|
|
|
498
|
+
// The complete PDF of a round's new version, for the review's full paper.
|
|
499
|
+
// A round may compile only the changed chapters; this compiles them all once.
|
|
500
|
+
app.get<{ Params: { projectId: string; name: string; round: string } }>('/review/:projectId/pdf/:name/:round', { config: { access: { project: 'projectId' } } }, async (request, reply) => {
|
|
501
|
+
const project = store.project(request.params.projectId), name = request.params.name, round = Number(request.params.round);
|
|
502
|
+
if (!/^[a-z0-9-]+$/.test(name) || !Number.isSafeInteger(round) || round < 1) return reply.code(404).send({ error: 'Review resource not found.' });
|
|
503
|
+
const pdf = await fullPaperPdf(project.path, name, round, 'after');
|
|
504
|
+
return reply.type('application/pdf').sendFile(path.basename(pdf), path.dirname(pdf), { cacheControl: false });
|
|
505
|
+
});
|
|
494
506
|
app.route<{ Params: { projectId: string; '*': string } }>({
|
|
495
507
|
// The review page posts with the review engine's token rather than Papilot's.
|
|
496
508
|
method: ['GET', 'POST'], url: '/review/:projectId/*', config: { access: { project: 'projectId' }, csrf: false }, handler: async (request, reply) => {
|
|
@@ -528,6 +540,10 @@ export async function buildApp(options: AppOptions) {
|
|
|
528
540
|
await workspace.refresh(task.id);
|
|
529
541
|
}
|
|
530
542
|
}
|
|
543
|
+
// A round's new version as one complete PDF, which Papilot compiles,
|
|
544
|
+
// lets the review show the changes in the whole paper.
|
|
545
|
+
const round = request.method === 'GET' && response.ok ? /^\/api\/tasks\/([a-z0-9-]+)\/rounds\/(\d+)$/.exec(route) : null;
|
|
546
|
+
if (round) (value as { pdf?: string }).pdf = `${prefix}/pdf/${round[1]}/${round[2]}`;
|
|
531
547
|
return rewriteReviewJson(value, prefix);
|
|
532
548
|
}
|
|
533
549
|
if (reviewTask && writesFiles) throw new Error('The review engine returned an unexpected response. Select Check status before continuing.');
|
package/src/server/changes.ts
CHANGED
|
@@ -17,7 +17,7 @@ type Op = ['equal' | 'replace' | 'insert' | 'delete', number, number, number, nu
|
|
|
17
17
|
export interface Unit { id: string; file: string; status: 'added' | 'deleted' | 'modified'; kind: string; whole_file: boolean; ops: Op[]; old_start: number; old_end: number; new_start: number; new_end: number; old_text: string; new_text: string; truncated: boolean }
|
|
18
18
|
type Decision = 'pending' | 'accepted' | 'rejected' | 'commented';
|
|
19
19
|
export type ChangeAction = 'stage' | 'unstage' | 'revert';
|
|
20
|
-
interface Round { number: number; before: string; after: string; units: Unit[]; folder: string; warnings: string[]; decisions: Record<string, Decision
|
|
20
|
+
interface Round { number: number; before: string; after: string; units: Unit[]; folder: string; warnings: string[]; decisions: Record<string, Decision>; pdf?: string }
|
|
21
21
|
interface Job { key: string; phase: 'build' | 'render'; status: 'running' | 'failed'; created: number; error?: string; details?: string[]; log?: string }
|
|
22
22
|
interface Snapshot { before: string | null; after: string }
|
|
23
23
|
interface Entry { mode: string; sha: string }
|
|
@@ -139,7 +139,7 @@ export class Changes {
|
|
|
139
139
|
const { Builder, BuildError, Repo, loadProject, withUnits, render } = await engine();
|
|
140
140
|
const project = loadProject(root), repo = new Repo(root), number = comparison.next++;
|
|
141
141
|
const units = await this.units(projectId, root, snapshot), folder = path.join(this.dir, projectId, String(number));
|
|
142
|
-
let warnings: string[] = [];
|
|
142
|
+
let warnings: string[] = [], pdf: string | undefined;
|
|
143
143
|
fs.rmSync(folder, { recursive: true, force: true }); fs.mkdirSync(folder, { recursive: true });
|
|
144
144
|
if (units.length) {
|
|
145
145
|
const builder = new Builder(project, repo);
|
|
@@ -154,8 +154,9 @@ export class Changes {
|
|
|
154
154
|
await render(folder, { main: project.main, source_dir: project.source_dir, figures: project.figures, old, new: next, units },
|
|
155
155
|
(subset: Unit[]) => builder.build(withUnits(repo, snapshot.before!, snapshot.after, subset), ['full']));
|
|
156
156
|
warnings = next.warnings.filter((warning: string) => !old.warnings.includes(warning));
|
|
157
|
+
pdf = next.pdf;
|
|
157
158
|
} else fs.writeFileSync(path.join(folder, 'round.json'), JSON.stringify({ units: {}, sections: [], loose: [] }));
|
|
158
|
-
comparison.rounds.push({ number, before: snapshot.before!, after: snapshot.after, units, folder, warnings, decisions: {} });
|
|
159
|
+
comparison.rounds.push({ number, before: snapshot.before!, after: snapshot.after, units, folder, warnings, decisions: {}, pdf });
|
|
159
160
|
// The viewer may still show the round before; older ones go.
|
|
160
161
|
for (const old of comparison.rounds.splice(0, Math.max(0, comparison.rounds.length - 3))) fs.rmSync(old.folder, { recursive: true, force: true });
|
|
161
162
|
if (comparison.job === job) comparison.job = null;
|
|
@@ -203,6 +204,8 @@ export class Changes {
|
|
|
203
204
|
task: 'changes', round: round.number, before: round.before, after: round.after,
|
|
204
205
|
units: round.units.map(unit => ({ ...unit, ...render.units[unit.id] })), sections: render.sections, loose: render.loose,
|
|
205
206
|
warnings: round.warnings, files: `/review/${projectId}/changes/files/${round.number}/`,
|
|
207
|
+
// The working tree compiles in full, so its PDF shows the whole paper.
|
|
208
|
+
...(round.pdf ? { pdf: `/review/${projectId}/changes/pdf/${round.number}` } : {}),
|
|
206
209
|
...(render.references ? { references: render.references } : {}),
|
|
207
210
|
};
|
|
208
211
|
}
|
|
@@ -238,6 +241,12 @@ export class Changes {
|
|
|
238
241
|
return { places: await more(round.folder, unit) };
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
pdf(projectId: string, number: number) {
|
|
245
|
+
const file = this.comparison(projectId).rounds.find(item => item.number === number)?.pdf;
|
|
246
|
+
if (!file || !fs.existsSync(file)) throw Object.assign(new Error('This comparison is out of date. Refresh the page.'), { statusCode: 404 });
|
|
247
|
+
return file;
|
|
248
|
+
}
|
|
249
|
+
|
|
241
250
|
file(projectId: string, number: number, name: string) {
|
|
242
251
|
const round = this.comparison(projectId).rounds.find(item => item.number === number);
|
|
243
252
|
const file = round && /^[\w.-]+\.(svg|png)$/.test(name) ? path.join(round.folder, name) : '';
|
package/src/server/imports.ts
CHANGED
|
@@ -149,7 +149,9 @@ export class PaperImports {
|
|
|
149
149
|
private save(value: PaperImport) {
|
|
150
150
|
fs.mkdirSync(path.join(this.directory, 'imports'), { recursive: true, mode: 0o700 });
|
|
151
151
|
const file = path.join(this.directory, 'imports', value.id + '.json');
|
|
152
|
-
|
|
152
|
+
// Progress is live state; a restarted server fails a running import anyway.
|
|
153
|
+
const { progress: _progress, ...stored } = value;
|
|
154
|
+
fs.writeFileSync(file + '.tmp', JSON.stringify(stored), { mode: 0o600 }); fs.renameSync(file + '.tmp', file);
|
|
153
155
|
this.sessions.set(value.id, value); return { ...value };
|
|
154
156
|
}
|
|
155
157
|
get(id: string) {
|
|
@@ -207,7 +209,7 @@ export class PaperImports {
|
|
|
207
209
|
if (input.name !== undefined && (typeof input.name !== 'string' || !input.name.trim() || input.name.length > 120)) throw new Error('Enter a paper name under 120 characters.');
|
|
208
210
|
this.active.add(id);
|
|
209
211
|
const previousError = value.error || '';
|
|
210
|
-
Object.assign(value, { main, engine, ...(input.name ? { name: input.name } : {}), phase: repair ? 'repairing' : 'building', error: undefined });
|
|
212
|
+
Object.assign(value, { main, engine, ...(input.name ? { name: input.name } : {}), phase: repair ? 'repairing' : 'building', error: undefined, startedAt: Date.now(), progress: undefined });
|
|
211
213
|
this.save(value);
|
|
212
214
|
void (async () => {
|
|
213
215
|
try {
|
|
@@ -227,10 +229,13 @@ export class PaperImports {
|
|
|
227
229
|
// opened paper finds this PDF in the cache instead of compiling again.
|
|
228
230
|
this.manuscripts.forgetProject('import-' + id);
|
|
229
231
|
let result = await this.manuscripts.state('import-' + id, value.path);
|
|
230
|
-
while (result.build.status === 'building') {
|
|
232
|
+
while (result.build.status === 'building') {
|
|
233
|
+
value.progress = result.build.progress;
|
|
234
|
+
await new Promise(resolve => setTimeout(resolve, 400)); result = await this.manuscripts.state('import-' + id, value.path);
|
|
235
|
+
}
|
|
231
236
|
value.phase = result.build.status === 'ready' ? 'ready' : 'failed'; value.error = result.build.error;
|
|
232
237
|
} catch (error) { value.phase = 'failed'; value.error = (error as Error).message; }
|
|
233
|
-
finally { this.active.delete(id); this.save(value); }
|
|
238
|
+
finally { this.active.delete(id); value.progress = undefined; this.save(value); }
|
|
234
239
|
})();
|
|
235
240
|
return this.get(id);
|
|
236
241
|
}
|
package/src/server/main.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { pathToFileURL } from 'node:url';
|
|
|
3
3
|
import { ROOT, DATA } from './paths.ts';
|
|
4
4
|
import { activeInstallation, waitForActivation } from '../../bin/installation.mjs';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const desktop = process.env.PAPILOT_DESKTOP === '1';
|
|
7
|
+
if (!desktop) await waitForActivation(DATA);
|
|
8
|
+
const root = desktop ? ROOT : activeInstallation(ROOT, DATA);
|
|
8
9
|
// A source checkout's default data directory must not move with the release.
|
|
9
10
|
process.env.PAPILOT_DATA = DATA;
|
|
10
11
|
await import(pathToFileURL(path.join(root, 'src/server/start.ts')).href);
|
package/src/server/manuscript.ts
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { promisify } from 'node:util';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
10
|
import { ENGINE } from './paths.ts';
|
|
11
|
-
import type { Annotation, BuildProblem, GitFile, GitStatus, ManuscriptBuild, ManuscriptProblems } from '../shared.ts';
|
|
11
|
+
import type { Annotation, BuildProblem, BuildProgress, GitFile, GitStatus, ManuscriptBuild, ManuscriptProblems } from '../shared.ts';
|
|
12
12
|
import { notesInstructions } from '../notes.ts';
|
|
13
13
|
import { ignorePattern } from '../commit.ts';
|
|
14
14
|
import { AnnotationPages, placeAnnotation } from './annotation-placement.ts';
|
|
@@ -360,7 +360,7 @@ export async function pdfWords(pdf: string, page: number) {
|
|
|
360
360
|
// `problems` come from the last finished build. They are placed on the PDF
|
|
361
361
|
// named `problemsKey`; a failed build has no PDF of its own to place them on.
|
|
362
362
|
interface Build {
|
|
363
|
-
tree: string; status: ManuscriptBuild['status']; key?: string; pdf?: string; root?: string; sourceDir?: string; pages?: [number, number][]; builtAt?: number; error?: string;
|
|
363
|
+
tree: string; status: ManuscriptBuild['status']; key?: string; pdf?: string; root?: string; sourceDir?: string; pages?: [number, number][]; builtAt?: number; error?: string; progress?: BuildProgress;
|
|
364
364
|
problems?: StoredProblem[]; problemsKey?: string; log?: string; logFolder?: string;
|
|
365
365
|
}
|
|
366
366
|
|
|
@@ -402,7 +402,7 @@ export class Manuscripts extends EventEmitter {
|
|
|
402
402
|
let build = this.builds.get(projectId);
|
|
403
403
|
if (!build || (build.status !== 'building' && (build.tree !== tree || rebuild))) build = this.start(projectId, root, tree, build, rebuild);
|
|
404
404
|
const { status: state, key, pages, builtAt, error } = build;
|
|
405
|
-
return { git: status, build: { status: state, key, pages, builtAt, error, current: build.tree === tree && state === 'ready', problems: build.problems && countProblems(build.problems) } };
|
|
405
|
+
return { git: status, build: { status: state, key, pages, builtAt, error, current: build.tree === tree && state === 'ready', problems: build.problems && countProblems(build.problems), ...(state === 'building' && build.progress ? { progress: build.progress } : {}) } };
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
// Keep the previous pages visible while a newer working tree compiles.
|
|
@@ -415,7 +415,8 @@ export class Manuscripts extends EventEmitter {
|
|
|
415
415
|
try {
|
|
416
416
|
const { Builder, Repo, loadProject, mupdf } = await engine();
|
|
417
417
|
project = loadProject(root);
|
|
418
|
-
const
|
|
418
|
+
const startedAt = Date.now();
|
|
419
|
+
const result = await new Builder(project, new Repo(root)).build(tree, ['full'], null, { force, progress: (progress: Omit<BuildProgress, 'startedAt'>) => { if (this.builds.get(projectId) === build) build.progress = { ...progress, startedAt }; } });
|
|
419
420
|
const document = mupdf.Document.openDocument(fs.readFileSync(result.pdf), 'application/pdf');
|
|
420
421
|
const pages: [number, number][] = [];
|
|
421
422
|
for (let i = 0; i < document.countPages(); i++) { const [x0, y0, x1, y1] = document.loadPage(i).getBounds(); pages.push([Math.round(x1 - x0), Math.round(y1 - y0)]); }
|
package/src/server/paths.ts
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
5
5
|
export const DATA = path.resolve(process.env.PAPILOT_DATA || path.join(ROOT, '.papilot'));
|
|
6
|
-
export const ENGINE = path.resolve(process.env.PAPILOT_REVIEW_ENGINE || path.join(ROOT, '
|
|
6
|
+
export const ENGINE = path.resolve(process.env.PAPILOT_REVIEW_ENGINE || path.join(ROOT, 'engine'));
|
|
7
7
|
export const VERSION: string = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
|
|
8
8
|
export const PORT = Number(process.env.PAPILOT_PORT || 7274);
|
|
9
9
|
export const PUBLIC_ORIGIN = process.env.PAPILOT_PUBLIC_ORIGIN || '';
|
package/src/server/start.ts
CHANGED
|
@@ -10,7 +10,6 @@ import { Manuscripts } from './manuscript.ts';
|
|
|
10
10
|
import { Changes } from './changes.ts';
|
|
11
11
|
import { buildApp, gitQueue } from './app.ts';
|
|
12
12
|
import { Updates } from './updates.ts';
|
|
13
|
-
import { installRelease } from './update-install.ts';
|
|
14
13
|
import { readJson } from '../../bin/installation.mjs';
|
|
15
14
|
import { desktopDemo } from './desktop-demo.ts';
|
|
16
15
|
|
|
@@ -22,16 +21,10 @@ const accounts = new Accounts(store, ACCOUNT_ORIGIN || null);
|
|
|
22
21
|
const manuscripts = new Manuscripts(), changes = new Changes();
|
|
23
22
|
const desktop = process.env.PAPILOT_DESKTOP === '1';
|
|
24
23
|
if (desktop) await desktopDemo(ROOT, DATA, store, workspace, manuscripts).catch(error => console.error('Could not prepare the demo paper:', error));
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
const updates = new Updates({ root: ROOT, data: DATA, version: VERSION, port: PORT,
|
|
28
|
-
...(shell ? {
|
|
29
|
-
desktop: { shell, installer: process.env.PAPILOT_DESKTOP_INSTALLER || VERSION, downloadUrl },
|
|
30
|
-
install: (data: string, version: string) => installRelease(data, version, { shell, codex: process.env.PAPILOT_DESKTOP_CODEX }),
|
|
31
|
-
restart: async (root: string, version: string) => { process.send!({ type: 'papilot:activate', root, version }); },
|
|
32
|
-
} : desktop ? { supported: false, downloadUrl } : {}),
|
|
24
|
+
// Desktop releases are complete installers; npm updates belong to the web server.
|
|
25
|
+
const updates = new Updates({ root: ROOT, data: DATA, version: VERSION, port: PORT, desktop,
|
|
33
26
|
busy: () => workspace.busyForUpdate || manuscripts.busyForUpdate || changes.busyForUpdate, shutdown: () => shutdown() });
|
|
34
|
-
const activation = readJson(path.join(DATA, 'updates/active.json'));
|
|
27
|
+
const activation = desktop ? null : readJson(path.join(DATA, 'updates/active.json'));
|
|
35
28
|
const updateInstance = typeof activation?.root === 'string' && fs.existsSync(activation.root) && fs.realpathSync(activation.root) === fs.realpathSync(ROOT) ? activation.id : undefined;
|
|
36
29
|
const shared = {
|
|
37
30
|
store, workspace, accounts, manuscripts, changes, updates, updateInstance, csrf: randomBytes(32).toString('base64url'), serialGit: gitQueue(),
|
|
@@ -4,7 +4,7 @@ import net from 'node:net';
|
|
|
4
4
|
import { execFile, spawn } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { devRevision,
|
|
7
|
+
import { devRevision, readJson, stableVersion } from '../../bin/installation.mjs';
|
|
8
8
|
|
|
9
9
|
export const DEV_REPOSITORY = 'https://github.com/PapilotAI/papilot.git';
|
|
10
10
|
const git = async (args: string[], cwd: string) => (await promisify(execFile)('git', args, { cwd, timeout: 120000, maxBuffer: 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } })).stdout.trim();
|
|
@@ -18,19 +18,6 @@ export function npmCli(env = process.env, executable = process.execPath) {
|
|
|
18
18
|
}
|
|
19
19
|
throw new Error('npm is unavailable to Papilot.');
|
|
20
20
|
}
|
|
21
|
-
// A desktop installer runs a newer release when it provides the shell level the
|
|
22
|
-
// release declares and its bundled Node meets the release's engine range.
|
|
23
|
-
export const NEEDS_INSTALLER = 'This version needs the latest desktop app. Download it from GitHub Releases.';
|
|
24
|
-
export function desktopSupports(pkg: any, shell: number, node = process.versions.node) {
|
|
25
|
-
if (!Number.isInteger(pkg?.desktopShell) || pkg.desktopShell < 1 || pkg.desktopShell > shell) return false;
|
|
26
|
-
// npm's --engine-strict checks other range forms during installation.
|
|
27
|
-
const minimum = typeof pkg.engines?.node === 'string' ? /^\s*>=\s*v?(\d+\.\d+\.\d+)\s*$/.exec(pkg.engines.node)?.[1] : undefined;
|
|
28
|
-
return !minimum || !newerVersion(minimum, node);
|
|
29
|
-
}
|
|
30
|
-
// The installer's Codex serves releases that test the same or an older one.
|
|
31
|
-
export function codexVersion(bin: string | undefined) {
|
|
32
|
-
try { return readJson(path.join(path.dirname(fs.realpathSync(bin!)), '../package.json'))?.version || null; } catch { return null; }
|
|
33
|
-
}
|
|
34
21
|
export const loaderFor = (root: string) => createRequire(path.join(root, 'package.json')).resolve('tsx/esm');
|
|
35
22
|
export function runInstall(args: string[], cwd: string, log: number, timeout = 600000) {
|
|
36
23
|
return new Promise<void>((resolve, reject) => {
|
|
@@ -84,7 +71,7 @@ export async function prepareDev(root: string, version: string, log: number, run
|
|
|
84
71
|
// Test-only tools can exclude a Node version that supports the application.
|
|
85
72
|
// Build first, then enforce engine compatibility for the retained runtime.
|
|
86
73
|
await run(['ci', '--include=dev', '--engine-strict=false', ...flags], root, log);
|
|
87
|
-
const engine = path.join(root, '
|
|
74
|
+
const engine = path.join(root, 'engine');
|
|
88
75
|
await run(['ci', '--include=dev', '--engine-strict=false', ...flags], engine, log);
|
|
89
76
|
// Only this installed snapshot gets a dev version; the repository stays unchanged.
|
|
90
77
|
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ ...pkg, version }, null, 2) + '\n');
|
|
@@ -115,7 +102,8 @@ async function installDev(data: string, version: string) {
|
|
|
115
102
|
} catch (error) { fs.rmSync(stage, { recursive: true, force: true }); throw error; }
|
|
116
103
|
finally { fs.closeSync(log); }
|
|
117
104
|
}
|
|
118
|
-
export async function installRelease(data: string, version: string
|
|
105
|
+
export async function installRelease(data: string, version: string) {
|
|
106
|
+
if (process.env.PAPILOT_DESKTOP === '1') throw new Error('Download desktop updates from GitHub Releases.');
|
|
119
107
|
if (devRevision(version)) return installDev(data, version);
|
|
120
108
|
if (!stableVersion(version)) throw new Error('Invalid release version.');
|
|
121
109
|
const releases = path.join(data, 'updates/releases'); fs.mkdirSync(releases, { recursive: true, mode: 0o700 });
|
|
@@ -126,17 +114,12 @@ export async function installRelease(data: string, version: string, desktop?: {
|
|
|
126
114
|
await runInstall(['install', '--prefix', stage, '--save-exact', ...flags, `papilot@${version}`], stage, log);
|
|
127
115
|
const pkg = readJson(path.join(root, 'package.json'));
|
|
128
116
|
if (pkg?.name !== 'papilot' || pkg.version !== version) throw new Error('The downloaded package does not match the requested release.');
|
|
129
|
-
|
|
117
|
+
// Releases also ship an empty vendor/latex-review package, where 0.8.2 and
|
|
118
|
+
// earlier look for the engine when they install an update.
|
|
119
|
+
for (const file of ['dist/index.html', 'src/server/start.ts', 'src/server/main.ts', 'bin/papilot.mjs', 'engine/package-lock.json']) {
|
|
130
120
|
if (!fs.existsSync(path.join(root, file))) throw new Error('The downloaded release is incomplete.');
|
|
131
121
|
}
|
|
132
|
-
|
|
133
|
-
await runInstall(['ci', '--prefix', path.join(root, 'vendor/latex-review'), ...flags], stage, log);
|
|
134
|
-
// Like the CLI's first run, Codex installs with its own scripts. The app
|
|
135
|
-
// finds it beside the release.
|
|
136
|
-
const tested = pkg.devDependencies?.['@openai/codex'], bundled = desktop && codexVersion(desktop.codex);
|
|
137
|
-
if (desktop && stableVersion(tested) && (!bundled || newerVersion(tested, bundled))) {
|
|
138
|
-
await runInstall(['install', '--prefix', path.join(stage, 'codex'), '--registry=https://registry.npmjs.org', '--no-audit', '--no-fund', `@openai/codex@${tested}`], stage, log);
|
|
139
|
-
}
|
|
122
|
+
await runInstall(['ci', '--prefix', path.join(root, 'engine'), ...flags], stage, log);
|
|
140
123
|
await probeInstallation(root, version, stage, log);
|
|
141
124
|
return root;
|
|
142
125
|
} catch (error) { fs.rmSync(stage, { recursive: true, force: true }); throw error; }
|