@maudecode/talaria-web 1.11.0 → 1.13.0
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/LICENSE +22 -0
- package/_release.json +4 -4
- package/dist/api/chat-router.d.ts +68 -4
- package/dist/api/chat-router.js +11 -8
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/router.d.ts +10 -0
- package/dist/api/router.js +2 -1
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +407 -51
- package/dist/api/sessions-router.js +2 -2
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/sse-routes.js +52 -8
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-router.d.ts +443 -0
- package/dist/api/tools-router.js +99 -4
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +926 -55
- package/dist/auth/oidc.js +2 -0
- package/dist/auth/oidc.js.map +1 -1
- package/dist/auth/passkeys.js +1 -1
- package/dist/auth/passkeys.js.map +1 -1
- package/dist/auth/store.d.ts +17 -0
- package/dist/auth/store.js +77 -19
- package/dist/auth/store.js.map +1 -1
- package/dist/cli/ctl.d.ts +6 -3
- package/dist/cli/ctl.js +25 -16
- package/dist/cli/ctl.js.map +1 -1
- package/dist/cli/launcher.js +2 -2
- package/dist/cli/launcher.js.map +1 -1
- package/dist/fs/atomic.d.ts +8 -1
- package/dist/fs/atomic.js +52 -3
- package/dist/fs/atomic.js.map +1 -1
- package/dist/http/context.d.ts +2 -0
- package/dist/http/context.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/redact.d.ts +15 -0
- package/dist/redact.js +1393 -9
- package/dist/redact.js.map +1 -1
- package/dist/release.js +3 -1
- package/dist/release.js.map +1 -1
- package/dist/runtime.js +20 -9
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +12 -6
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +2 -0
- package/dist/sessions/anchor.js +22 -17
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/export.js +1 -1
- package/dist/sessions/export.js.map +1 -1
- package/dist/sessions/file-changes.d.ts +15 -0
- package/dist/sessions/file-changes.js +58 -0
- package/dist/sessions/file-changes.js.map +1 -0
- package/dist/sessions/journal.d.ts +6 -1
- package/dist/sessions/journal.js +9 -15
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +21 -1
- package/dist/sessions/list.js +81 -17
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +66 -2
- package/dist/sessions/merge.js +291 -9
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/pending.d.ts +11 -2
- package/dist/sessions/pending.js +52 -3
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/relay.js +1 -1
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +16 -3
- package/dist/sessions/service.js +92 -54
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/state-db.d.ts +5 -0
- package/dist/sessions/state-db.js +34 -0
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/streams.d.ts +4 -1
- package/dist/sessions/streams.js +3 -0
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +21 -0
- package/dist/sessions/tool-display.js +113 -0
- package/dist/sessions/tool-display.js.map +1 -0
- package/dist/sessions/turn.d.ts +25 -7
- package/dist/sessions/turn.js +153 -90
- package/dist/sessions/turn.js.map +1 -1
- package/dist/spa.d.ts +2 -0
- package/dist/spa.js +9 -0
- package/dist/spa.js.map +1 -1
- package/dist/test/harness.d.ts +2 -0
- package/dist/test/harness.js +1 -1
- package/dist/test/harness.js.map +1 -1
- package/dist/tools/terminal.d.ts +6 -0
- package/dist/tools/terminal.js +24 -3
- package/dist/tools/terminal.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +133 -0
- package/dist/tools/update-notifications.js +407 -0
- package/dist/tools/update-notifications.js.map +1 -0
- package/dist/tools/updates.d.ts +53 -16
- package/dist/tools/updates.js +314 -351
- package/dist/tools/updates.js.map +1 -1
- package/package.json +3 -2
- package/sidecar/scripts/replay_sidecar.py +17 -0
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/home.py +39 -16
- package/sidecar/talaria_sidecar/methods/chat.py +113 -4
- package/static/dist/FILES.txt +116 -116
- package/static/dist/THIRD_PARTY_LICENSES.md +5170 -0
- package/static/dist/assets/AppShell-irdqom6e.js +1 -0
- package/static/dist/assets/ChatPage-ldhqevjn.js +20 -0
- package/static/dist/assets/HubRoute-hq24vhj8.js +2 -0
- package/static/dist/assets/{Markdown-ftyfzds0.js → Markdown-bx4n7qs4.js} +7 -7
- package/static/dist/assets/{RightPanel-febk3dkd.js → RightPanel-ds4majj3.js} +1 -1
- package/static/dist/assets/Select-gpqmt6x1.js +1 -0
- package/static/dist/assets/SessionListPanel-hkvipik4.js +1 -0
- package/static/dist/assets/{SettingsLayout-pdpmhy1o.js → SettingsLayout-oigbg9rl.js} +1 -1
- package/static/dist/assets/States-fn4o2wdf.js +1 -0
- package/static/dist/assets/Toaster-cwnut53k.js +1 -0
- package/static/dist/assets/_app-h6ago6y6.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-erqnfff5.js +1 -0
- package/static/dist/assets/_app.index-cq2fxmda.js +1 -0
- package/static/dist/assets/_app.insights-nzhld9gu.js +1 -0
- package/static/dist/assets/_app.kanban-m5uggety.js +1 -0
- package/static/dist/assets/_app.logs-d2kk9lza.js +1 -0
- package/static/dist/assets/_app.memory-gcmop1e7.js +1 -0
- package/static/dist/assets/_app.profiles-e9b2my7x.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-n1ggcf5e.js → _app.session._sessionId-ciix6itb.js} +1 -1
- package/static/dist/assets/_app.settings-kcjum3c1.js +1 -0
- package/static/dist/assets/_app.settings._section-bi4jjex0.js +1 -0
- package/static/dist/assets/_app.skills-e4e61l23.js +1 -0
- package/static/dist/assets/_app.tasks-f1evdhbl.js +2 -0
- package/static/dist/assets/_app.todos-o7ijtk42.js +1 -0
- package/static/dist/assets/_app.workspaces-o18voh0w.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-i4bgzu83.js → abnfDiagram-VCTEODGH-krt0yaru.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-notdhmpv.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-mx21lwqa.js → architectureDiagram-5GKGNRK7-i54t9ywl.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-o1hhchxv.js → blockDiagram-I7D4REHJ-fevbz4mz.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-jygqtvd8.js → c4Diagram-7LVT6UL2-np8eclmc.js} +1 -1
- package/static/dist/assets/channel-jpk1nas2.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-hjcpubgk.js → chunk-4HAMMTFA-owoi036m.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-eibnex6m.js → chunk-75Z2AOVW-md44zf1n.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-k1tp3qnp.js → chunk-DU6HZSFF-ka3rw8d6.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-lvcgn16f.js → chunk-F27PBJKO-iasnjsky.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-ldlzruv1.js → chunk-GMAD6QVW-ex1up6tz.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-hs1mva9d.js → chunk-GVQU2GXP-d4kmmg9p.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-o7eqx3yb.js → chunk-IMKFNOWR-mwy42t85.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-bp1ofl70.js → chunk-L3NEJ4N5-fv7ry213.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-o8m6mdz5.js → chunk-OSK3NFVY-jg9ir8ac.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-kx83fti2.js → chunk-P2QGCYS3-pe9ms3ua.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-mwl99e9y.js → chunk-POPQ4Y6H-dimsjrn1.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-oriuokxy.js → chunk-PWAF6VOD-m4hv98dy.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-ab5cf00v.js → chunk-SHT3W25Y-gkjgl6qe.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-kqdrpr17.js → chunk-SVP7TREG-bc8qss5l.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-kfbnjabk.js → chunk-TICWLB2K-k9wzw38w.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-hld301gc.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-hld301gc.js +1 -0
- package/static/dist/assets/{copied-dp33szhq.js → copied-d5joolw7.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-mm5sj9s7.js → cynefin-OW5HDTMX-hueklikk.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-nfon5q38.js → cynefinDiagram-5FMLGOSQ-e83o1wr1.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-dzdp9ypb.js → dagre-GXQ25YYZ-ir1tj2ar.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-ckxzqo01.js → diagram-S7CK7UJ4-o5218p2q.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-hw16h1um.js → diagram-UQ7AKVKN-ciad4o1i.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-mu7wu2zg.js → diagram-VSXAHHWV-oharqjbt.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-k9uprwtj.js → diagram-VX7I27RA-wkwy62iv.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-csdjzrg9.js → diagram-Z3DM3KII-bl252i7r.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-n8mbw2jj.js → ebnfDiagram-PWID7BFC-nm0rcly7.js} +1 -1
- package/static/dist/assets/endpoints-d8n28yf3.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fsfe1uj0.js → erDiagram-RLTQ6QDP-m1w0vta2.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-eaqs19mj.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-eli5764n.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-gsba3iq9.js → ganttDiagram-EL5Y4UJY-czxo6iew.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-na86q1pu.js → gitGraph-4MIJSDKK-ip9j1rph.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-g72ew3xz.js → gitGraphDiagram-WWUBYQGX-kalxdole.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-okn18cp1.js → highlighted-body-KPVGNVTW-lgsj6fsk.js} +1 -1
- package/static/dist/assets/index-l08181vr.js +11 -0
- package/static/dist/assets/index-pdtcl20t.css +2 -0
- package/static/dist/assets/{info-A6RAGUB7-i01jeltk.js → info-A6RAGUB7-fv4fbc5m.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-cayiv6fa.js → infoDiagram-27XIBGKW-kinx5jsl.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-g4rf14bv.js → ishikawaDiagram-5VMMS53U-kozvlqdv.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-fusb2424.js → journeyDiagram-3NMN7TZE-lgpouxoe.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-g831pwlr.js → kanban-definition-UXKFOSKX-b320wo7b.js} +1 -1
- package/static/dist/assets/{lazyRouteComponent-ol965zwk.js → lazyRouteComponent-hy8doeye.js} +1 -1
- package/static/dist/assets/{line-md6em6gc.js → line-gxfwyhjq.js} +1 -1
- package/static/dist/assets/{login-kfclsqd2.js → login-llplsuey.js} +1 -1
- package/static/dist/assets/mermaid-HWGCJPDP-f3scrlds.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-mq3uns9m.js → mermaid-parser.core-loxeqsu3.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-oxafrned.js → mindmap-definition-YA3MSWOX-edi534gc.js} +1 -1
- package/static/dist/assets/{onboarding-fo6hnkyv.js → onboarding-n1xhlgym.js} +1 -1
- package/static/dist/assets/{packet-AYTQ26CC-ggxyvbym.js → packet-AYTQ26CC-gmb3yy71.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-nk7xcqgp.js → pegDiagram-XKGWAZYB-fxy5j8cx.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-ito2razh.js → pie-WAS4IAKB-do2pm4s6.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-g02xexar.js → pieDiagram-E7YTZNPT-mgjvb365.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-d4jn4xq5.js → quadrantDiagram-AXDQQJYC-dhok6m1j.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-nebvfdzt.js → radar-RG4KPBEZ-n5giw3vb.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-ny3gebse.js → railroad-74A4TZTK-h6nfs9bj.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-d5gaaud9.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-ihudnbec.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-lgpvhel7.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-m09mmu67.js → railroadDiagram-O6MQD6OU-lfwg92r4.js} +1 -1
- package/static/dist/assets/{refresh-cw-eitd7yva.js → refresh-cw-bg6oc499.js} +1 -1
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-evggpcbk.js → requirementDiagram-BXWQKSXE-c5k5pg07.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-bun89039.js → sankeyDiagram-P5KCCOFB-o9auio4y.js} +1 -1
- package/static/dist/assets/{saved-culx14ii.js → saved-j5wyhbik.js} +1 -1
- package/static/dist/assets/{search-nu6kzq55.js → search-ipa17zlr.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-hhs9f09o.js → sequenceDiagram-WJ2MYXX4-pfgodnmk.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-mj0gcozh.js → settings_section_extensions_title-fcfjf6l8.js} +1 -1
- package/static/dist/assets/share._token-i339gqj4.js +2 -0
- package/static/dist/assets/{sliders-horizontal-b9ljga1o.js → sliders-horizontal-bfrh4rch.js} +1 -1
- package/static/dist/assets/{stateDiagram-D77RDMKH-gi7t1tro.js → stateDiagram-D77RDMKH-m1y2cr3q.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hehvfv55.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-ea5urvph.js → swimlanes-42K2YHIH-hzai6fwo.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-nfk9mtst.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-mq64rm3b.js → timeline-definition-24CTP7MA-gwefy9gi.js} +1 -1
- package/static/dist/assets/todoStore-fhvdsen0.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-cimbtxlz.js → treeView-Q6P3EWNA-7q91p0dn.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-wig5gi1y.js → treemap-WGGIJYW6-occom39a.js} +1 -1
- package/static/dist/assets/{untitled-mbwemvxf.js → untitled-cgw2enra.js} +1 -1
- package/static/dist/assets/{useForm-li4bljlv.js → useForm-jwbfla68.js} +1 -1
- package/static/dist/assets/useLocale-gieo1fn7.js +1 -0
- package/static/dist/assets/{vennDiagram-4TSXK5OY-ez63kh7n.js → vennDiagram-4TSXK5OY-ew4bhldi.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-r7rftz31.js → wardley-WFR3VGLG-njdacy0u.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-mvqg9eqc.js → wardleyDiagram-VM6X3IG4-mkvxx7u9.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eqxdfc91.js → xychartDiagram-S5SC5T6Z-8uriotdw.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/manifest.webmanifest +1 -1
- package/static/dist/sw.js +114 -152
- package/static/dist/assets/AppShell-hikzkcql.js +0 -1
- package/static/dist/assets/ChatPage-ofqn89h9.js +0 -21
- package/static/dist/assets/HubRoute-f2zrs9si.js +0 -2
- package/static/dist/assets/Select-lvx8gihl.js +0 -1
- package/static/dist/assets/SessionListPanel-o70ljmik.js +0 -1
- package/static/dist/assets/States-i46tz8an.js +0 -1
- package/static/dist/assets/Toaster-fklyeeyh.js +0 -1
- package/static/dist/assets/_app-j3rors25.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-c0ey5ec9.js +0 -1
- package/static/dist/assets/_app.index-coy9jrto.js +0 -1
- package/static/dist/assets/_app.insights-ebd2k0i5.js +0 -1
- package/static/dist/assets/_app.kanban-e2p7ypi5.js +0 -1
- package/static/dist/assets/_app.logs-ml21mfyz.js +0 -1
- package/static/dist/assets/_app.memory-lhu5mm47.js +0 -1
- package/static/dist/assets/_app.profiles-egx0fgjo.js +0 -1
- package/static/dist/assets/_app.settings-d0vqtyid.js +0 -1
- package/static/dist/assets/_app.settings._section-cnzj6pli.js +0 -1
- package/static/dist/assets/_app.skills-kkug8obu.js +0 -1
- package/static/dist/assets/_app.tasks-nf285d2o.js +0 -2
- package/static/dist/assets/_app.todos-dxalqp1e.js +0 -1
- package/static/dist/assets/_app.workspaces-bqtduujs.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-o8zqevhv.js +0 -1
- package/static/dist/assets/channel-mw8l793e.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-egajr1wp.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-egajr1wp.js +0 -1
- package/static/dist/assets/endpoints-f6ngsyu5.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-k3zhqcyl.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-bdxes51f.js +0 -1
- package/static/dist/assets/index-ab0wj9ik.css +0 -2
- package/static/dist/assets/index-fd27xooo.js +0 -11
- package/static/dist/assets/mermaid-HWGCJPDP-ejk0d88t.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-gtd38edw.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-o238oax2.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-c4acqp2j.js +0 -1
- package/static/dist/assets/share._token-k4pcnxqu.js +0 -2
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-jtadbol7.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ki7weepw.js +0 -8
- package/static/dist/assets/todoStore-azvfuqbv.js +0 -1
- package/static/dist/assets/trash-osyo895g.js +0 -1
- package/static/dist/assets/useLocale-kdew6auf.js +0 -1
package/dist/tools/updates.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Web: a recognized clean Talaria checkout fast-forwards to the newest completed
|
|
5
5
|
* release set (stable) or `origin/main` (experimental) and stamps `web/_release.json`
|
|
6
6
|
* from immutable git blobs. Direct global npm installs can replace themselves
|
|
7
|
-
* with an exact completed Stable package
|
|
7
|
+
* with an exact completed Stable package or the verified GHCR Experimental artifact, switching
|
|
8
|
+
* channels in either direction; other packaged installs stay manual.
|
|
8
9
|
* Agent: the external checkout follows its own `v*` tags with a
|
|
9
10
|
* stash/pull or force reset, then the gateway restarts through the sidecar.
|
|
10
11
|
* Nothing here deletes git locks; `clearLock` only inventories them.
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
import { readCapped } from '../http/capped.js';
|
|
13
14
|
import { execFile } from 'node:child_process';
|
|
14
15
|
import { createHash } from 'node:crypto';
|
|
15
|
-
import { existsSync, lstatSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync,
|
|
16
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
17
|
import { delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
17
18
|
import { dict } from '../config/agent-config.js';
|
|
18
19
|
import { validateReleaseInfo } from '../release.js';
|
|
@@ -22,9 +23,6 @@ export const REPOSITORY_URL = `https://github.com/${REPOSITORY}`;
|
|
|
22
23
|
export const API_ROOT = `https://api.github.com/repos/${REPOSITORY}`;
|
|
23
24
|
/** Anchored paths work from both the git root and Web's nested working directory. */
|
|
24
25
|
export const WEB_UPDATE_PATHS = [':(top)web/', ':(top)contracts/'];
|
|
25
|
-
/** Relative to the git root: the release stamp and the blobs it is verified against. */
|
|
26
|
-
export const RELEASE_STAMP = 'web/_release.json';
|
|
27
|
-
const RELEASE_BLOBS = ['sidecar/agent_dependency.json', 'contract_versions.json'];
|
|
28
26
|
export const CACHE_TTL_S = 1800;
|
|
29
27
|
export const AUTO_UPDATE_INTERVAL_MS = 5 * 60_000;
|
|
30
28
|
export const RESTART_MAX_WAIT_S = 300;
|
|
@@ -57,15 +55,9 @@ export const runGit = (args, cwd, timeoutMs = 10_000) => new Promise((done) => {
|
|
|
57
55
|
done({ out: err || out || `git exited with status ${String(e.code)}`, ok: false });
|
|
58
56
|
});
|
|
59
57
|
});
|
|
60
|
-
/** The steps a source checkout needs after its files change (README "From a source checkout"), run from `<root>/web`. */
|
|
61
|
-
export const WEB_BUILD_STEPS = [
|
|
62
|
-
['ci', '--workspace', 'packages/contracts', '--workspace', 'packages/server', '--include=dev'],
|
|
63
|
-
['run', 'build', '--workspace', 'packages/contracts'],
|
|
64
|
-
['run', 'build', '--workspace', 'packages/server'],
|
|
65
|
-
];
|
|
66
58
|
export const WEB_BUILD_TIMEOUT_MS = 10 * 60_000;
|
|
67
|
-
/**
|
|
68
|
-
export const
|
|
59
|
+
/** Source checkouts are contributor-only: they update with git, and the updater never mutates them (TAL-379). */
|
|
60
|
+
export const CONTRIBUTOR_CHECKOUT_MESSAGE = 'This is a contributor source checkout. Update it with git and rebuild Web (see web/README.md); only npm installs update automatically.';
|
|
69
61
|
const runNpmCommand = (args, cwd, timeoutMs, env) => new Promise((done) => {
|
|
70
62
|
// Prefer the npm beside the running node so a supervisor started with an absolute node path still finds it.
|
|
71
63
|
const beside = join(dirname(process.execPath), 'npm');
|
|
@@ -87,20 +79,7 @@ const runNpmCommand = (args, cwd, timeoutMs, env) => new Promise((done) => {
|
|
|
87
79
|
done({ out: (stderr.trim() || stdout.trim() || `npm exited with status ${String(e.code)}`).slice(-4000), ok: false });
|
|
88
80
|
});
|
|
89
81
|
});
|
|
90
|
-
export const runNpm = (args, cwd, timeoutMs) => runNpmCommand(args, cwd, timeoutMs, { ...process.env, NODE_ENV: 'development' });
|
|
91
82
|
export const runPackageNpm = (args, cwd, timeoutMs) => runNpmCommand(args, cwd, timeoutMs, process.env);
|
|
92
|
-
/** Install and build the checkout's Web packages; null on success, else the message for the caller. */
|
|
93
|
-
async function buildWeb(root, build) {
|
|
94
|
-
const cwd = join(root, 'web');
|
|
95
|
-
for (const step of WEB_BUILD_STEPS) {
|
|
96
|
-
const result = await build(step, cwd, WEB_BUILD_TIMEOUT_MS);
|
|
97
|
-
if (!result.ok)
|
|
98
|
-
return `\`npm ${step.join(' ')}\` failed: ${result.out || 'unknown error'}`;
|
|
99
|
-
}
|
|
100
|
-
if (!existsSync(join(cwd, WEB_SERVER_ENTRY)))
|
|
101
|
-
return `build finished without producing ${WEB_SERVER_ENTRY}`;
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
83
|
export const isGitLockError = (output) => { const l = output.toLowerCase(); return GIT_LOCK_SIGNATURES.some((s) => l.includes(s)); };
|
|
105
84
|
/** Python `_sanitize_git_diagnostic`: strip URL userinfo, GitHub token shapes, and secret query values. */
|
|
106
85
|
export function sanitizeGitDiagnostic(output, limit = 300) {
|
|
@@ -116,10 +95,14 @@ function fetchFailureMessage(out, network) {
|
|
|
116
95
|
const lower = detail.toLowerCase();
|
|
117
96
|
return NETWORK_FAILURES.some((s) => lower.includes(s)) ? network : `fetch failed: ${detail}`;
|
|
118
97
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
/** One delayed retry absorbs a transient network blip; a lock conflict is not transient. */
|
|
99
|
+
export const FETCH_RETRY_MS = 1000;
|
|
100
|
+
async function fetchWithRetry(git, args, cwd, timeoutMs) {
|
|
101
|
+
const first = await git(args, cwd, timeoutMs);
|
|
102
|
+
if (first.ok || isGitLockError(first.out))
|
|
103
|
+
return first;
|
|
104
|
+
await new Promise((r) => setTimeout(r, FETCH_RETRY_MS));
|
|
105
|
+
return git(args, cwd, timeoutMs);
|
|
123
106
|
}
|
|
124
107
|
function normalizeRemoteUrl(remote) {
|
|
125
108
|
let url = remote.trim();
|
|
@@ -155,7 +138,13 @@ function sortKeys(value) {
|
|
|
155
138
|
}
|
|
156
139
|
const same = (a, b) => canon(a) === canon(b);
|
|
157
140
|
// ── published release sets ───────────────────────────────────────────────────
|
|
141
|
+
/** `transient`: GitHub was unreachable, slow, or overloaded, so a retry can succeed; the rest describe published state. */
|
|
158
142
|
export class ReleaseUnavailable extends Error {
|
|
143
|
+
transient;
|
|
144
|
+
constructor(message, transient = false) {
|
|
145
|
+
super(message);
|
|
146
|
+
this.transient = transient;
|
|
147
|
+
}
|
|
159
148
|
}
|
|
160
149
|
/** GitHub API client: opt-in `TALARIA_RELEASE_TOKEN`, 2 MB cap, and asset redirects only to GitHub's release host with the token stripped. */
|
|
161
150
|
export function githubJson(fetchImpl, env) {
|
|
@@ -164,17 +153,25 @@ export function githubJson(fetchImpl, env) {
|
|
|
164
153
|
const token = (env.TALARIA_RELEASE_TOKEN ?? '').trim();
|
|
165
154
|
if (token)
|
|
166
155
|
headers.Authorization = `Bearer ${token}`;
|
|
167
|
-
|
|
156
|
+
const send = async (url, init) => {
|
|
157
|
+
try {
|
|
158
|
+
return await fetchImpl(url, init);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw new ReleaseUnavailable(`GitHub is unreachable: ${error.message}`, true);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
let res = await send(API_ROOT + path, { headers, redirect: 'manual', signal: AbortSignal.timeout(5000) });
|
|
168
165
|
if (res.status >= 300 && res.status < 400) {
|
|
169
166
|
const target = new URL(res.headers.get('location') ?? '', API_ROOT + path);
|
|
170
167
|
if (target.protocol !== 'https:' || target.hostname !== 'release-assets.githubusercontent.com')
|
|
171
168
|
throw new ReleaseUnavailable('Unexpected release download redirect');
|
|
172
169
|
const anonymous = { ...headers };
|
|
173
170
|
delete anonymous.Authorization;
|
|
174
|
-
res = await
|
|
171
|
+
res = await send(target.toString(), { headers: anonymous, signal: AbortSignal.timeout(5000) });
|
|
175
172
|
}
|
|
176
173
|
if (!res.ok)
|
|
177
|
-
throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}
|
|
174
|
+
throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}`, res.status >= 500 || res.status === 429);
|
|
178
175
|
const body = await readCapped(res, 2_000_000);
|
|
179
176
|
if (!body)
|
|
180
177
|
throw new ReleaseUnavailable('Release metadata exceeds the download limit');
|
|
@@ -187,7 +184,7 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
|
|
|
187
184
|
const deadline = now() + 15_000;
|
|
188
185
|
const fetchJson = (path, asset = false) => {
|
|
189
186
|
if (now() >= deadline)
|
|
190
|
-
throw new ReleaseUnavailable('Release lookup exceeded its deadline; retry or update manually');
|
|
187
|
+
throw new ReleaseUnavailable('Release lookup exceeded its deadline; retry or update manually', true);
|
|
191
188
|
return getJson(path, { asset });
|
|
192
189
|
};
|
|
193
190
|
const published = [];
|
|
@@ -263,6 +260,81 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
|
|
|
263
260
|
}
|
|
264
261
|
throw new ReleaseUnavailable('No completed Talaria Web release is available on this channel');
|
|
265
262
|
}
|
|
263
|
+
// ── Experimental artifacts (TAL-343) ─────────────────────────────────────────
|
|
264
|
+
const GHCR = 'https://ghcr.io';
|
|
265
|
+
const EXPERIMENTAL_REPOSITORY = 'maudecode/talaria-web-experimental';
|
|
266
|
+
const EXPERIMENTAL_ARTIFACT_TYPE = 'application/vnd.maudecode.talaria-web.experimental.v1';
|
|
267
|
+
const EXPERIMENTAL_LAYER_TYPE = 'application/vnd.maudecode.talaria-web.npm.tgz';
|
|
268
|
+
/** GHCR serves blobs by redirecting here; the pull token never follows. */
|
|
269
|
+
const EXPERIMENTAL_BLOB_HOST = 'pkg-containers.githubusercontent.com';
|
|
270
|
+
export const EXPERIMENTAL_TARBALL_CAP = 200_000_000;
|
|
271
|
+
const EXPERIMENTAL_VERSION = new RegExp(`^${VERSION}-exp\\.([a-f0-9]{12})$`);
|
|
272
|
+
/** Anonymous GHCR client with `githubJson`'s timeouts, caps, and transient classification. */
|
|
273
|
+
export function ghcrExperimental(fetchImpl) {
|
|
274
|
+
const send = async (url, init) => {
|
|
275
|
+
try {
|
|
276
|
+
return await fetchImpl(url, { redirect: 'manual', ...init });
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
throw new ReleaseUnavailable(`GHCR is unreachable: ${error.message}`, true);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const check = (res, what) => {
|
|
283
|
+
if (!res.ok)
|
|
284
|
+
throw new ReleaseUnavailable(`GHCR answered ${String(res.status)} for the Experimental ${what}`, res.status >= 500 || res.status === 429);
|
|
285
|
+
};
|
|
286
|
+
const json = async (res, what) => {
|
|
287
|
+
check(res, what);
|
|
288
|
+
const body = await readCapped(res, 2_000_000);
|
|
289
|
+
if (!body)
|
|
290
|
+
throw new ReleaseUnavailable(`Experimental ${what} exceeds the download limit`);
|
|
291
|
+
try {
|
|
292
|
+
return dict(JSON.parse(body.toString('utf8')));
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
throw new ReleaseUnavailable(`Invalid Experimental ${what}`);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const token = async () => {
|
|
299
|
+
const body = await json(await send(`${GHCR}/token?scope=repository:${EXPERIMENTAL_REPOSITORY}:pull`, { signal: AbortSignal.timeout(5000) }), 'pull token');
|
|
300
|
+
if (typeof body.token !== 'string' || !body.token)
|
|
301
|
+
throw new ReleaseUnavailable('GHCR did not issue an anonymous pull token');
|
|
302
|
+
return body.token;
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
release: async () => {
|
|
306
|
+
const auth = { Authorization: `Bearer ${await token()}` };
|
|
307
|
+
const m = await json(await send(`${GHCR}/v2/${EXPERIMENTAL_REPOSITORY}/manifests/experimental`, { headers: { ...auth, Accept: 'application/vnd.oci.image.manifest.v1+json' }, signal: AbortSignal.timeout(5000) }), 'manifest');
|
|
308
|
+
const layers = Array.isArray(m.layers) ? m.layers.map(dict) : [];
|
|
309
|
+
const layer = layers[0];
|
|
310
|
+
if (m.artifactType !== EXPERIMENTAL_ARTIFACT_TYPE || layers.length !== 1 || layer?.mediaType !== EXPERIMENTAL_LAYER_TYPE)
|
|
311
|
+
throw new ReleaseUnavailable('The Experimental artifact has an unexpected format');
|
|
312
|
+
const annotations = dict(m.annotations);
|
|
313
|
+
const sourceRevision = annotations['org.opencontainers.image.revision'];
|
|
314
|
+
const version = annotations['org.opencontainers.image.version'];
|
|
315
|
+
const size = layer.size;
|
|
316
|
+
if (typeof sourceRevision !== 'string' || !SHA.test(sourceRevision) || typeof version !== 'string' || EXPERIMENTAL_VERSION.exec(version)?.[4] !== sourceRevision.slice(0, 12)
|
|
317
|
+
|| typeof layer.digest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(layer.digest) || typeof size !== 'number' || !Number.isInteger(size) || size < 1 || size > EXPERIMENTAL_TARBALL_CAP) {
|
|
318
|
+
throw new ReleaseUnavailable('The Experimental artifact has malformed provenance');
|
|
319
|
+
}
|
|
320
|
+
return { tag: `web-exp-v${version}`, version, sourceRevision, digest: layer.digest, size };
|
|
321
|
+
},
|
|
322
|
+
download: async (release) => {
|
|
323
|
+
let res = await send(`${GHCR}/v2/${EXPERIMENTAL_REPOSITORY}/blobs/${release.digest}`, { headers: { Authorization: `Bearer ${await token()}` }, signal: AbortSignal.timeout(120_000) });
|
|
324
|
+
if (res.status >= 300 && res.status < 400) {
|
|
325
|
+
const target = new URL(res.headers.get('location') ?? '', GHCR);
|
|
326
|
+
if (target.protocol !== 'https:' || target.hostname !== EXPERIMENTAL_BLOB_HOST)
|
|
327
|
+
throw new ReleaseUnavailable('Unexpected Experimental download redirect');
|
|
328
|
+
res = await send(target.toString(), { redirect: 'error', signal: AbortSignal.timeout(120_000) });
|
|
329
|
+
}
|
|
330
|
+
check(res, 'download');
|
|
331
|
+
const body = await readCapped(res, release.size);
|
|
332
|
+
if (body?.length !== release.size || `sha256:${createHash('sha256').update(body).digest('hex')}` !== release.digest)
|
|
333
|
+
throw new ReleaseUnavailable('The Experimental download does not match its digest');
|
|
334
|
+
return body;
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
266
338
|
// ── Web checkout ─────────────────────────────────────────────────────────────
|
|
267
339
|
/** The git root when `webRoot` is `<root>/web` of a Talaria checkout with a GitHub `maudecode/talaria` origin; else null. */
|
|
268
340
|
export async function checkoutRoot(webRoot, git) {
|
|
@@ -305,7 +377,7 @@ export async function npmInstallInfo(webRoot, npm = runPackageNpm, container = e
|
|
|
305
377
|
catch {
|
|
306
378
|
return null;
|
|
307
379
|
}
|
|
308
|
-
if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !new RegExp(`^${VERSION}$`).test(metadata.version))
|
|
380
|
+
if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !(new RegExp(`^${VERSION}$`).test(metadata.version) || EXPERIMENTAL_VERSION.test(metadata.version)))
|
|
309
381
|
return null;
|
|
310
382
|
const global = await npm(['root', '--global'], webRoot, 10_000);
|
|
311
383
|
if (!global.ok || !isAbsolute(global.out))
|
|
@@ -336,141 +408,54 @@ function diskRelease(webRoot) {
|
|
|
336
408
|
return null;
|
|
337
409
|
}
|
|
338
410
|
}
|
|
339
|
-
/**
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
for (const name of RELEASE_BLOBS) {
|
|
343
|
-
const shown = await git(['show', `${release.sourceRevision}:web/${name}`], root);
|
|
344
|
-
if (!shown.ok)
|
|
345
|
-
throw new Error('missing release metadata');
|
|
346
|
-
files[name] = JSON.parse(shown.out);
|
|
347
|
-
}
|
|
348
|
-
const pin = dict(files['sidecar/agent_dependency.json']);
|
|
349
|
-
const versions = dict(files['contract_versions.json']);
|
|
350
|
-
const expected = {
|
|
351
|
-
tag: release.tag,
|
|
352
|
-
version: release.version,
|
|
353
|
-
sourceRevision: release.sourceRevision,
|
|
354
|
-
releaseSet: release.sourceRevision,
|
|
355
|
-
contracts: { appWeb: [dict(versions.appWeb).fixtureVersion], webRelay: [dict(versions.webRelay).protocolVersion] },
|
|
356
|
-
compatibleAgent: { ...dict(pin['x-talaria']), image: dict(dict(pin.services)['hermes-agent']).image },
|
|
357
|
-
};
|
|
358
|
-
if (!same(expected, release.runtime))
|
|
359
|
-
throw new Error('release metadata differs from source');
|
|
360
|
-
return expected;
|
|
361
|
-
}
|
|
362
|
-
const stampPath = (root) => join(root, RELEASE_STAMP);
|
|
363
|
-
const readStamp = (path) => {
|
|
364
|
-
if (!existsSync(path) && !isSymlink(path))
|
|
365
|
-
return null;
|
|
366
|
-
if (isSymlink(path))
|
|
367
|
-
throw new Error('local release stamp is a symbolic link');
|
|
368
|
-
return readFileSync(path, 'utf8');
|
|
369
|
-
};
|
|
370
|
-
const isSymlink = (path) => { try {
|
|
371
|
-
return lstatSync(path).isSymbolicLink();
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
return false;
|
|
375
|
-
} };
|
|
376
|
-
/** Python `_verified_release_stamp`: `[expected, installed]`; throws on a modified stamp. */
|
|
377
|
-
async function verifiedReleaseStamp(root, release, git, id) {
|
|
378
|
-
const expected = await verifyReleaseSource(root, release, git);
|
|
379
|
-
const raw = readStamp(stampPath(root));
|
|
380
|
-
const installed = raw === null ? null : JSON.parse(raw);
|
|
381
|
-
if (installed !== null && ![id.release(), id.stamped(), expected].some((c) => same(c, installed)))
|
|
382
|
-
throw new Error('local release stamp was modified');
|
|
383
|
-
return [expected, installed];
|
|
384
|
-
}
|
|
385
|
-
/** Python `_main_stamp`: the unchanged generated stamp bytes (to discard when leaving a release), else null. */
|
|
386
|
-
function mainStamp(root, id) {
|
|
387
|
-
const data = readStamp(stampPath(root));
|
|
388
|
-
if (data !== null && (!id.stamped().tag || !same(JSON.parse(data), id.stamped())))
|
|
389
|
-
throw new Error('local release stamp was modified');
|
|
390
|
-
return data;
|
|
391
|
-
}
|
|
392
|
-
async function mainRevision(root, git) {
|
|
393
|
-
const fetched = await git(['fetch', '--no-tags', 'origin', 'refs/heads/main:refs/remotes/origin/main'], root, 30_000);
|
|
394
|
-
if (!fetched.ok)
|
|
395
|
-
return [null, fetched.out];
|
|
396
|
-
const source = await git(['rev-parse', 'refs/remotes/origin/main^{commit}'], root);
|
|
397
|
-
return source.ok && SHA.test(source.out) ? [source.out, ''] : [null, ''];
|
|
398
|
-
}
|
|
399
|
-
async function mainPathsDiffer(root, before, after, git) {
|
|
400
|
-
const files = await git(['diff', '--no-renames', '--name-only', before, after, '--', ...WEB_UPDATE_PATHS], root);
|
|
401
|
-
return files.ok ? Boolean(files.out) : null;
|
|
402
|
-
}
|
|
403
|
-
async function mainChangeCount(root, before, after, git) {
|
|
404
|
-
const changed = await mainPathsDiffer(root, before, after, git);
|
|
405
|
-
if (changed === null)
|
|
406
|
-
return null;
|
|
407
|
-
if (!changed)
|
|
408
|
-
return 0;
|
|
409
|
-
const count = await git(['rev-list', '--count', '--full-history', `${before}..${after}`, '--', ...WEB_UPDATE_PATHS], root);
|
|
410
|
-
const n = Number.parseInt(count.out, 10);
|
|
411
|
-
return count.ok && /^\d+$/.test(count.out) && n > 0 ? n : null;
|
|
412
|
-
}
|
|
413
|
-
async function mainRestartPending(root, head, git, id) {
|
|
414
|
-
const running = id.runningSourceRevision();
|
|
415
|
-
if (running === head)
|
|
416
|
-
return false;
|
|
417
|
-
if (!running)
|
|
418
|
-
return true;
|
|
419
|
-
return (await mainPathsDiffer(root, running, head, git)) !== false;
|
|
411
|
+
/** The channel an npm install came from: its release tag prefix (`web-exp-v` or `web-v`). */
|
|
412
|
+
function npmInstallChannel(installed, id) {
|
|
413
|
+
return str(diskRelease(installed.packageRoot)?.tag ?? id.release().tag).startsWith('web-exp-v') ? 'experimental' : 'stable';
|
|
420
414
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return { ...result, manual_update: true, message: 'Main updates require an authenticated Talaria source checkout with Web under web/.' };
|
|
425
|
-
const [source, error] = await mainRevision(root, git);
|
|
426
|
-
if (source === null)
|
|
427
|
-
return { ...result, error: gitFailure(error, 'Could not fetch origin/main; check Git read access.').message };
|
|
428
|
-
Object.assign(result, { latest_sha: source, latest_version: `main@${source.slice(0, 12)}` });
|
|
429
|
-
const head = await git(['rev-parse', 'HEAD'], root);
|
|
430
|
-
const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
|
|
431
|
-
if (!head.ok || !SHA.test(head.out) || !status.ok)
|
|
432
|
-
return { ...result, manual_update: true, error: 'Could not verify the source checkout' };
|
|
433
|
-
Object.assign(result, { installed_sha: head.out, dirty: Boolean(status.out) });
|
|
434
|
-
const base = await git(['merge-base', head.out, source], root);
|
|
435
|
-
result.current_sha = base.ok && SHA.test(base.out) ? base.out : null;
|
|
436
|
-
if (result.current_sha)
|
|
437
|
-
result.compare_url = `${REPOSITORY_URL}/compare/${base.out}...${source}`;
|
|
438
|
-
if (!base.ok || base.out !== head.out)
|
|
439
|
-
return { ...result, manual_update: true, message: 'This checkout is ahead of or diverged from origin/main; reconcile it manually.' };
|
|
440
|
-
const count = await mainChangeCount(root, head.out, source, git);
|
|
441
|
-
if (count === null)
|
|
442
|
-
return { ...result, error: 'Could not compare the source checkout with origin/main' };
|
|
415
|
+
/** An npm install on Experimental: behind when its source differs from the `experimental` tag, or when switching channels. */
|
|
416
|
+
async function checkNpmExperimental(result, installed, id, registry) {
|
|
417
|
+
let release;
|
|
443
418
|
try {
|
|
444
|
-
|
|
419
|
+
release = await registry.release();
|
|
445
420
|
}
|
|
446
|
-
catch {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
421
|
+
catch (error) {
|
|
422
|
+
const unavailable = error instanceof ReleaseUnavailable ? error : new ReleaseUnavailable('The Experimental artifact is unavailable');
|
|
423
|
+
return { ...result, install_kind: 'npm', manual_update: true, error: unavailable.message, ...(unavailable.transient ? { stale_check: true } : {}) };
|
|
424
|
+
}
|
|
425
|
+
Object.assign(result, { latest_version: release.tag, latest_sha: release.sourceRevision, branch: 'experimental', release_based: true, no_git: true, install_kind: 'npm', manual_update: false });
|
|
426
|
+
const disk = diskRelease(installed.packageRoot);
|
|
427
|
+
const current = str(disk?.sourceRevision ?? id.release().sourceRevision) || null;
|
|
428
|
+
if (npmInstallChannel(installed, id) !== 'experimental')
|
|
429
|
+
return { ...result, current_sha: current, behind: 1, channel_switch: true, message: `Switching to Experimental installs ${release.version}.` };
|
|
430
|
+
if (disk?.sourceRevision !== release.sourceRevision)
|
|
431
|
+
return { ...result, current_sha: current, behind: 1, message: `Experimental update ${release.version} is available.` };
|
|
432
|
+
const metadataRepair = !same(id.release(), disk);
|
|
433
|
+
return { ...result, current_sha: release.sourceRevision, behind: 0, metadata_repair: metadataRepair, ...(metadataRepair ? { message: 'The npm package is current; finish the update to restart with that version.' } : {}) };
|
|
455
434
|
}
|
|
456
|
-
/** Python `check_web_update
|
|
457
|
-
export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm) {
|
|
435
|
+
/** Python `check_web_update`: npm and packaged installs; a source checkout is contributor-only (TAL-379). */
|
|
436
|
+
export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm, registry) {
|
|
458
437
|
const root = await checkoutRoot(webRoot, git);
|
|
459
438
|
const npmInstall = root === null ? await npmInstallInfo(webRoot, npmRun) : null;
|
|
460
439
|
const result = { name: 'webui', channel, repo_url: REPOSITORY_URL, current_version: currentVersion, behind: null, no_git: root === null };
|
|
461
|
-
if (
|
|
462
|
-
return
|
|
440
|
+
if (root !== null)
|
|
441
|
+
return { ...result, manual_update: true, message: CONTRIBUTOR_CHECKOUT_MESSAGE };
|
|
442
|
+
if (channel === 'experimental' && npmInstall && registry)
|
|
443
|
+
return checkNpmExperimental(result, npmInstall, id, registry);
|
|
463
444
|
let release;
|
|
464
445
|
try {
|
|
465
446
|
release = await publishedWebRelease(channel, getJson);
|
|
466
447
|
}
|
|
467
448
|
catch (error) {
|
|
468
449
|
if (error instanceof ReleaseUnavailable)
|
|
469
|
-
return { ...result, manual_update: true, error: error.message };
|
|
450
|
+
return { ...result, manual_update: true, error: error.message, ...(error.transient ? { stale_check: true } : {}) };
|
|
470
451
|
return { ...result, manual_update: true, error: 'Talaria release metadata is unavailable. Private repositories require TALARIA_RELEASE_TOKEN with Contents read access.' };
|
|
471
452
|
}
|
|
472
453
|
Object.assign(result, { latest_version: release.tag, latest_sha: release.sourceRevision, branch: release.tag, release_based: true, release_url: release.release_url, image: release.image });
|
|
473
|
-
|
|
454
|
+
{
|
|
455
|
+
if (npmInstall && channel === 'stable' && release.npm && npmInstallChannel(npmInstall, id) === 'experimental') {
|
|
456
|
+
// A channel switch installs the newest Stable package regardless of version order (TAL-343).
|
|
457
|
+
return { ...result, current_sha: id.release().sourceRevision ?? null, behind: 1, no_git: true, install_kind: 'npm', npm: release.npm, manual_update: false, channel_switch: true, message: `Switching to Stable installs npm release ${release.version}.` };
|
|
458
|
+
}
|
|
474
459
|
if (npmInstall && channel === 'stable' && release.npm && !str(id.release().tag).startsWith('web-exp-v')) {
|
|
475
460
|
const comparison = compareVersions(npmInstall.version, release.version);
|
|
476
461
|
if (comparison > 0)
|
|
@@ -495,75 +480,83 @@ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJ
|
|
|
495
480
|
}
|
|
496
481
|
return { ...result, current_sha: current, behind, no_git: true, manual_update: true, message: 'Use the published Talaria Web image or authenticated monorepo installation; legacy checkouts require migration.' };
|
|
497
482
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
base = current;
|
|
518
|
-
knownBase = true;
|
|
483
|
+
}
|
|
484
|
+
export const CHANNEL_SWITCH_BACKUPS = 5;
|
|
485
|
+
/** Web's persisted stores (`settings.json`, the project store, the workspace store) as seen from the state dir. */
|
|
486
|
+
const PERSISTED_STORES = ['settings.json', 'projects.json', 'workspaces.json', 'last_workspace.txt'];
|
|
487
|
+
/** Copy the persisted stores to `<stateDir>/backups/channel-switch-<UTC timestamp>/`, keeping the newest few; throws on failure. */
|
|
488
|
+
export function backupPersistedStores(stateDir, now = new Date(), keep = CHANNEL_SWITCH_BACKUPS) {
|
|
489
|
+
const root = join(stateDir, 'backups');
|
|
490
|
+
mkdirSync(root, { recursive: true });
|
|
491
|
+
const name = `channel-switch-${now.toISOString().replace(/[:.]/g, '-')}`;
|
|
492
|
+
const target = join(root, name);
|
|
493
|
+
if (existsSync(target))
|
|
494
|
+
throw new Error(`${name} already exists`);
|
|
495
|
+
// Only a complete copy is published under the name that counts toward the retained backups.
|
|
496
|
+
const partial = mkdtempSync(join(root, `.${name}-`));
|
|
497
|
+
try {
|
|
498
|
+
for (const store of PERSISTED_STORES)
|
|
499
|
+
if (existsSync(join(stateDir, store)))
|
|
500
|
+
copyFileSync(join(stateDir, store), join(partial, store));
|
|
501
|
+
renameSync(partial, target);
|
|
519
502
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (knownBase && base !== current)
|
|
529
|
-
Object.assign(result, { manual_update: true, message: 'Reconcile divergent source history before updating Web.' });
|
|
530
|
-
}
|
|
531
|
-
// Local-only commits cannot appear in a GitHub comparison; omit unresolvable links.
|
|
532
|
-
result.current_sha = knownBase && SHA.test(base) ? base : null;
|
|
533
|
-
if (result.current_sha)
|
|
534
|
-
result.compare_url = `${REPOSITORY_URL}/compare/${base}...${release.sourceRevision}`;
|
|
535
|
-
if (status.out)
|
|
536
|
-
Object.assign(result, { manual_update: true, message: 'Commit or remove local changes before updating; Web updates never discard them.' });
|
|
537
|
-
return result;
|
|
503
|
+
catch (error) {
|
|
504
|
+
rmSync(partial, { recursive: true, force: true });
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
const backups = readdirSync(root).filter((name) => name.startsWith('channel-switch-')).sort();
|
|
508
|
+
for (const old of backups.slice(0, Math.max(0, backups.length - keep)))
|
|
509
|
+
rmSync(join(root, old), { recursive: true, force: true });
|
|
510
|
+
return target;
|
|
538
511
|
}
|
|
539
|
-
async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply) {
|
|
540
|
-
if (channel !== 'stable')
|
|
541
|
-
return { ok: false, manual_update: true, message: 'Packaged Experimental updates require manual artifact replacement.' };
|
|
542
|
-
if (str(id.release().tag).startsWith('web-exp-v'))
|
|
543
|
-
return { ok: false, manual_update: true, message: 'Switch packaged update channels manually before enabling Stable updates.' };
|
|
512
|
+
async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged) {
|
|
544
513
|
const installed = await npmInstallInfo(webRoot, npmRun);
|
|
545
514
|
if (!installed)
|
|
546
515
|
return { ok: false, manual_update: true, message: 'Automatic npm updates require a direct global @maudecode/talaria-web installation.' };
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
516
|
+
// Switching channels installs the selected channel's newest artifact regardless of version order (TAL-343).
|
|
517
|
+
const switching = npmInstallChannel(installed, id) !== channel;
|
|
518
|
+
const onDisk = diskRelease(installed.packageRoot);
|
|
519
|
+
let target;
|
|
520
|
+
if (channel === 'stable') {
|
|
521
|
+
let release;
|
|
522
|
+
try {
|
|
523
|
+
release = await publishedWebRelease('stable', getJson);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
|
|
527
|
+
}
|
|
528
|
+
if (!release.npm)
|
|
529
|
+
return { ok: false, manual_update: true, message: 'The completed Stable release does not include an npm package identity.' };
|
|
530
|
+
target = { version: release.version, sourceRevision: release.sourceRevision, label: `npm release ${release.version}`, matches: (r) => same(r, release.runtime), npm: release.npm };
|
|
531
|
+
if (!switching) {
|
|
532
|
+
if (!canApply())
|
|
533
|
+
return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
|
|
534
|
+
const comparison = compareVersions(installed.version, release.version);
|
|
535
|
+
if (comparison > 0)
|
|
536
|
+
return { ok: false, manual_update: true, message: 'This npm installation is ahead of the selected Stable release.' };
|
|
537
|
+
if (comparison === 0 && !target.matches(onDisk))
|
|
538
|
+
return { ok: false, manual_update: true, message: 'Installed npm release metadata does not match the completed release.' };
|
|
539
|
+
}
|
|
550
540
|
}
|
|
551
|
-
|
|
552
|
-
|
|
541
|
+
else {
|
|
542
|
+
if (!packaged.registry)
|
|
543
|
+
return { ok: false, manual_update: true, message: 'Packaged Experimental updates are unavailable in this installation.' };
|
|
544
|
+
let release;
|
|
545
|
+
try {
|
|
546
|
+
release = await packaged.registry.release();
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
return { ok: false, message: error instanceof ReleaseUnavailable ? error.message : 'The Experimental artifact is unavailable.' };
|
|
550
|
+
}
|
|
551
|
+
const identity = { tag: release.tag, version: release.version, sourceRevision: release.sourceRevision, releaseSet: release.sourceRevision };
|
|
552
|
+
// validateReleaseInfo (in diskRelease) already applies Stable's contract and Agent-pin checks to the packaged files.
|
|
553
|
+
target = { version: release.version, sourceRevision: release.sourceRevision, label: `Experimental ${release.version}`, matches: (r) => r !== null && same({ tag: r.tag, version: r.version, sourceRevision: r.sourceRevision, releaseSet: r.releaseSet }, identity), experimental: release };
|
|
553
554
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (comparison > 0)
|
|
560
|
-
return { ok: false, manual_update: true, message: 'This npm installation is ahead of the selected Stable release.' };
|
|
561
|
-
if (comparison === 0) {
|
|
562
|
-
if (!same(diskRelease(installed.packageRoot), release.runtime))
|
|
563
|
-
return { ok: false, manual_update: true, message: 'Installed npm release metadata does not match the completed release.' };
|
|
564
|
-
if (same(id.release(), release.runtime))
|
|
565
|
-
return { ok: true, up_to_date: true, target: 'webui', channel, message: 'Talaria Web already contains the selected npm release.' };
|
|
566
|
-
return { ok: true, target: 'webui', channel, sourceRevision: release.sourceRevision, npm: release.npm, message: `Restarting Talaria Web with npm release ${release.version}.` };
|
|
555
|
+
const current = !switching && target.matches(onDisk);
|
|
556
|
+
if (current) {
|
|
557
|
+
if (same(id.release(), onDisk))
|
|
558
|
+
return { ok: true, up_to_date: true, target: 'webui', channel, message: channel === 'stable' ? 'Talaria Web already contains the selected npm release.' : `Talaria Web already contains ${target.label}.` };
|
|
559
|
+
return { ok: true, target: 'webui', channel, sourceRevision: target.sourceRevision, ...(target.npm ? { npm: target.npm } : {}), message: `Restarting Talaria Web with ${target.label}.` };
|
|
567
560
|
}
|
|
568
561
|
if (!canApply())
|
|
569
562
|
return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
|
|
@@ -573,19 +566,42 @@ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply
|
|
|
573
566
|
const candidate = join(staging, 'lib/node_modules/@maudecode/talaria-web');
|
|
574
567
|
const backup = join(staging, 'previous');
|
|
575
568
|
let keepBackup = false;
|
|
569
|
+
let stores = null;
|
|
576
570
|
try {
|
|
577
|
-
|
|
571
|
+
let spec = target.npm ?? '';
|
|
572
|
+
if (target.experimental) {
|
|
573
|
+
let tarball;
|
|
574
|
+
try {
|
|
575
|
+
tarball = await packaged.registry.download(target.experimental);
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
return { ok: false, verification_failed: true, target: 'webui', channel, message: `${error instanceof ReleaseUnavailable ? error.message : 'The Experimental download failed'}. The installed package was preserved.` };
|
|
579
|
+
}
|
|
580
|
+
spec = join(staging, 'talaria-web-experimental.tgz');
|
|
581
|
+
writeFileSync(spec, tarball);
|
|
582
|
+
}
|
|
583
|
+
const update = await npmRun(['install', '--global', '--prefix', staging, '--no-audit', '--no-fund', spec], staging, WEB_BUILD_TIMEOUT_MS);
|
|
578
584
|
if (!update.ok)
|
|
579
585
|
return { ok: false, install_failed: true, target: 'webui', channel, message: `npm update failed: ${sanitizeGitDiagnostic(update.out, 1000) || 'unknown error'}` };
|
|
580
586
|
const metadata = dict(JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')));
|
|
581
587
|
const bins = dict(metadata.bin);
|
|
582
|
-
if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !==
|
|
588
|
+
if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !== target.version || !target.matches(diskRelease(candidate))
|
|
583
589
|
|| !['talaria-web', 'talaria-web-mcp'].every((name) => str(bins[name]).replace(/^\.\//, '') === `dist/bin/${name}.js` && existsSync(join(candidate, `dist/bin/${name}.js`)))) {
|
|
584
|
-
return { ok: false, verification_failed: true, target: 'webui', channel, message:
|
|
590
|
+
return { ok: false, verification_failed: true, target: 'webui', channel, message: `The downloaded npm package does not match ${channel === 'stable' ? 'the completed release' : 'the Experimental artifact'}. The installed package was preserved.` };
|
|
585
591
|
}
|
|
586
|
-
const
|
|
587
|
-
if (!canApply() ||
|
|
592
|
+
const recheck = await npmInstallInfo(webRoot, npmRun);
|
|
593
|
+
if (!canApply() || recheck?.packageRoot !== installed.packageRoot || recheck.version !== installed.version)
|
|
588
594
|
return { ok: false, message: 'The npm installation or update settings changed; retry the update.' };
|
|
595
|
+
if (switching) {
|
|
596
|
+
try {
|
|
597
|
+
if (!packaged.stateDir)
|
|
598
|
+
throw new Error('the Web state directory is unknown');
|
|
599
|
+
stores = backupPersistedStores(packaged.stateDir);
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
return { ok: false, backup_failed: true, target: 'webui', channel, message: `Channel switch aborted: Web settings could not be backed up (${sanitizeGitDiagnostic(error.message, 300)}). The installed package was preserved.` };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
589
605
|
renameSync(installed.packageRoot, backup);
|
|
590
606
|
try {
|
|
591
607
|
renameSync(candidate, installed.packageRoot);
|
|
@@ -609,128 +625,15 @@ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply
|
|
|
609
625
|
catch { /* a leftover staging directory must not prevent restart after a verified switch */ }
|
|
610
626
|
}
|
|
611
627
|
}
|
|
612
|
-
return { ok: true, target: 'webui', channel, sourceRevision:
|
|
628
|
+
return { ok: true, target: 'webui', channel, sourceRevision: target.sourceRevision, ...(target.npm ? { npm: target.npm } : {}),
|
|
629
|
+
...(switching ? { channel_switch: true, backup_dir: stores } : {}), message: `Updated Talaria Web to ${target.label}.${stores ? ` Settings were backed up to ${stores}.` : ''}` };
|
|
613
630
|
}
|
|
614
|
-
/** Python `apply_web_update`:
|
|
615
|
-
export async function applyWebUpdate(webRoot, channel, git, getJson, id,
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
|
|
620
|
-
if (!status.ok || status.out)
|
|
621
|
-
return { ok: false, dirty: true, message: 'Web update refused: the checkout must be clean, including untracked files.' };
|
|
622
|
-
for (const marker of ['MERGE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD', 'rebase-merge', 'rebase-apply', 'BISECT_LOG']) {
|
|
623
|
-
const path = await git(['rev-parse', '--git-path', marker], root);
|
|
624
|
-
if (!path.ok || existsSync(resolve(root, path.out)))
|
|
625
|
-
return { ok: false, message: 'Finish or abort the repository operation before updating Web.' };
|
|
626
|
-
}
|
|
627
|
-
const headRes = await git(['rev-parse', 'HEAD'], root);
|
|
628
|
-
if (!headRes.ok || !SHA.test(headRes.out))
|
|
629
|
-
return { ok: false, message: 'Could not verify the current source revision' };
|
|
630
|
-
const head = headRes.out;
|
|
631
|
-
const main = channel === 'experimental';
|
|
632
|
-
let source;
|
|
633
|
-
let tag;
|
|
634
|
-
let release = null;
|
|
635
|
-
if (main) {
|
|
636
|
-
const [revision, error] = await mainRevision(root, git);
|
|
637
|
-
if (revision === null)
|
|
638
|
-
return gitFailure(error, 'Could not fetch origin/main; check Git read access.');
|
|
639
|
-
source = revision;
|
|
640
|
-
tag = 'main';
|
|
641
|
-
}
|
|
642
|
-
else {
|
|
643
|
-
try {
|
|
644
|
-
release = await publishedWebRelease(channel, getJson);
|
|
645
|
-
}
|
|
646
|
-
catch {
|
|
647
|
-
return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
|
|
648
|
-
}
|
|
649
|
-
source = release.sourceRevision;
|
|
650
|
-
tag = release.tag;
|
|
651
|
-
}
|
|
652
|
-
if (head !== source && !main) {
|
|
653
|
-
// Fetch only this immutable tag; never force-replace a local tag or pull an unrecorded tip.
|
|
654
|
-
const fetched = await git(['fetch', '--no-tags', 'origin', `refs/tags/${tag}:refs/tags/${tag}`], root, 30_000);
|
|
655
|
-
if (!fetched.ok)
|
|
656
|
-
return gitFailure(fetched.out, 'Could not fetch the published Web tag. Check Git credentials or a conflicting local tag.');
|
|
657
|
-
const resolved = await git(['rev-parse', `refs/tags/${tag}^{commit}`], root);
|
|
658
|
-
if (!resolved.ok || resolved.out !== source)
|
|
659
|
-
return { ok: false, message: 'Published Web tag does not match the immutable release manifest' };
|
|
660
|
-
}
|
|
661
|
-
if (head !== source) {
|
|
662
|
-
const forward = await git(['merge-base', '--is-ancestor', head, source], root);
|
|
663
|
-
if (!forward.ok) {
|
|
664
|
-
const contains = await git(['merge-base', '--is-ancestor', source, head], root);
|
|
665
|
-
if (contains.ok)
|
|
666
|
-
return { ok: false, manual_update: true, target: 'webui', channel, message: 'This checkout is ahead of the selected source. Manage it manually; updates never rewind local work.' };
|
|
667
|
-
return { ok: false, message: 'Web update refused: source histories diverge; reconcile the checkout manually.' };
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (main) {
|
|
671
|
-
const count = await mainChangeCount(root, head, source, git);
|
|
672
|
-
if (count === null)
|
|
673
|
-
return { ok: false, message: 'Could not compare Web and contract changes against origin/main.' };
|
|
674
|
-
if (count === 0)
|
|
675
|
-
source = head; // No checkout mutation for App/Relay-only changes.
|
|
676
|
-
}
|
|
677
|
-
// Compare provenance with the immutable incoming files before modifying the checkout.
|
|
678
|
-
let expected = null;
|
|
679
|
-
let installed;
|
|
680
|
-
try {
|
|
681
|
-
if (main)
|
|
682
|
-
installed = mainStamp(root, id);
|
|
683
|
-
else
|
|
684
|
-
[expected, installed] = await verifiedReleaseStamp(root, release, git, id);
|
|
685
|
-
}
|
|
686
|
-
catch {
|
|
687
|
-
return { ok: false, message: 'Web update refused: source or local provenance does not match the release manifest.' };
|
|
688
|
-
}
|
|
689
|
-
const runtimeCurrent = main ? !(await mainRestartPending(root, head, git, id)) : same(installed, expected) && same(id.release(), expected);
|
|
690
|
-
if (head === source && runtimeCurrent)
|
|
691
|
-
return { ok: true, up_to_date: true, target: 'webui', channel, message: main ? 'Web and shared contracts are current on main.' : 'Talaria Web already contains the selected release.' };
|
|
692
|
-
const again = await git(['rev-parse', 'HEAD'], root);
|
|
693
|
-
const clean = await git(['status', '--porcelain', '--untracked-files=all'], root);
|
|
694
|
-
if (!again.ok || again.out !== head || !clean.ok || clean.out)
|
|
695
|
-
return { ok: false, message: 'The checkout changed during the update; retry after it is clean.' };
|
|
696
|
-
if (!canApply())
|
|
697
|
-
return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
|
|
698
|
-
if (head !== source) {
|
|
699
|
-
const merged = await git(['merge', '--ff-only', '--no-stat', '--no-overwrite-ignore', source], root, 30_000);
|
|
700
|
-
const actual = await git(['rev-parse', 'HEAD'], root);
|
|
701
|
-
if (!merged.ok || !actual.ok || actual.out !== source)
|
|
702
|
-
return gitFailure(merged.out, 'Web fast-forward failed; no local changes were discarded.');
|
|
703
|
-
}
|
|
704
|
-
// The supervisor re-executes the built artifact, so the checkout is installed and rebuilt before the stamp claims
|
|
705
|
-
// the new release; a failed build leaves the old stamp (and the running server) truthful.
|
|
706
|
-
const buildError = await buildWeb(root, build);
|
|
707
|
-
if (buildError !== null)
|
|
708
|
-
return { ok: false, build_failed: true, target: 'webui', channel, sourceRevision: source, message: `Source advanced to ${tag}, but the Web build failed: ${buildError}. Run \`npm ci\` and \`npm run build\` in web/ (see README), then restart Web.` };
|
|
709
|
-
const stamp = stampPath(root);
|
|
710
|
-
if (main && installed !== null) {
|
|
711
|
-
try {
|
|
712
|
-
if (isSymlink(stamp) || readFileSync(stamp, 'utf8') !== installed)
|
|
713
|
-
throw new Error('release stamp changed during update');
|
|
714
|
-
unlinkSync(stamp);
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
return { ok: false, message: 'Source advanced but its release stamp could not be cleared; inspect it before restarting Web.' };
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
else if (!main && !same(installed, expected)) {
|
|
721
|
-
const temporary = join(dirname(stamp), `.release-${String(process.pid)}-${String(Date.now())}`);
|
|
722
|
-
try {
|
|
723
|
-
writeFileSync(temporary, `${JSON.stringify(expected, null, 2)}\n`);
|
|
724
|
-
renameSync(temporary, stamp);
|
|
725
|
-
}
|
|
726
|
-
catch {
|
|
727
|
-
rmSync(temporary, { force: true });
|
|
728
|
-
return { ok: false, message: 'Source advanced, but release metadata could not be written. Repair file permissions before restarting Web.' };
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return { ok: true, target: 'webui', channel, sourceRevision: source, message: `Updated Talaria Web to ${tag}.` };
|
|
631
|
+
/** Python `apply_web_update`: installs npm releases; a source checkout is never mutated (TAL-379). */
|
|
632
|
+
export async function applyWebUpdate(webRoot, channel, git, getJson, id, npmRun = runPackageNpm, canApply = () => true, packaged = {}) {
|
|
633
|
+
if (await checkoutRoot(webRoot, git) !== null)
|
|
634
|
+
return { ok: false, manual_update: true, target: 'webui', channel, message: CONTRIBUTOR_CHECKOUT_MESSAGE };
|
|
635
|
+
return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged);
|
|
732
636
|
}
|
|
733
|
-
// ── Agent checkout: independent Stable releases or Experimental default branch ──
|
|
734
637
|
async function releaseTags(path, git) {
|
|
735
638
|
const out = await git(['tag', '--list', AGENT_TAG_GLOB, '--sort=-v:refname'], path);
|
|
736
639
|
return out.ok ? out.out.split('\n').map((l) => l.trim()).filter((tag) => /^v\d+\.\d+\.\d+$/.test(tag)) : [];
|
|
@@ -739,6 +642,12 @@ async function currentReleaseTag(path, git) {
|
|
|
739
642
|
const out = await git(['describe', '--tags', '--exact-match', '--match', AGENT_TAG_GLOB, 'HEAD'], path);
|
|
740
643
|
return out.ok && out.out ? out.out : null;
|
|
741
644
|
}
|
|
645
|
+
async function verifiedAgentIdentity(path, expectedRevision, git) {
|
|
646
|
+
const head = await git(['rev-parse', 'HEAD'], path);
|
|
647
|
+
if (!head.ok || head.out !== expectedRevision || !SHA.test(head.out))
|
|
648
|
+
return null;
|
|
649
|
+
return { verified_revision: head.out, verified_version: await currentReleaseTag(path, git) };
|
|
650
|
+
}
|
|
742
651
|
const headContainsRef = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', ref, 'HEAD'], path)).ok;
|
|
743
652
|
const canFastForwardTo = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', 'HEAD', ref], path)).ok;
|
|
744
653
|
async function detectDefaultBranch(path, git) {
|
|
@@ -795,7 +704,7 @@ async function isDirty(path, git) {
|
|
|
795
704
|
export async function checkAgentUpdate(path, git, channel = DEFAULT_CHANNEL) {
|
|
796
705
|
if (!path || !existsSync(join(path, '.git')))
|
|
797
706
|
return { name: 'agent', behind: null, no_git: true };
|
|
798
|
-
const fetched = await git
|
|
707
|
+
const fetched = await fetchWithRetry(git, ['fetch', 'origin', '--tags', '--force'], path, 15_000);
|
|
799
708
|
if (!fetched.ok) {
|
|
800
709
|
const message = fetched.out ? `fetch failed: ${sanitizeGitDiagnostic(fetched.out)}` : 'fetch failed';
|
|
801
710
|
return { name: 'agent', channel, behind: null, error: message, stale_check: true, dirty: await isDirty(path, git) };
|
|
@@ -824,8 +733,12 @@ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, pol
|
|
|
824
733
|
catch (error) {
|
|
825
734
|
return { ok: false, message: error.message };
|
|
826
735
|
}
|
|
827
|
-
if (info.current_sha === info.latest_sha)
|
|
828
|
-
|
|
736
|
+
if (info.current_sha === info.latest_sha) {
|
|
737
|
+
const verified = await verifiedAgentIdentity(path, str(info.latest_sha), git);
|
|
738
|
+
if (!verified)
|
|
739
|
+
return { ok: false, message: 'The installed Agent revision could not be verified.' };
|
|
740
|
+
return { ok: true, up_to_date: true, target: 'agent', message: 'Agent is up to date.', ...verified };
|
|
741
|
+
}
|
|
829
742
|
if (info.manual_update || info.error)
|
|
830
743
|
return { ...info, ok: false, message: info.message ?? info.error };
|
|
831
744
|
const warning = confirmAgent(info, policy);
|
|
@@ -864,7 +777,10 @@ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, pol
|
|
|
864
777
|
if (!popped.ok)
|
|
865
778
|
message += '. Local changes remain in `git stash list`; resolve them manually.';
|
|
866
779
|
}
|
|
867
|
-
|
|
780
|
+
const verified = await verifiedAgentIdentity(path, revision, git);
|
|
781
|
+
if (!verified)
|
|
782
|
+
return { ok: false, message: 'The Agent update completed, but the installed revision could not be verified.', target: 'agent' };
|
|
783
|
+
return { ok: true, message, target: 'agent', ref, ...verified };
|
|
868
784
|
}
|
|
869
785
|
async function restoreStash(path, git, pullOut) {
|
|
870
786
|
if ((await git(['stash', 'pop'], path)).ok)
|
|
@@ -903,7 +819,10 @@ export async function forceAgentUpdate(path, git, log, channel = DEFAULT_CHANNEL
|
|
|
903
819
|
log(`[updates] force update: git clean -fd failed (continuing to reset --hard): ${cleaned.out}`);
|
|
904
820
|
if (!(await git(['reset', '--hard', revision], path)).ok)
|
|
905
821
|
return { ok: false, message: `Force reset to ${ref} failed` };
|
|
906
|
-
|
|
822
|
+
const verified = await verifiedAgentIdentity(path, revision, git);
|
|
823
|
+
if (!verified)
|
|
824
|
+
return { ok: false, message: 'The Agent force update completed, but the installed revision could not be verified.', target: 'agent' };
|
|
825
|
+
return { ok: true, message: `agent force-updated to ${ref}`, target: 'agent', ref, ...verified };
|
|
907
826
|
}
|
|
908
827
|
/** Python `_inventory_locks`: report `.git/**\/*.lock` without touching any of them. */
|
|
909
828
|
export function inventoryLocks(path) {
|
|
@@ -1058,6 +977,7 @@ export class UpdateService {
|
|
|
1058
977
|
cache = { webui: null, agent: null, checked_at: 0, include_agent: true, channel: DEFAULT_CHANNEL, agent_channel: DEFAULT_CHANNEL };
|
|
1059
978
|
checking = null;
|
|
1060
979
|
checkingKey = null;
|
|
980
|
+
lastGood = new Map();
|
|
1061
981
|
applying = false;
|
|
1062
982
|
autoTimer = null;
|
|
1063
983
|
autoStarted = false;
|
|
@@ -1117,8 +1037,11 @@ export class UpdateService {
|
|
|
1117
1037
|
this.checkingKey = key;
|
|
1118
1038
|
this.checking = (async () => {
|
|
1119
1039
|
try {
|
|
1120
|
-
const webui = await checkWebUpdate(this.deps.webRoot, this.deps.webuiVersion, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.npm ?? runPackageNpm);
|
|
1121
|
-
|
|
1040
|
+
const webui = this.keepLastGood('webui', channel, await checkWebUpdate(this.deps.webRoot, this.deps.webuiVersion, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.npm ?? runPackageNpm, this.deps.experimental));
|
|
1041
|
+
// The check may have fetched release tags the startup `git describe` never saw.
|
|
1042
|
+
if (this.deps.refreshWebuiVersion)
|
|
1043
|
+
webui.current_version = this.deps.refreshWebuiVersion();
|
|
1044
|
+
const agent = includeAgent ? this.keepLastGood('agent', agentChannel, await checkAgentUpdate(this.deps.agentDir(), this.git, agentChannel)) : ignoredAgent();
|
|
1122
1045
|
if (agent.latest_sha)
|
|
1123
1046
|
Object.assign(agent, agentWarning(agent, this.agentPolicy()));
|
|
1124
1047
|
Object.assign(this.cache, { webui, agent, checked_at: this.now(), include_agent: includeAgent, channel, agent_channel: agentChannel });
|
|
@@ -1131,6 +1054,18 @@ export class UpdateService {
|
|
|
1131
1054
|
})();
|
|
1132
1055
|
return this.checking;
|
|
1133
1056
|
}
|
|
1057
|
+
/** A failed fetch (`stale_check`) keeps the channel's last good result until the cache TTL, so a network blip never flips the status; persistent failures surface. */
|
|
1058
|
+
keepLastGood(target, channel, result) {
|
|
1059
|
+
const key = `${target}:${channel}`;
|
|
1060
|
+
if (!result.stale_check) {
|
|
1061
|
+
if (!result.error)
|
|
1062
|
+
this.lastGood.set(key, { at: this.now(), result });
|
|
1063
|
+
return result;
|
|
1064
|
+
}
|
|
1065
|
+
this.deps.log(`[updates] ${target} fetch failed: ${str(result.error)}`);
|
|
1066
|
+
const prior = this.lastGood.get(key);
|
|
1067
|
+
return prior && this.now() - prior.at < CACHE_TTL_S ? { ...prior.result, stale_check: true } : result;
|
|
1068
|
+
}
|
|
1134
1069
|
startAutoApply(intervalMs = AUTO_UPDATE_INTERVAL_MS) {
|
|
1135
1070
|
if (this.autoStarted || this.autoRestartScheduled)
|
|
1136
1071
|
return;
|
|
@@ -1155,11 +1090,19 @@ export class UpdateService {
|
|
|
1155
1090
|
}
|
|
1156
1091
|
/** Shared turn admission stays closed from update start through the supervisor restart. */
|
|
1157
1092
|
blocksNewWork() { return this.applying || this.autoRestartScheduled; }
|
|
1093
|
+
pendingRestartResponse(target) {
|
|
1094
|
+
if (!this.autoRestartScheduled)
|
|
1095
|
+
return null;
|
|
1096
|
+
if (target === 'webui')
|
|
1097
|
+
return { ok: true, target, restart_scheduled: true, message: 'A Web restart is already scheduled.' };
|
|
1098
|
+
return { ok: false, status: 'already_in_progress', target, message: 'A Talaria Web restart is already scheduled. Wait for the server to restart before updating Hermes Agent.' };
|
|
1099
|
+
}
|
|
1158
1100
|
async autoApplyOnce() {
|
|
1159
1101
|
if (this.autoRunning || this.applying || this.autoRestartScheduled || !(this.deps.checkEnabled?.() ?? this.deps.autoApply?.()))
|
|
1160
1102
|
return null;
|
|
1161
1103
|
this.autoRunning = true;
|
|
1162
1104
|
const lifecycle = this.lifecycle;
|
|
1105
|
+
let notificationId = null;
|
|
1163
1106
|
try {
|
|
1164
1107
|
const channel = this.deps.channel();
|
|
1165
1108
|
const stillEnabled = () => lifecycle === this.lifecycle && Boolean(this.deps.autoApply?.()) && (this.deps.checkEnabled?.() ?? true) && channel === this.deps.channel();
|
|
@@ -1173,13 +1116,26 @@ export class UpdateService {
|
|
|
1173
1116
|
}
|
|
1174
1117
|
if (!(Number(web.behind) > 0 || web.metadata_repair === true))
|
|
1175
1118
|
return web;
|
|
1119
|
+
notificationId = this.deps.autoNotification?.begin() ?? null;
|
|
1176
1120
|
const result = await this.apply('webui', channel, stillEnabled);
|
|
1121
|
+
if (notificationId) {
|
|
1122
|
+
if (result.restart_blocked === true)
|
|
1123
|
+
this.deps.autoNotification?.transition(notificationId, 'blocked', null, undefined, result.message);
|
|
1124
|
+
else if (result.ok !== true)
|
|
1125
|
+
this.deps.autoNotification?.transition(notificationId, 'failed', null, undefined, result.message || result.error);
|
|
1126
|
+
else if (result.restart_scheduled === true)
|
|
1127
|
+
this.deps.autoNotification?.transition(notificationId, 'restarting', str(result.sourceRevision || result.candidate_revision));
|
|
1128
|
+
else
|
|
1129
|
+
this.deps.autoNotification?.transition(notificationId, 'succeeded');
|
|
1130
|
+
}
|
|
1177
1131
|
this.deps.log(`[updates] automatic Web update: ${str(result.message || (result.ok ? 'applied' : 'failed'))}`);
|
|
1178
1132
|
if (!result.ok)
|
|
1179
1133
|
this.cache.webui = { ...web, message: result.message, error: result.restart_blocked ? undefined : result.message };
|
|
1180
1134
|
return result;
|
|
1181
1135
|
}
|
|
1182
1136
|
catch (error) {
|
|
1137
|
+
if (notificationId)
|
|
1138
|
+
this.deps.autoNotification?.transition(notificationId, 'failed', null, undefined, error.message);
|
|
1183
1139
|
this.deps.log(`[updates] automatic Web update failed: ${error.message}`);
|
|
1184
1140
|
return { ok: false, error: error.message };
|
|
1185
1141
|
}
|
|
@@ -1217,8 +1173,9 @@ export class UpdateService {
|
|
|
1217
1173
|
async apply(target, channel, canApply = () => true, agentOptions = {}) {
|
|
1218
1174
|
if (this.checking)
|
|
1219
1175
|
await this.checking;
|
|
1220
|
-
|
|
1221
|
-
|
|
1176
|
+
const pendingRestart = this.pendingRestartResponse(target);
|
|
1177
|
+
if (pendingRestart)
|
|
1178
|
+
return pendingRestart;
|
|
1222
1179
|
if (!canApply())
|
|
1223
1180
|
return { ok: false, message: 'Web update settings changed; update deferred.' };
|
|
1224
1181
|
const blocked = this.blockedResponse(target);
|
|
@@ -1230,7 +1187,7 @@ export class UpdateService {
|
|
|
1230
1187
|
async applyInner(target, channel, canApply = () => true, agentOptions = {}) {
|
|
1231
1188
|
if (target === 'webui') {
|
|
1232
1189
|
const lifecycle = this.lifecycle;
|
|
1233
|
-
const result = await applyWebUpdate(this.deps.webRoot, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.
|
|
1190
|
+
const result = await applyWebUpdate(this.deps.webRoot, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.npm ?? runPackageNpm, canApply, { registry: this.deps.experimental, stateDir: this.deps.stateDir });
|
|
1234
1191
|
// Settings can cancel before mutation, but a committed replacement must finish restarting unless shutting down.
|
|
1235
1192
|
if (result.ok && !result.up_to_date && lifecycle === this.lifecycle) {
|
|
1236
1193
|
this.cache.checked_at = 0;
|
|
@@ -1286,6 +1243,9 @@ export class UpdateService {
|
|
|
1286
1243
|
await this.checking;
|
|
1287
1244
|
if (target === 'webui')
|
|
1288
1245
|
return this.apply(target, channel);
|
|
1246
|
+
const pendingRestart = this.pendingRestartResponse(target);
|
|
1247
|
+
if (pendingRestart)
|
|
1248
|
+
return pendingRestart;
|
|
1289
1249
|
const blocked = this.blockedResponse(target);
|
|
1290
1250
|
if (blocked)
|
|
1291
1251
|
return Promise.resolve(blocked);
|
|
@@ -1300,6 +1260,9 @@ export class UpdateService {
|
|
|
1300
1260
|
clearLock(target, agentOptions = {}) {
|
|
1301
1261
|
if (target === 'webui')
|
|
1302
1262
|
return this.apply(target).then((r) => ({ ...r, lock_recovery: { action: 'retry-only' } }));
|
|
1263
|
+
const pendingRestart = this.pendingRestartResponse(target);
|
|
1264
|
+
if (pendingRestart)
|
|
1265
|
+
return Promise.resolve(pendingRestart);
|
|
1303
1266
|
const blocked = this.blockedResponse(target);
|
|
1304
1267
|
if (blocked)
|
|
1305
1268
|
return Promise.resolve(blocked);
|