papilot 0.8.0 → 0.8.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/README.de.md +110 -0
- package/README.es.md +110 -0
- package/README.fr.md +110 -0
- package/README.it.md +110 -0
- package/README.ja.md +110 -0
- package/README.ko.md +110 -0
- package/README.md +54 -121
- package/README.pt.md +110 -0
- package/README.sv.md +110 -0
- package/README.zh-CN.md +58 -117
- package/README.zh-Hant.md +110 -0
- package/bin/doctor.mjs +7 -5
- package/dist/assets/{_virtual_papilot-licenses-D7ro6ecN.js → _virtual_papilot-licenses-CMfCtEYQ.js} +1 -1
- package/dist/assets/index-oAIkxhfp.js +265 -0
- package/dist/assets/index-t_qCOF_B.css +1 -0
- package/dist/assets/{math-V9uUaUzA.js → math-CEU4m4-H.js} +1 -1
- package/dist/assets/{pdf-view-Bnb2uIBE.js → pdf-view-HWRGBLsZ.js} +1 -1
- package/dist/index.html +2 -2
- package/examples/papilot-paper/preview/main.pdf +0 -0
- package/examples/papilot-paper/preview/main.synctex.gz +0 -0
- package/package.json +18 -5
- package/src/server/acp.ts +44 -7
- package/src/server/agents.ts +5 -3
- package/src/server/app.ts +10 -3
- package/src/server/desktop-demo.ts +38 -0
- package/src/server/imports.ts +60 -21
- package/src/server/manuscript.ts +44 -8
- package/src/server/reconcile.ts +20 -0
- package/src/server/review.ts +3 -2
- package/src/server/start.ts +26 -4
- package/src/server/store.ts +0 -8
- package/src/server/update-install.ts +22 -2
- package/src/server/updates.ts +32 -12
- package/src/server/workspace.ts +78 -15
- package/src/shared.ts +20 -9
- package/src/skill-metadata.ts +1 -1
- package/vendor/latex-review/dist/web/assets/index-DVs7cTBl.js +19 -0
- package/vendor/latex-review/dist/web/assets/index-ysOpu05D.css +1 -0
- package/vendor/latex-review/dist/web/index.html +2 -2
- package/vendor/latex-review/src/core/build.ts +180 -97
- package/vendor/latex-review/src/core/current-commit.ts +104 -0
- package/vendor/latex-review/src/core/delivery.ts +62 -16
- package/vendor/latex-review/src/core/git.ts +135 -29
- package/vendor/latex-review/src/core/outline.ts +0 -9
- package/vendor/latex-review/src/core/publish.ts +7 -1
- package/vendor/latex-review/src/core/reconcile.ts +101 -0
- package/vendor/latex-review/src/core/review.ts +5 -2
- package/vendor/latex-review/src/core/store.ts +1 -0
- package/vendor/latex-review/src/core/tasks.ts +1 -1
- package/vendor/latex-review/src/locales.ts +47 -0
- package/vendor/latex-review/src/server.ts +12 -1
- package/vendor/latex-review/src/types.ts +5 -2
- package/vendor/latex-review/src/web/i18n.ts +9 -21
- package/vendor/latex-review/src/web/locales/de.json +250 -0
- package/vendor/latex-review/src/web/locales/es.json +250 -0
- package/vendor/latex-review/src/web/locales/fr.json +250 -0
- package/vendor/latex-review/src/web/locales/index.ts +16 -0
- package/vendor/latex-review/src/web/locales/it.json +250 -0
- package/vendor/latex-review/src/web/locales/ja.json +250 -0
- package/vendor/latex-review/src/web/locales/ko.json +250 -0
- package/vendor/latex-review/src/web/locales/pt.json +250 -0
- package/vendor/latex-review/src/web/locales/sv.json +250 -0
- package/vendor/latex-review/src/web/locales/zh-Hant.json +250 -0
- package/vendor/latex-review/src/web/references.tsx +1 -1
- package/vendor/latex-review/src/web/review.tsx +8 -6
- package/vendor/latex-review/src/web/style.css +1 -11
- package/vendor/latex-review/src/web/zh-CN.ts +4 -2
- package/dist/assets/index-Cc-awdNn.css +0 -1
- package/dist/assets/index-D_VX51no.js +0 -265
- package/vendor/latex-review/dist/web/assets/index-7VKAte5x.js +0 -19
- package/vendor/latex-review/dist/web/assets/index-DEz-LsGl.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@font-face{font-family:Review Serif;src:url(/assets/TeXGyrePagellaX-Regular-DQJjZE_L.otf)format("opentype");font-weight:400;font-display:swap}@font-face{font-family:Review Serif;src:url(/assets/TeXGyrePagellaX-Bold-CEtYdOXj.otf)format("opentype");font-weight:700;font-display:swap}:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--serif:"Review Serif", Palatino, "Palatino Linotype", "Songti SC", Georgia, serif;--sans:-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Noto Sans", sans-serif;--frame:#fff;--bar:#fff;--ink:#171717;--muted:#666;--line:#e5e5e5;--paper:#fff;--accent:#356aa0;--accept:#1d7448;--reject:#b0313b;--comment:#356aa0;--before:#fbeeee;--before-edge:#d17c82;--after:#eef8f1;--after-edge:#68ab82;--head:#fafafa;--warn:#8a5300;--warn-soft:#fff8e6;--warn-line:#f1d48c;--ok-soft:#eef8f1;--bad-soft:#fbeeee;--shadow:0 1px 2px #0000000f, 0 4px 18px #0000000d;--paper-top:24px;font:14px/1.5 var(--sans);color:var(--ink);background:var(--frame)}*{box-sizing:border-box}body{background:var(--frame);text-rendering:optimizelegibility;grid-template-rows:auto 1fr;height:100dvh;margin:0;display:grid;overflow:hidden}button,input,select,textarea{font:inherit;color:inherit}button{cursor:pointer}button:disabled{cursor:default;opacity:.38}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:3px}[hidden]{display:none!important}.muted{color:var(--muted)}a{color:var(--accent);text-underline-offset:.18em}h1,h2,.brand{font-family:var(--serif);font-weight:400;line-height:1.3}.brand{letter-spacing:.02em;color:var(--ink);font-size:20px;font-variant-caps:small-caps;text-decoration:none}kbd{border:1px solid var(--line);color:var(--muted);background:var(--frame);vertical-align:1px;border-radius:4px;margin-left:6px;padding:2px 4px;font:600 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace}.bar{background:var(--bar);border-bottom:1px solid var(--line);z-index:3;align-items:center;gap:18px;min-height:62px;padding:10px 24px;display:flex}.identity{flex:1;align-items:baseline;gap:10px;min-width:0;display:flex}.identity h1{letter-spacing:.02em;white-space:nowrap;text-overflow:ellipsis;margin:0;font-size:20px;font-variant-caps:small-caps;overflow:hidden}.identity span{white-space:nowrap;font-size:13px}.meter{color:var(--muted);white-space:nowrap;font-variant-numeric:tabular-nums;align-items:center;gap:10px;font-size:12px;display:flex}.review-progress{flex:none;min-height:28px;padding:0 2px;line-height:1}.review-progress-value{color:var(--ink);font-weight:500}.track{background:var(--line);width:120px;height:2px;overflow:hidden}.track i{background:#999;width:0;height:100%;transition:width .3s;display:block}.bar-end{align-items:center;gap:8px;display:flex}.status:empty{display:none}.status{background:var(--head);color:var(--muted);white-space:nowrap;border-radius:999px;padding:3px 9px;font-size:12px}.status[data-state=build_failed],.status[data-state=offline]{color:var(--reject)}.ghost{color:var(--muted);background:0 0;border:1px solid #0000;border-radius:7px;padding:5px 9px;font-size:13px}.ghost:hover{border-color:var(--line);color:var(--ink);text-underline-offset:.18em;text-decoration:underline}.accept-all,.reject-all{color:var(--ink);border-color:var(--line);border-radius:999px;box-shadow:0 1px 3px #0000000d}.batch-count{text-align:center;font-variant-numeric:tabular-nums;min-width:18px;color:var(--muted);background:#0000000f;border-radius:999px;margin-left:6px;padding:0 5px;font-size:11.5px;line-height:18px;display:inline-block}.overview-link{color:var(--accent);flex:none;text-decoration:none}.ghost.round{border-color:var(--line);border-radius:50%;width:30px;height:30px;padding:0}#paper{padding:var(--paper-top) 24px 90px;overflow-anchor:none;outline:none;scroll-padding:70px 0 90px;overflow:auto}.document{max-width:980px;margin:0 auto}.empty{text-align:center;color:var(--muted);padding:60px 16px}.part+.part{margin-top:30px}.part-title{color:var(--muted);letter-spacing:.02em;margin:14px 0 18px;font-size:19px;font-variant-caps:small-caps;font-weight:400}.part-title .number{font-variant-numeric:tabular-nums;margin-right:8px}.sheet{background:var(--paper);box-shadow:var(--shadow);border-radius:3px;padding:10px 0}.context,.side{line-height:0}.document img{width:100%;height:auto;display:block}.fold{color:#9a9d98;letter-spacing:4px;background:repeating-linear-gradient(90deg,#0000 0 6px,#ececec 6px 12px) 50%/100% 1px no-repeat;justify-content:center;align-items:center;height:22px;margin:4px 0;font-size:16px;display:flex}.change{color:var(--ink);outline:none;scroll-margin:70px;position:relative}.change.standalone{background:var(--paper);box-shadow:var(--shadow);border-radius:3px;margin-bottom:26px}.change.inline{margin:6px 0}.change-head{background:var(--head);color:var(--muted);border-top:1px solid var(--line);border-bottom:1px solid var(--line);align-items:baseline;gap:10px;padding:8px 12px;font-size:12px;line-height:1.6;display:flex}.change-number{color:var(--ink);white-space:nowrap;font-weight:400}.change-where{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font:11px/1.8 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.change-kind{white-space:nowrap}.badge{white-space:nowrap;font-weight:600}.badge:empty{display:none}.link{color:var(--accent);background:0 0;border:0;padding:0 2px;font-size:12px}.link.active{font-weight:600;text-decoration:underline}.edits{white-space:nowrap}.side{border-left:2px solid #0000;position:relative}.side.before{background:var(--before);border-left-color:var(--before-edge)}.side.after{background:var(--after);border-left-color:var(--after-edge)}.side.after.identical{border-left-color:var(--line);background:0 0}.side img{mix-blend-mode:multiply}.side:after{content:attr(aria-label);font:10px/1 var(--sans);letter-spacing:.06em;text-transform:uppercase;color:var(--before-edge);opacity:.9;pointer-events:none;position:absolute;top:4px;right:8px}.side.after:after{color:var(--after-edge)}.side.after.identical:after,.change[data-resolved] .side:after{content:none}.caption{color:var(--muted);margin:0;padding:4px 12px 8px;font-size:12px}.effects{border-top:1px solid var(--line);padding:6px 0 10px}.effects-head,.place-where{color:var(--muted);margin:0;padding:4px 14px;font-size:12px}.effects-head{font-weight:600}.effects-toggle{font:inherit;color:inherit}.effects-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.more-places{margin:6px 14px 0;font-size:12px}.change.current{box-shadow:0 0 0 2px var(--accent);z-index:1}.change.current .change-head{background:color-mix(in srgb, var(--accent) 8%, var(--head));color:var(--ink)}.change[data-state=accepted] .badge{color:var(--accept)}.change[data-state=rejected] .badge{color:var(--reject)}.change[data-state=commented] .badge,.change[data-state=mixed] .badge{color:var(--comment)}.change[data-resolved=after] .side.before,.change[data-resolved=before] .side.after:not(.identical){display:none}.change[data-resolved] .side{background:0 0;border-left-color:#0000}.change[data-resolved] .caption{display:none}.remark,.histories{padding:8px 14px;font-size:13px;line-height:1.55}.remark{background:color-mix(in srgb, var(--comment) 8%, #fff);border-top:1px solid var(--line);color:var(--ink)}.remark p,.histories p{white-space:pre-wrap;margin:0}.histories{border-bottom:1px solid var(--line);color:var(--muted);background:#f6f7fb}.source{background:var(--paper);border-top:1px solid var(--line);padding:10px 14px 12px;line-height:1.5}.source-unit+.source-unit{margin-top:12px}.source-title{color:#555;align-items:center;gap:8px;margin-bottom:6px;font:11px ui-monospace,SFMono-Regular,Menlo,monospace;display:flex}.unit-actions{font:13px var(--sans);align-items:center;gap:2px;margin-left:auto;display:flex}.unit-actions button{border:1px solid var(--line);background:var(--paper);border-radius:0;padding:1px 8px;font-weight:600}.unit-actions button:not(:disabled):hover{background:var(--frame)}.unit-actions .accept{color:var(--accept)}.unit-actions .reject{color:var(--reject)}.unit-actions .comment{color:var(--comment)}.unit-actions .unstage{color:var(--muted)}.unit-state{font-weight:600}.source-unit[data-state=accepted] .unit-state{color:var(--accept)}.source-unit[data-state=rejected] .unit-state{color:var(--reject)}.source-unit[data-state=commented] .unit-state{color:var(--comment)}.source-code{border:1px solid var(--line);white-space:pre-wrap;overflow-wrap:anywhere;color:#2b2f33;background:#fff;border-radius:5px;max-height:420px;margin:0;padding:10px 12px;font:12.5px/1.65 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:auto}.source-code del{color:#8e1d28;background:#fadfe1;-webkit-text-decoration:line-through #c9636b;text-decoration:line-through #c9636b}.source-code ins{color:#11603a;background:#d6f0df;text-decoration:none}.references{border-top:1px solid var(--line);background:var(--paper)}.reference-entry{border-left:3px solid var(--line);padding:10px 14px 12px}.reference-entry+.reference-entry{border-top:1px solid var(--line)}.reference-entry[data-check=verified],.reference-entry[data-check=confirmed]{border-left-color:var(--accept)}.reference-entry[data-check=mismatch],.reference-entry[data-check=unreadable]{border-left-color:var(--reject)}.reference-entry[data-check=unverified],.reference-entry[data-check=failed],.reference-entry[data-check=skipped],.reference-entry[data-check=off],.reference-entry[data-check=checking]{border-left-color:var(--warn-line)}.reference-head{flex-wrap:wrap;align-items:baseline;gap:4px 10px;margin-bottom:6px;display:flex}.reference-key{overflow-wrap:anywhere;font:600 12.5px ui-monospace,SFMono-Regular,Menlo,monospace}.reference-key del,.reference-entry td del{background:var(--bad-soft);color:var(--reject);text-decoration:line-through}.reference-key ins,.reference-entry td ins{background:var(--ok-soft);color:var(--accept);text-decoration:none}.reference-status{color:var(--muted);font-size:12px}.reference-check{border:1px solid var(--line);white-space:nowrap;border-radius:999px;padding:0 7px;font-size:12px;font-weight:600}.reference-check[data-check=verified],.reference-check[data-check=confirmed]{color:var(--accept);background:var(--ok-soft);border-color:#0000}.reference-check[data-check=mismatch]{color:var(--reject);background:var(--bad-soft);border-color:#0000}.reference-check[data-check=unverified],.reference-check[data-check=failed],.reference-check[data-check=skipped],.reference-check[data-check=off],.reference-check[data-check=checking]{color:var(--warn);background:var(--warn-soft);border-color:#0000}.reference-lines{color:var(--muted);margin-left:auto;font:11px ui-monospace,SFMono-Regular,Menlo,monospace}.reference-problem{background:var(--bad-soft);color:var(--reject);border-radius:6px;margin:4px 0 8px;padding:6px 10px;font-size:13px}.reference-uncited{border-top:1px solid var(--line);border-radius:0;margin:0}.reference-fields{border-collapse:collapse;width:100%;font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}.reference-fields th{text-align:left;vertical-align:top;width:1%;color:var(--muted);white-space:nowrap;padding:2px 12px 2px 0;font-weight:400}.reference-fields td{overflow-wrap:anywhere;padding:2px 0}.reference-entry[data-status=modified] tr:not([data-change]) td{color:var(--muted)}.reference-entry[data-status=removed] td{text-decoration:line-through;-webkit-text-decoration-color:var(--reject);text-decoration-color:var(--reject);color:var(--muted)}.reference-resolved{color:var(--muted)}.reference-cited{color:var(--muted);margin:6px 0 0;font-size:12px}.reference-verdict{background:var(--head);border-radius:6px;margin-top:8px;padding:8px 10px;font-size:13px}.reference-verdict p{margin:0}.reference-verdict[data-check=mismatch]{background:var(--bad-soft)}.reference-verdict[data-check=unverified],.reference-verdict[data-check=failed],.reference-verdict[data-check=off],.reference-verdict[data-check=skipped]{background:var(--warn-soft)}.reference-differences{grid-template-columns:auto 1fr;gap:6px 12px;margin:8px 0 0;display:grid}.reference-differences dt{white-space:nowrap;font-weight:600}.reference-differences dd{overflow-wrap:anywhere;gap:2px;min-width:0;margin:0;display:grid}.reference-side{letter-spacing:.02em;min-width:5.5em;color:var(--muted);margin-right:6px;font-size:11px;display:inline-block}.reference-record{overflow-wrap:anywhere;margin:8px 0 0!important}.reference-record a{color:var(--accent)}.reference-confirmation{margin:8px 0 0!important}.reference-confirm{border:1px solid var(--line);background:var(--paper);border-radius:6px;padding:3px 10px;font-size:12.5px}.reference-entry[data-check=confirmed] .reference-verdict{background:var(--head)}.references-banner summary strong{font-weight:600}.references-flagged{margin:6px 0 2px;padding-left:18px}.references-flagged li+li{margin-top:3px}.references-confirm ul{margin:0 0 12px;padding-left:18px;font-size:13px}.references-confirm li+li{margin-top:4px}.references-confirm code{overflow-wrap:anywhere;font-size:12.5px}.references-confirm-actions{flex-wrap:wrap;justify-content:flex-end;gap:8px;display:flex}.references-confirm-actions .accept{color:var(--accept);font-weight:600}@media (width<=560px){.reference-fields,.reference-fields tbody,.reference-fields tr,.reference-fields th,.reference-fields td{width:auto;display:block}.reference-fields tr+tr{margin-top:4px}.reference-fields th{padding:0;font-size:11px}.reference-differences{grid-template-columns:1fr;gap:2px}.reference-differences dd+dt{margin-top:6px}.reference-lines{margin-left:0}}.banner{background:color-mix(in srgb, var(--accent) 6%, var(--paper));border:1px solid var(--line);border-radius:8px;max-width:980px;margin:0 auto 14px;padding:10px 14px;font-size:13px}.banner p{margin:3px 0}.banner .details{background:var(--bar);white-space:pre-wrap;border-radius:5px;max-height:220px;margin:6px 0 2px;padding:8px 10px;font:11.5px/1.5 ui-monospace,Menlo,monospace;overflow:auto}.banner summary{cursor:pointer;color:var(--muted)}.waiting{text-align:center;padding:90px 16px}.waiting p{margin:6px 0}.pulse{background:var(--accent);border-radius:50%;width:12px;height:12px;margin:0 auto 18px;animation:1.6s ease-in-out infinite pulse}@keyframes pulse{0%,to{opacity:.25;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}}.task-list{background:var(--bar);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:8px;margin:0;padding:0;list-style:none}.task-list li+li{border-top:1px solid var(--line)}.index .document{max-width:880px}.overview-heading{margin:32px 0}.overview-heading h1{letter-spacing:.02em;margin:0 0 6px;font-size:34px;font-variant-caps:small-caps}.overview-heading p{margin:6px 0}.overview-summary{color:var(--muted);font-size:13px}.task-tools{gap:10px;margin-bottom:16px;display:flex}.task-tools input,.task-tools select{font:inherit;color:var(--ink);background:var(--bar);border:1px solid var(--line);border-radius:7px;padding:9px 12px}.task-tools input{flex:1;min-width:0}.task-tools input::placeholder{color:var(--muted)}.task-card{color:var(--ink);border-radius:7px;padding:24px 20px;text-decoration:none;display:block}.task-card:hover{background:var(--head)}.task-card:focus-visible,.overview-link:focus-visible,.task-tools :focus-visible{outline:2px solid var(--accent);outline-offset:2px}.task-heading{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.task-heading h2{overflow-wrap:anywhere;margin:0;font-size:21px;font-weight:400}.task-meta{color:var(--muted);flex-wrap:wrap;gap:4px 14px;margin-top:5px;font-size:12px;display:flex}.task-name{overflow-wrap:anywhere;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.task-progress{color:var(--muted);align-items:center;gap:10px;margin-top:14px;font-size:12px;display:flex}.task-progress .track{width:140px}.task-footer{justify-content:space-between;align-items:baseline;gap:10px;margin-top:9px;font-size:12px;display:flex}.task-counts{color:var(--muted);flex-wrap:wrap;gap:4px 14px;display:flex}.task-counts [data-kind=pending]{color:var(--accent);font-weight:600}.task-counts [data-kind=commented]{color:var(--comment)}.task-open{color:var(--accent);flex:none;font-weight:600}.actions{z-index:4;background:var(--bar);border:1px solid var(--line);max-width:calc(100vw - 24px);box-shadow:var(--shadow);border-radius:12px;align-items:center;gap:2px;padding:5px;display:flex;position:fixed;bottom:22px;left:50%;transform:translate(-50%)}.actions button{white-space:nowrap;background:0 0;border:0;border-radius:8px;padding:9px 14px;font-size:14px}.actions button:not(:disabled):hover{background:var(--head)}.actions .nav{color:var(--muted);padding:6px 12px;font-size:20px;line-height:1}.actions .accept{color:var(--accept)}.actions .reject{color:var(--reject)}.actions .comment{color:var(--comment)}.actions .unstage{color:var(--muted)}.actions .skip{font-weight:600}.actions .undo{color:var(--muted);border-left:1px solid var(--line);border-radius:0 8px 8px 0;margin-left:4px;font-size:13px}.actions .working{opacity:.55}.position{text-align:center;min-width:52px;color:var(--muted);font-variant-numeric:tabular-nums;font-size:12px}.complete{color:var(--muted);white-space:nowrap;padding:0 14px;font-size:13px}.change.current>.change-head{top:calc(-1 * var(--paper-top));z-index:4;position:sticky}.change-head .dock{flex:none;align-items:center;margin:0;display:flex}.change-head .actions{background:var(--bar);border:1px solid var(--line);border-radius:10px;gap:0;max-width:none;padding:3px;position:static;transform:none;box-shadow:0 1px 2px #00000014,0 6px 20px #0000001f}.change-head .actions button{padding:4px 8px;font-size:13px;line-height:1.5}.change-head .actions .nav{padding:2px 8px;font-size:17px}.change-head .actions .undo{margin-left:3px;padding:4px 10px;font-size:12px}.change-head .position{min-width:40px;font-size:11.5px}.change-head .complete{padding:0 8px;font-size:12px}.change-head .composer{width:min(380px,100vw - 24px);position:absolute;inset:calc(100% + 6px) 8px auto auto;transform:none}.composer{z-index:5;background:var(--bar);border:1px solid var(--line);border-radius:12px;width:min(540px,100vw - 24px);padding:12px;position:fixed;bottom:84px;left:50%;transform:translate(-50%);box-shadow:0 6px 24px #00000014}.composer label{color:var(--ink);margin-bottom:6px;font-size:17px;font-weight:400;display:block}.composer textarea{resize:vertical;border:1px solid var(--line);background:var(--paper);color:#1f2328;border-radius:7px;width:100%;min-height:84px;padding:8px 10px}.composer textarea:focus{outline:2px solid color-mix(in srgb, var(--comment) 45%, transparent);border-color:#0000}.composer-row{justify-content:space-between;align-items:center;margin-top:8px;font-size:12px;display:flex}.composer-row kbd{margin-left:0}.composer button{border:1px solid var(--line);background:var(--paper);color:var(--ink);border-radius:7px;padding:6px 16px}.toast{z-index:6;border:1px solid var(--line);background:var(--paper);max-width:min(640px,100vw - 24px);color:var(--ink);white-space:pre-wrap;padding:9px 14px;font-size:13px;position:fixed;top:74px;left:50%;transform:translate(-50%);box-shadow:0 3px 12px #0000000f}.toast.error{background:#7f2b31}dialog{border:1px solid var(--line);box-shadow:var(--shadow);background:var(--bar);color:var(--ink);border-radius:12px;max-width:min(460px,100vw - 32px);padding:20px 24px}dialog::backdrop{background:#00000040}dialog h2{margin:0 0 12px;font-size:23px}dialog dl{grid-template-columns:auto 1fr;gap:8px 16px;margin:0 0 12px;font-size:13px;display:grid}dialog dt{white-space:nowrap}dialog dt kbd{margin:0 2px 0 0}dialog dd{margin:0}dialog form{text-align:right}dialog button{border:1px solid var(--line);background:var(--frame);border-radius:7px;padding:5px 14px}@media (width<=720px){.bar{grid-template-columns:auto 1fr auto;gap:8px;padding:12px 16px;display:grid}.identity{grid-area:1/1/auto/-1}.bar .meter{justify-self:center;font-size:11px}.bar-end{gap:2px}.bar .ghost{padding:4px 2px;font-size:12px}.side{min-height:20px}.track{width:70px}.actions button{padding:9px}.change-head{flex-wrap:wrap}.change-head .dock{flex-basis:100%;order:9;justify-content:center;margin:4px 0 2px}.change-head .composer{width:calc(100vw - 24px);right:0}:root{--paper-top:10px}#paper{padding:var(--paper-top) 6px 90px}#paper.index{padding:10px 14px 40px}.overview-heading{margin-top:16px}.task-tools{flex-wrap:wrap}.task-tools input{flex-basis:100%}.task-tools select{width:100%}.task-card{padding:14px}.task-heading{flex-direction:column;gap:6px}}@media (prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important;animation:none!important}}body{display:block}#root{grid-template-rows:auto 1fr;grid-template-columns:minmax(0,1fr);height:100dvh;display:grid;overflow:hidden}.review-body{min-height:0;position:relative}.review-body>#paper{scrollbar-width:none;height:100%;padding-right:54px}.review-body>#paper::-webkit-scrollbar{display:none}.change-map{z-index:5;border:1px solid var(--line);touch-action:none;-webkit-user-select:none;user-select:none;background:#fafafa;border-radius:7px;width:22px;position:absolute;top:12px;bottom:12px;right:18px;box-shadow:0 1px 4px #0000000d}.change-map-viewport{cursor:grab;background:#6a6f741c;border:1px solid #c6c9cc;border-radius:5px;position:absolute;left:0;right:0}.change-map-viewport:active{cursor:grabbing}.change-map-mark{--mark:#b18a3c;background:var(--mark);border:0;border-radius:2px;width:calc(100% - 10px);min-height:0;padding:0;position:absolute;left:5px}.change-map-mark[data-state=accepted]{--mark:var(--accept)}.change-map-mark[data-state=rejected]{--mark:var(--reject)}.change-map-mark[data-state=commented]{--mark:var(--comment)}.change-map-mark[data-state=mixed]{--mark:#796195}.change-map-mark[aria-current]{outline:2px solid var(--ink);outline-offset:2px}.change-map-mark:hover,.change-map-mark:focus-visible{z-index:1;outline:2px solid var(--accent);outline-offset:2px}.change-map-tooltip{border:1px solid var(--line);background:var(--paper);width:max-content;max-width:min(360px,100vw - 64px);box-shadow:var(--shadow);color:var(--ink);font:12px/1.5 var(--sans);text-align:left;overflow-wrap:anywhere;pointer-events:none;visibility:hidden;border-radius:7px;padding:7px 10px;position:absolute;top:0;right:calc(100% + 12px)}.change-map-mark:hover .change-map-tooltip,.change-map-mark:focus-visible .change-map-tooltip{visibility:visible}@media (width<=720px){.review-body>#paper{padding-right:46px}.change-map{width:18px;right:16px}.change-map-mark{width:calc(100% - 6px);left:3px}.change-head .actions{flex-wrap:wrap;justify-content:center;max-width:100%}.change-head .actions button{padding:4px 6px;font-size:12px}.change-head .actions .nav{padding:2px 6px}.change-head .position{min-width:32px}}.pdf-strip{isolation:isolate;width:100%;position:relative;container-type:inline-size}.pdf-strip img{-webkit-user-select:none;user-select:none;pointer-events:none;position:absolute;inset:0}.pdf-text{color:#0000;-webkit-user-select:text;user-select:text;cursor:text;line-height:1;position:absolute;inset:0;overflow:hidden}.pdf-text-line{white-space:pre}.pdf-glyph{white-space:pre;transform-origin:0 0;color:#0000;font-family:Times New Roman,serif;font-weight:400;line-height:1;position:absolute}.pdf-glyph::selection{color:#0000;background:#437ee759}.copy-reference{white-space:nowrap}.change-head{flex-wrap:wrap}.change-head .change-where{min-width:160px}.document .source-unit button{line-height:1.5}.help-backdrop{z-index:8;background:#00000040;place-items:center;display:grid;position:fixed;inset:0}.help-backdrop dialog{margin:0;position:static}.composer{line-height:1.5}@media (width<=900px){.change-head .dock{flex-basis:100%;order:9;justify-content:center;margin:4px 0 2px}}#root:has(>.review-mode){grid-template-rows:auto auto minmax(0,1fr)}.reviewed-change,.change.reviewed-change.current{box-shadow:none;background:0 0;border:0}.reviewed-change>.change-head,.reviewed-change>.histories,.reviewed-change>.remark,.reviewed-change>.source,.reviewed-change>.caption,.mixed-reviewed>.side{display:none}.reviewed-placeholder{color:var(--muted);font:13px/1.5 var(--sans);text-align:center;margin:0;padding:10px 14px}.review-mode{border-bottom:1px solid var(--line);background:var(--paper);font:13px/1.4 var(--sans);flex-wrap:wrap;align-items:center;gap:8px;padding:6px 14px;display:flex}.review-mode>span,.review-mode>label{color:var(--muted);margin-right:auto}.review-mode label{align-items:center;gap:8px;display:flex}.review-mode select{border:1px solid var(--line);background:var(--paper);max-width:160px;font:inherit;color:var(--ink);border-radius:6px;padding:4px 8px}.review-mode .ghost{white-space:nowrap;padding:4px 8px;font-size:13px}.review-mode .undo{border:1px solid var(--line);border-radius:6px}.review-complete{max-width:var(--page-width);border:1px solid var(--line);background:var(--paper);font:14px/1.5 var(--sans);border-radius:10px;flex-wrap:wrap;align-items:baseline;gap:2px 8px;margin:0 auto 14px;padding:9px 14px;display:flex}.review-complete strong{font-weight:600}.review-complete span{color:var(--muted)}.review-complete-actions{align-items:center;gap:8px;margin-left:auto;display:flex}.review-complete-actions:empty{display:none}@media (width<=680px){.review-mode{padding:5px 8px}.review-mode .ghost{min-height:36px}.review-mode label{font-size:12px}.review-mode select{max-width:135px}}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
rel="icon"
|
|
10
10
|
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%232b6e5c'/%3E%3Cpath d='M4 8.5l2.5 2.5L12 5' stroke='white' stroke-width='2' fill='none'/%3E%3C/svg%3E"
|
|
11
11
|
/>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-DVs7cTBl.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-ysOpu05D.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
5
6
|
import { Repo } from "./git.ts";
|
|
6
7
|
import { listFiles, readJson, writeJson, withLock, now } from "./files.ts";
|
|
7
8
|
import { stripComments } from "./outline.ts";
|
|
@@ -28,6 +29,27 @@ const AUXILIARY = new Set([
|
|
|
28
29
|
".out",
|
|
29
30
|
".thm",
|
|
30
31
|
]);
|
|
32
|
+
// Output that TeX writes for this folder: SyncTeX data and the log name its
|
|
33
|
+
// absolute path.
|
|
34
|
+
const OUTPUT = /\.(?:pdf|xdv|dvi|log|synctex(?:\.gz)?)$/i;
|
|
35
|
+
function clone(file: string, target: string) {
|
|
36
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
37
|
+
fs.copyFileSync(file, target, fs.constants.COPYFILE_FICLONE);
|
|
38
|
+
}
|
|
39
|
+
// Output that latexmk kept from an earlier build folder still names that
|
|
40
|
+
// folder. Build folders differ only in their equal-length key, so the paths
|
|
41
|
+
// are replaced in place without moving anything else in the file.
|
|
42
|
+
export function relocate(file: string, from: string, to: string) {
|
|
43
|
+
if (!fs.existsSync(file)) return;
|
|
44
|
+
const old = Buffer.from(from + path.sep), next = Buffer.from(to + path.sep);
|
|
45
|
+
if (old.length !== next.length) return;
|
|
46
|
+
const zipped = file.endsWith(".gz"),
|
|
47
|
+
data = zipped ? gunzipSync(fs.readFileSync(file)) : fs.readFileSync(file);
|
|
48
|
+
let at = data.indexOf(old);
|
|
49
|
+
if (at < 0) return;
|
|
50
|
+
for (; at >= 0; at = data.indexOf(old, at + next.length)) next.copy(data, at);
|
|
51
|
+
fs.writeFileSync(file, zipped ? gzipSync(data) : data);
|
|
52
|
+
}
|
|
31
53
|
const hash = (value: string | Buffer) =>
|
|
32
54
|
createHash("sha256").update(value).digest("hex");
|
|
33
55
|
export const includes = (text: string) =>
|
|
@@ -215,6 +237,7 @@ export class Builder {
|
|
|
215
237
|
}
|
|
216
238
|
const tree = this.repo.treeId(rev, p.source_dir),
|
|
217
239
|
date = buildDate(),
|
|
240
|
+
context = hash(JSON.stringify([signature(p), version])),
|
|
218
241
|
key = hash(
|
|
219
242
|
JSON.stringify([
|
|
220
243
|
tree,
|
|
@@ -239,109 +262,169 @@ export class Builder {
|
|
|
239
262
|
fs.utimesSync(folder, new Date(), new Date());
|
|
240
263
|
return cached;
|
|
241
264
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
main = path.posix.basename(p.main),
|
|
250
|
-
job = path.posix.basename(main, ".tex"),
|
|
251
|
-
out = path.join(cwd, "build");
|
|
252
|
-
fs.mkdirSync(out, { recursive: true });
|
|
253
|
-
for (const target of includes(
|
|
254
|
-
fs.readFileSync(path.join(cwd, main), "utf8"),
|
|
255
|
-
))
|
|
256
|
-
fs.mkdirSync(path.join(out, path.dirname(target)), { recursive: true });
|
|
257
|
-
const args = [
|
|
258
|
-
"-norc",
|
|
259
|
-
...(p.latexmkrc
|
|
260
|
-
? ["-r", path.posix.relative(p.doc_dir || ".", p.latexmkrc)]
|
|
261
|
-
: []),
|
|
262
|
-
{ pdflatex: "-pdf", xelatex: "-xelatex", lualatex: "-lualatex" }[
|
|
263
|
-
p.engine
|
|
264
|
-
],
|
|
265
|
-
"-outdir=build",
|
|
266
|
-
`-jobname=${job}`,
|
|
267
|
-
"-synctex=1",
|
|
268
|
-
"-interaction=nonstopmode",
|
|
269
|
-
"-halt-on-error",
|
|
270
|
-
"-file-line-error",
|
|
271
|
-
];
|
|
272
|
-
if (partial) {
|
|
273
|
-
const seedOut = path.dirname(seed!.pdf);
|
|
274
|
-
for (const file of listFiles(seedOut))
|
|
275
|
-
if (AUXILIARY.has(path.extname(file))) {
|
|
276
|
-
const target = path.join(out, path.relative(seedOut, file));
|
|
277
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
278
|
-
if (!fs.existsSync(target)) fs.copyFileSync(file, target);
|
|
279
|
-
}
|
|
280
|
-
const preload = Object.entries(p.preload)
|
|
281
|
-
.filter(
|
|
282
|
-
([name, file]) =>
|
|
283
|
-
!targets.includes(name) && fs.existsSync(path.join(cwd, file)),
|
|
284
|
-
)
|
|
285
|
-
.map(([, file]) => `\\input{${file}}`)
|
|
286
|
-
.join("");
|
|
287
|
-
args.push(
|
|
288
|
-
`-usepretex=\\includeonly{${targets.join(",")}}` +
|
|
289
|
-
(preload ? `\\AtBeginDocument{${preload}}` : ""),
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
args.push(main);
|
|
293
|
-
const transcript = path.join(folder, "latexmk.log"),
|
|
294
|
-
fd = fs.openSync(transcript, "w"),
|
|
295
|
-
started = now();
|
|
296
|
-
let failure: Error | null = null;
|
|
265
|
+
// A full build continues from the latest completed one, or from the
|
|
266
|
+
// author's own latexmk output, as latexmk does when a folder is
|
|
267
|
+
// compiled again: unchanged files are cloned and LaTeX keeps its state,
|
|
268
|
+
// so it reruns only as often as the edit requires. Manual recompilation
|
|
269
|
+
// starts clean, and so does a retry when leftover state broke a build.
|
|
270
|
+
const start = partial || options.force ? null : this.latest(context) ?? "author";
|
|
271
|
+
let result: BuildInfo;
|
|
297
272
|
try {
|
|
298
|
-
await
|
|
299
|
-
cwd,
|
|
300
|
-
env: { ...process.env, ...date },
|
|
301
|
-
timeout: 1800000,
|
|
302
|
-
output: (chunk) => fs.writeSync(fd, chunk),
|
|
303
|
-
});
|
|
273
|
+
result = await this.compile(rev, tree, key, targets, seed, date, start);
|
|
304
274
|
} catch (error) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
fs.closeSync(fd);
|
|
275
|
+
if (!start || (error instanceof BuildError && !error.details.some(line => /(?:^|\s|\()(?:\.\/)?build\//.test(line)))) throw error;
|
|
276
|
+
result = await this.compile(rev, tree, key, targets, seed, date, null);
|
|
308
277
|
}
|
|
309
|
-
|
|
310
|
-
log = path.join(out, job + ".log"),
|
|
311
|
-
logText = fs.existsSync(log) ? fs.readFileSync(log, "utf8") : "";
|
|
312
|
-
if (failure || !fs.existsSync(pdf)) {
|
|
313
|
-
const transcriptText = fs.readFileSync(transcript, "utf8"),
|
|
314
|
-
details = compilerDiagnostics(logText, transcriptText),
|
|
315
|
-
kept = path.join(p.state, "logs", key + ".log");
|
|
316
|
-
fs.mkdirSync(path.dirname(kept), { recursive: true });
|
|
317
|
-
fs.writeFileSync(
|
|
318
|
-
kept,
|
|
319
|
-
transcriptText + (logText ? "\n\n--- TeX log ---\n" + logText : ""),
|
|
320
|
-
);
|
|
321
|
-
const tail = transcriptText.split("\n").slice(-25);
|
|
322
|
-
fs.rmSync(folder, { recursive: true, force: true });
|
|
323
|
-
throw new BuildError(
|
|
324
|
-
failure?.message.includes("ENOENT")
|
|
325
|
-
? "latexmk was not found. Install TeX Live or MacTeX."
|
|
326
|
-
: "LaTeX failed to compile.",
|
|
327
|
-
details.length ? details : tail,
|
|
328
|
-
kept,
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
const result: BuildInfo = {
|
|
332
|
-
key,
|
|
333
|
-
tree,
|
|
334
|
-
targets,
|
|
335
|
-
root: source,
|
|
336
|
-
cwd,
|
|
337
|
-
pdf,
|
|
338
|
-
seconds: Math.round((now() - started) * 10) / 10,
|
|
339
|
-
warnings: warnings(logText),
|
|
340
|
-
};
|
|
341
|
-
writeJson(path.join(folder, "result.json"), result);
|
|
278
|
+
if (!partial) writeJson(path.join(this.cache, "latest.json"), { key, context, date: date.SOURCE_DATE_EPOCH });
|
|
342
279
|
return result;
|
|
343
280
|
});
|
|
344
281
|
}
|
|
282
|
+
private latest(context: string): { info: BuildInfo; date: unknown } | null {
|
|
283
|
+
const latest = readJson<{ key?: unknown; context?: unknown; date?: unknown } | null>(path.join(this.cache, "latest.json"), null);
|
|
284
|
+
if (typeof latest?.key !== "string" || !/^[0-9a-f]{32}$/.test(latest.key) || latest.context !== context) return null;
|
|
285
|
+
const info = readJson<BuildInfo | null>(path.join(this.cache, latest.key, "result.json"), null);
|
|
286
|
+
return info && JSON.stringify(info.targets) === '["full"]' && fs.existsSync(info.pdf) && fs.existsSync(info.root) ? { info, date: latest.date } : null;
|
|
287
|
+
}
|
|
288
|
+
private async compile(
|
|
289
|
+
rev: string,
|
|
290
|
+
tree: string,
|
|
291
|
+
key: string,
|
|
292
|
+
targets: string[],
|
|
293
|
+
seed: BuildInfo | null,
|
|
294
|
+
date: ReturnType<typeof buildDate>,
|
|
295
|
+
start: { info: BuildInfo; date: unknown } | "author" | null,
|
|
296
|
+
): Promise<BuildInfo> {
|
|
297
|
+
const p = this.project,
|
|
298
|
+
partial = JSON.stringify(targets) !== '["full"]',
|
|
299
|
+
folder = path.join(this.cache, key);
|
|
300
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
301
|
+
const source = path.join(folder, "src");
|
|
302
|
+
const base = start && start !== "author" ? start.info : null;
|
|
303
|
+
await this.repo.export(rev, p.source_dir, source, base ? { root: base.root, tree: base.tree } : undefined);
|
|
304
|
+
const cwd = path.join(
|
|
305
|
+
source,
|
|
306
|
+
path.posix.relative(p.source_dir || ".", p.doc_dir || "."),
|
|
307
|
+
),
|
|
308
|
+
main = path.posix.basename(p.main),
|
|
309
|
+
job = path.posix.basename(main, ".tex"),
|
|
310
|
+
out = path.join(cwd, "build");
|
|
311
|
+
fs.mkdirSync(out, { recursive: true });
|
|
312
|
+
const included = includes(fs.readFileSync(path.join(cwd, main), "utf8"));
|
|
313
|
+
for (const target of included)
|
|
314
|
+
fs.mkdirSync(path.join(out, path.dirname(target)), { recursive: true });
|
|
315
|
+
// The PDF, SyncTeX data and log carry over only within a day, so that
|
|
316
|
+
// \today stays current; latexmk then skips TeX when no input changed.
|
|
317
|
+
const reused = base !== null && start !== "author" && start?.date === date.SOURCE_DATE_EPOCH;
|
|
318
|
+
if (base) {
|
|
319
|
+
const previous = path.dirname(base.pdf);
|
|
320
|
+
for (const file of listFiles(previous))
|
|
321
|
+
if (!/\.fls$/i.test(file) && (reused || !OUTPUT.test(file)))
|
|
322
|
+
clone(file, path.join(out, path.relative(previous, file)));
|
|
323
|
+
} else if (start === "author") {
|
|
324
|
+
// Only this job's state: the author's folder may hold other editions.
|
|
325
|
+
const own = path.join(p.root, p.doc_dir, "build");
|
|
326
|
+
if (fs.existsSync(path.join(own, job + ".fdb_latexmk")))
|
|
327
|
+
for (const name of [
|
|
328
|
+
...fs.readdirSync(own).filter((name) => (name.startsWith(job + ".") && !OUTPUT.test(name) && !name.endsWith(".fls")) || name.startsWith("_minted")),
|
|
329
|
+
...included.map((target) => target + ".aux"),
|
|
330
|
+
])
|
|
331
|
+
try {
|
|
332
|
+
if (fs.statSync(path.join(own, name)).isDirectory()) fs.cpSync(path.join(own, name), path.join(out, name), { recursive: true });
|
|
333
|
+
else clone(path.join(own, name), path.join(out, name));
|
|
334
|
+
} catch {
|
|
335
|
+
/* A missing file is regenerated. */
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const args = [
|
|
339
|
+
"-norc",
|
|
340
|
+
...(p.latexmkrc
|
|
341
|
+
? ["-r", path.posix.relative(p.doc_dir || ".", p.latexmkrc)]
|
|
342
|
+
: []),
|
|
343
|
+
{ pdflatex: "-pdf", xelatex: "-xelatex", lualatex: "-lualatex" }[
|
|
344
|
+
p.engine
|
|
345
|
+
],
|
|
346
|
+
"-outdir=build",
|
|
347
|
+
`-jobname=${job}`,
|
|
348
|
+
"-synctex=1",
|
|
349
|
+
"-interaction=nonstopmode",
|
|
350
|
+
"-halt-on-error",
|
|
351
|
+
"-file-line-error",
|
|
352
|
+
];
|
|
353
|
+
if (partial) {
|
|
354
|
+
const seedOut = path.dirname(seed!.pdf);
|
|
355
|
+
for (const file of listFiles(seedOut))
|
|
356
|
+
if (AUXILIARY.has(path.extname(file))) {
|
|
357
|
+
const target = path.join(out, path.relative(seedOut, file));
|
|
358
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
359
|
+
if (!fs.existsSync(target)) fs.copyFileSync(file, target);
|
|
360
|
+
}
|
|
361
|
+
const preload = Object.entries(p.preload)
|
|
362
|
+
.filter(
|
|
363
|
+
([name, file]) =>
|
|
364
|
+
!targets.includes(name) && fs.existsSync(path.join(cwd, file)),
|
|
365
|
+
)
|
|
366
|
+
.map(([, file]) => `\\input{${file}}`)
|
|
367
|
+
.join("");
|
|
368
|
+
args.push(
|
|
369
|
+
`-usepretex=\\includeonly{${targets.join(",")}}` +
|
|
370
|
+
(preload ? `\\AtBeginDocument{${preload}}` : ""),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
args.push(main);
|
|
374
|
+
const transcript = path.join(folder, "latexmk.log"),
|
|
375
|
+
fd = fs.openSync(transcript, "w"),
|
|
376
|
+
started = now();
|
|
377
|
+
let failure: Error | null = null;
|
|
378
|
+
try {
|
|
379
|
+
await runAsync("latexmk", args, {
|
|
380
|
+
cwd,
|
|
381
|
+
env: { ...process.env, ...date },
|
|
382
|
+
timeout: 1800000,
|
|
383
|
+
output: (chunk) => fs.writeSync(fd, chunk),
|
|
384
|
+
});
|
|
385
|
+
} catch (error) {
|
|
386
|
+
failure = error as Error;
|
|
387
|
+
} finally {
|
|
388
|
+
fs.closeSync(fd);
|
|
389
|
+
}
|
|
390
|
+
const pdf = path.join(out, job + ".pdf"),
|
|
391
|
+
log = path.join(out, job + ".log");
|
|
392
|
+
if (reused && !failure)
|
|
393
|
+
for (const file of [path.join(out, job + ".synctex.gz"), log])
|
|
394
|
+
relocate(file, path.dirname(base!.root), folder);
|
|
395
|
+
const logText = fs.existsSync(log) ? fs.readFileSync(log, "utf8") : "";
|
|
396
|
+
if (failure || !fs.existsSync(pdf)) {
|
|
397
|
+
const transcriptText = fs.readFileSync(transcript, "utf8"),
|
|
398
|
+
details = compilerDiagnostics(logText, transcriptText),
|
|
399
|
+
kept = path.join(p.state, "logs", key + ".log");
|
|
400
|
+
fs.mkdirSync(path.dirname(kept), { recursive: true });
|
|
401
|
+
fs.writeFileSync(
|
|
402
|
+
kept,
|
|
403
|
+
transcriptText + (logText ? "\n\n--- TeX log ---\n" + logText : ""),
|
|
404
|
+
);
|
|
405
|
+
const tail = transcriptText.split("\n").slice(-25);
|
|
406
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
407
|
+
throw new BuildError(
|
|
408
|
+
failure?.message.includes("ENOENT")
|
|
409
|
+
? "latexmk was not found. Install TeX Live or MacTeX."
|
|
410
|
+
: "LaTeX failed to compile.",
|
|
411
|
+
details.length ? details : tail,
|
|
412
|
+
kept,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const result: BuildInfo = {
|
|
416
|
+
key,
|
|
417
|
+
tree,
|
|
418
|
+
targets,
|
|
419
|
+
root: source,
|
|
420
|
+
cwd,
|
|
421
|
+
pdf,
|
|
422
|
+
seconds: Math.round((now() - started) * 10) / 10,
|
|
423
|
+
warnings: warnings(logText),
|
|
424
|
+
};
|
|
425
|
+
writeJson(path.join(folder, "result.json"), result);
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
345
428
|
async prune(keep: Set<string>, maxAge = 3 * 86400) {
|
|
346
429
|
if (!fs.existsSync(this.cache)) return;
|
|
347
430
|
for (const entry of fs.readdirSync(this.cache, { withFileTypes: true })) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { Repo } from "./git.ts";
|
|
6
|
+
import { merge3 } from "./git.ts";
|
|
7
|
+
import type { Delivery } from "../types.ts";
|
|
8
|
+
|
|
9
|
+
export interface CombinedCommitReview { token: string; files: string[]; diff: string }
|
|
10
|
+
export class CommitNeedsReview extends Error {
|
|
11
|
+
constructor(public review: CombinedCommitReview) { super("Earlier edits to the same passages need to be included. Review the combined commit or apply without committing."); }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Project only the task delta onto HEAD. The worktree and staging area are
|
|
15
|
+
// separate destinations: neither is used as the content of the commit.
|
|
16
|
+
export function planCurrentCommit(repo: Repo, data: Delivery, before: string, files: string[], message: string, confirmation?: string) {
|
|
17
|
+
const ref = repo.text(["symbolic-ref", "-q", "HEAD"], { check: false });
|
|
18
|
+
if (!ref.startsWith("refs/heads/")) throw new Error("Open a branch to commit this task, or apply it without committing.");
|
|
19
|
+
const parent = repo.rev("HEAD"), index = repo.text(["write-tree"]);
|
|
20
|
+
const base = repo.blobs(before, files), accepted = repo.blobs(data.accepted, files), head = repo.blobs(parent, files), staged = repo.blobs(index, files);
|
|
21
|
+
const committed: Record<string, Buffer | null> = {}, nextIndex: Record<string, Buffer | null> = {};
|
|
22
|
+
const dependencies: string[] = [];
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
let [content, conflict] = merge3(head[file], base[file], accepted[file]);
|
|
25
|
+
if (conflict) {
|
|
26
|
+
// Include only the overlapping earlier passages, never the whole dirty
|
|
27
|
+
// file. This is a proposal until the author confirms its exact diff.
|
|
28
|
+
[content, conflict] = merge3(accepted[file], base[file], head[file], true);
|
|
29
|
+
if (conflict) throw new Error(`The earlier changes in ${file} cannot be included automatically. Apply to workspace to save your reviewed result without a commit.`);
|
|
30
|
+
dependencies.push(file);
|
|
31
|
+
}
|
|
32
|
+
let [combined, indexConflict] = merge3(staged[file], head[file], content);
|
|
33
|
+
if (indexConflict && dependencies.includes(file) && !merge3(staged[file], base[file], accepted[file])[1])
|
|
34
|
+
[combined, indexConflict] = merge3(content, head[file], staged[file], true);
|
|
35
|
+
if (indexConflict) throw new Error(`A different version of ${file} is already selected for another commit. Apply to workspace to save this result without changing that selection.`);
|
|
36
|
+
committed[file] = content;
|
|
37
|
+
nextIndex[file] = combined;
|
|
38
|
+
}
|
|
39
|
+
const tree = repo.treeWith(parent, committed);
|
|
40
|
+
if (tree === repo.treeId(parent)) throw new Error("These changes are already in the current commit. Apply without committing to finish this task.");
|
|
41
|
+
if (dependencies.length || confirmation) {
|
|
42
|
+
const token = createHash("sha256").update(JSON.stringify([ref, parent, index, before, data.accepted, tree, data.writes])).digest("hex");
|
|
43
|
+
if (confirmation !== token) {
|
|
44
|
+
const diff = repo.text(["diff", "--no-ext-diff", "--no-textconv", parent, tree, "--"]);
|
|
45
|
+
if (diff.length > 500000) throw new Error("The combined changes are too large to preview here. Apply to workspace and review them in Git before committing.");
|
|
46
|
+
throw new CommitNeedsReview({ token, files: dependencies, diff });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
data.branch = ref.slice("refs/heads/".length);
|
|
50
|
+
data.parent = parent;
|
|
51
|
+
data.indexBefore = index;
|
|
52
|
+
data.indexAfter = repo.treeWith(index, nextIndex);
|
|
53
|
+
data.commit = repo.text(["commit-tree", tree, "-p", parent, "-m", message.trim()]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Hold Git's index lock while checking and installing the recorded result.
|
|
57
|
+
// CAS on the branch prevents updating the wrong or concurrently moved HEAD.
|
|
58
|
+
// A retry recognizes our commit and completes its files/index without a
|
|
59
|
+
// duplicate commit. Unrelated staged entries (including their flags) survive.
|
|
60
|
+
export function finishCurrentCommit(repo: Repo, data: Delivery, validateFiles: () => void, writeFiles: () => void) {
|
|
61
|
+
const index = path.resolve(repo.root, repo.text(["rev-parse", "--git-path", "index"]));
|
|
62
|
+
const lock = index + ".lock";
|
|
63
|
+
let fd: number;
|
|
64
|
+
try { fd = fs.openSync(lock, "wx", fs.existsSync(index) ? fs.statSync(index).mode & 0o777 : 0o644); }
|
|
65
|
+
catch (error) {
|
|
66
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") throw new Error("Another Git operation is using the staging area. Wait for it to finish, then retry.");
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
let temp: string | undefined;
|
|
70
|
+
try {
|
|
71
|
+
temp = fs.mkdtempSync(path.join(os.tmpdir(), "papilot-task-index-"));
|
|
72
|
+
const ref = `refs/heads/${data.branch}`;
|
|
73
|
+
if (repo.text(["symbolic-ref", "-q", "HEAD"], { check: false }) !== ref)
|
|
74
|
+
throw new Error("The current branch changed. Return to the original branch before retrying this task commit.");
|
|
75
|
+
const head = repo.rev("HEAD");
|
|
76
|
+
if (head !== data.parent && head !== data.commit)
|
|
77
|
+
throw new Error("The current branch moved during this task commit. Nothing else was changed; inspect the Git history before retrying.");
|
|
78
|
+
const target = path.join(temp, "index");
|
|
79
|
+
fs.copyFileSync(index, target);
|
|
80
|
+
const env = { ...process.env, GIT_INDEX_FILE: target };
|
|
81
|
+
const currentIndex = repo.text(["write-tree"], { env });
|
|
82
|
+
if (currentIndex !== data.indexBefore && currentIndex !== data.indexAfter)
|
|
83
|
+
throw new Error("The staged changes changed during this task commit. Nothing else was changed; restore the previous staging selection before retrying.");
|
|
84
|
+
validateFiles();
|
|
85
|
+
if (currentIndex !== data.indexAfter) {
|
|
86
|
+
const changed = repo.changedFiles(data.indexBefore!, data.indexAfter!);
|
|
87
|
+
for (const file of changed) {
|
|
88
|
+
const blob = repo.blob(data.indexAfter!, file);
|
|
89
|
+
if (blob === null) repo.run(["update-index", "--force-remove", "--", file], { env });
|
|
90
|
+
else repo.run(["update-index", "--add", "--cacheinfo", `${repo.mode(data.indexAfter!, file)},${repo.hashObject(blob)},${file}`], { env });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (repo.text(["write-tree"], { env }) !== data.indexAfter) throw new Error("Could not prepare the task's staging result.");
|
|
94
|
+
fs.writeFileSync(fd, fs.readFileSync(target));
|
|
95
|
+
fs.fsyncSync(fd);
|
|
96
|
+
if (head !== data.commit) repo.run(["update-ref", "-m", "Papilot: commit reviewed task", ref, data.commit!, data.parent!]);
|
|
97
|
+
writeFiles();
|
|
98
|
+
fs.renameSync(lock, index);
|
|
99
|
+
} finally {
|
|
100
|
+
fs.closeSync(fd);
|
|
101
|
+
fs.rmSync(lock, { force: true });
|
|
102
|
+
if (temp) fs.rmSync(temp, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -4,8 +4,11 @@ import type { App } from "../server.ts";
|
|
|
4
4
|
import type { Delivery } from "../types.ts";
|
|
5
5
|
import { merge3 } from "./git.ts";
|
|
6
6
|
import { readBytes, same, writeOrDelete } from "./files.ts";
|
|
7
|
+
import { planCurrentCommit, finishCurrentCommit, CommitNeedsReview } from "./current-commit.ts";
|
|
7
8
|
import { safePath } from "./review.ts";
|
|
8
9
|
|
|
10
|
+
class DeliveryConflict extends Error {}
|
|
11
|
+
|
|
9
12
|
const encode = (data: Buffer | null) => data?.toString("base64") ?? null;
|
|
10
13
|
const decode = (data: string | null) => data === null ? null : Buffer.from(data, "base64");
|
|
11
14
|
|
|
@@ -24,16 +27,50 @@ export class Deliveries {
|
|
|
24
27
|
const last = this.get(name);
|
|
25
28
|
if (last && !last.done) throw new Error("Finish the pending delivery before changing this review. Retry the same destination.");
|
|
26
29
|
}
|
|
27
|
-
|
|
30
|
+
apply(name: string, keepCurrent = false, mode?: "workspace" | "commit", message?: string, confirmation?: string) {
|
|
31
|
+
const active = this.app.reconciliation.get(name);
|
|
32
|
+
mode ??= active?.intent?.mode ?? "workspace";
|
|
33
|
+
message ??= active?.intent?.message ?? "";
|
|
34
|
+
if (mode !== "workspace" && mode !== "commit") throw new Error("Choose a task commit or apply without committing.");
|
|
35
|
+
if (mode === "commit" && (!message.trim() || message.length > 5000)) throw new Error("Write a commit message (at most 5,000 characters).");
|
|
36
|
+
const last = this.get(name);
|
|
37
|
+
if (last && !last.done) {
|
|
38
|
+
const delivery = this.finish(name, last);
|
|
39
|
+
if (last.mode !== "branch") return { delivery };
|
|
40
|
+
}
|
|
41
|
+
if (keepCurrent) {
|
|
42
|
+
const task = this.app.tasks.get(name);
|
|
43
|
+
if (!active?.ready || task.accepted !== active.reviewBase || this.app.repo.changedFiles(task.accepted, this.app.tasks.snapshot(task)).length)
|
|
44
|
+
throw new Error("Review the merge suggestion before applying it, or ask the agent to keep the latest wording.");
|
|
45
|
+
}
|
|
46
|
+
if (active) this.app.reconciliation.intent(name, { mode, message });
|
|
47
|
+
if (!keepCurrent && active && (!active.ready || (this.app.review.latestRound(name)?.number ?? 0) <= active.round))
|
|
48
|
+
return { reconciliation: this.app.reconciliation.prepare(name, this.app.reconciliation.get(name)!) };
|
|
49
|
+
try { return { delivery: this.deliver(name, mode, message, keepCurrent, confirmation) }; }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof CommitNeedsReview) return { commitReview: error.review };
|
|
52
|
+
if (!(error instanceof DeliveryConflict)) throw error;
|
|
53
|
+
const reconciliation = this.app.reconciliation.start(name, keepCurrent, { mode, message });
|
|
54
|
+
if (reconciliation) return { reconciliation };
|
|
55
|
+
return { delivery: this.deliver(name, mode, message, keepCurrent, confirmation) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
deliver(name: string, mode: Delivery["mode"], message = "", keepCurrent = false, confirmation?: string) {
|
|
28
59
|
const app = this.app, task = app.tasks.get(name), last = this.get(name);
|
|
29
60
|
if (!task.deferred) throw new Error("This task already applies accepted changes to the manuscript.");
|
|
30
|
-
if (mode !== "workspace" && mode !== "branch") throw new Error("Choose a working directory or task branch.");
|
|
31
|
-
|
|
32
|
-
if (
|
|
61
|
+
if (mode !== "workspace" && mode !== "commit" && mode !== "branch") throw new Error("Choose a working directory or task branch.");
|
|
62
|
+
const reconciliation = app.reconciliation.get(name);
|
|
63
|
+
if (reconciliation && (!reconciliation.ready || mode === "branch" || (!keepCurrent && (app.review.latestRound(name)?.number ?? 0) <= reconciliation.round)))
|
|
64
|
+
throw new Error("Review the merge suggestion before applying it to the manuscript.");
|
|
65
|
+
// Older task-branch commits remain available, but can now be applied
|
|
66
|
+
// through the same manuscript action. Never move or merge those branches.
|
|
67
|
+
if (last && last.mode !== mode && (!last.done || mode === "branch"))
|
|
68
|
+
throw new Error("Finish the pending delivery before applying to the manuscript.");
|
|
69
|
+
if (last && last.mode === mode && (!last.done || (!reconciliation && last.accepted === task.accepted))) return this.finish(name, last);
|
|
33
70
|
if (app.publisher.busy(name) || app.review.counts(name).pending) throw new Error("Finish reviewing every change before choosing a destination.");
|
|
34
|
-
const before =
|
|
71
|
+
const before = reconciliation?.base ?? (last && last.mode !== "branch" ? last.accepted : task.base);
|
|
35
72
|
const files = app.repo.changedFiles(before, task.accepted);
|
|
36
|
-
if (!files.length) throw new Error("This task has no accepted changes left to deliver.");
|
|
73
|
+
if (!files.length && !reconciliation) throw new Error("This task has no accepted changes left to deliver.");
|
|
37
74
|
const base = app.repo.blobs(before, files), accepted = app.repo.blobs(task.accepted, files);
|
|
38
75
|
const data: Delivery = { mode, accepted: task.accepted, seq: app.store.one<{ seq: number }>("SELECT MAX(seq) AS seq FROM actions WHERE task=?", name)!.seq, done: false, writes: [] };
|
|
39
76
|
const contents: Record<string, Buffer | null> = {};
|
|
@@ -55,14 +92,16 @@ export class Deliveries {
|
|
|
55
92
|
}
|
|
56
93
|
for (const file of files) {
|
|
57
94
|
const [merged, conflict] = merge3(current[file], base[file], accepted[file]);
|
|
58
|
-
if (conflict) throw new
|
|
95
|
+
if (conflict) throw new DeliveryConflict(`Cannot deliver ${file}: it conflicts with the destination. Nothing was written. Revise the task and try again.`);
|
|
59
96
|
contents[file] = merged;
|
|
60
|
-
if (mode
|
|
97
|
+
if (mode !== "branch" && !same(current[file], merged)) data.writes.push({ file, before: encode(current[file]), after: encode(merged) });
|
|
61
98
|
}
|
|
62
99
|
if (mode === "branch") {
|
|
63
100
|
const tree = app.repo.treeWith(data.parent!, contents);
|
|
64
101
|
data.commit = app.repo.text(["commit-tree", tree, "-p", data.parent!, "-m", message.trim()]);
|
|
65
102
|
}
|
|
103
|
+
if (mode === "commit" && (!message.trim() || message.length > 5000)) throw new Error("Write a commit message (at most 5,000 characters).");
|
|
104
|
+
if (mode === "commit") planCurrentCommit(app.repo, data, before, files, message, confirmation);
|
|
66
105
|
this.save(name, data);
|
|
67
106
|
return this.finish(name, data);
|
|
68
107
|
}
|
|
@@ -75,7 +114,7 @@ export class Deliveries {
|
|
|
75
114
|
throw new Error("Finish or abort the merge before applying changes.");
|
|
76
115
|
}
|
|
77
116
|
finish(name: string, data: Delivery) {
|
|
78
|
-
if (data.done) return this.result(data);
|
|
117
|
+
if (data.done) { if (data.mode !== "branch") this.app.reconciliation.clear(name); return this.result(data); }
|
|
79
118
|
const app = this.app;
|
|
80
119
|
if (data.mode === "branch") {
|
|
81
120
|
const ref = `refs/heads/${data.branch}`;
|
|
@@ -87,14 +126,19 @@ export class Deliveries {
|
|
|
87
126
|
} else {
|
|
88
127
|
this.assertNoMerge();
|
|
89
128
|
const writes = data.writes.map(write => ({ ...write, target: safePath(app.root, write.file) }));
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
129
|
+
const validate = () => {
|
|
130
|
+
for (const write of writes) {
|
|
131
|
+
const current = readBytes(write.target);
|
|
132
|
+
if (!same(current, decode(write.before)) && !same(current, decode(write.after)))
|
|
133
|
+
throw new Error(`Cannot finish delivery: ${write.file} changed. Restore it to its state before delivery and retry.`);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const applyFiles = () => { for (const write of writes) writeOrDelete(write.target, decode(write.after)); };
|
|
137
|
+
if (data.mode === "commit") finishCurrentCommit(app.repo, data, validate, applyFiles);
|
|
138
|
+
else { validate(); applyFiles(); }
|
|
96
139
|
}
|
|
97
140
|
this.save(name, { ...data, done: true, writes: [] });
|
|
141
|
+
if (data.mode !== "branch") this.app.reconciliation.clear(name);
|
|
98
142
|
return this.result(data);
|
|
99
143
|
}
|
|
100
144
|
result(data: Delivery) {
|
|
@@ -103,7 +147,9 @@ export class Deliveries {
|
|
|
103
147
|
}
|
|
104
148
|
recover() {
|
|
105
149
|
for (const row of this.app.store.all<{ task: string; data: Delivery }>("SELECT * FROM deliveries")) {
|
|
106
|
-
if (
|
|
150
|
+
if (row.data.done && row.data.mode !== "branch" && this.app.reconciliation.get(row.task) && this.app.tasks.get(row.task).accepted === row.data.accepted)
|
|
151
|
+
this.app.reconciliation.clear(row.task);
|
|
152
|
+
else if (!row.data.done) try { this.finish(row.task, row.data); } catch { /* Retain the journal for an explicit retry. */ }
|
|
107
153
|
}
|
|
108
154
|
}
|
|
109
155
|
}
|