@maudecode/talaria-web 1.11.0 → 1.12.1
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/_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 +329 -0
- package/dist/api/tools-router.js +97 -4
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +812 -55
- package/dist/auth/oidc.js +2 -0
- package/dist/auth/oidc.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/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/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 +11 -5
- 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/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/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 +120 -0
- package/dist/tools/update-notifications.js +387 -0
- package/dist/tools/update-notifications.js.map +1 -0
- package/dist/tools/updates.d.ts +49 -3
- package/dist/tools/updates.js +319 -53
- package/dist/tools/updates.js.map +1 -1
- package/package.json +2 -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 +115 -116
- package/static/dist/assets/AppShell-m4w420jl.js +1 -0
- package/static/dist/assets/ChatPage-cuqcnisv.js +20 -0
- package/static/dist/assets/HubRoute-hqgqtm35.js +2 -0
- package/static/dist/assets/{Markdown-ftyfzds0.js → Markdown-j2hlu30v.js} +7 -7
- package/static/dist/assets/RightPanel-oid5q557.js +1 -0
- package/static/dist/assets/Select-ot2zr0il.js +1 -0
- package/static/dist/assets/SessionListPanel-o3sgjnrq.js +1 -0
- package/static/dist/assets/{SettingsLayout-pdpmhy1o.js → SettingsLayout-b0idn649.js} +1 -1
- package/static/dist/assets/States-htz1oee5.js +1 -0
- package/static/dist/assets/Toaster-h49pk1hy.js +1 -0
- package/static/dist/assets/_app-h7eaezs7.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-h8oaqzsj.js +1 -0
- package/static/dist/assets/_app.index-xinao3xh.js +1 -0
- package/static/dist/assets/_app.insights-cxplvhkm.js +1 -0
- package/static/dist/assets/_app.kanban-hhli8h0h.js +1 -0
- package/static/dist/assets/_app.logs-f6srwjfl.js +1 -0
- package/static/dist/assets/_app.memory-lntcea85.js +1 -0
- package/static/dist/assets/_app.profiles-lftyp6of.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-n1ggcf5e.js → _app.session._sessionId-hhpcqih4.js} +1 -1
- package/static/dist/assets/_app.settings-rs93zpvw.js +1 -0
- package/static/dist/assets/_app.settings._section-nafqsbwm.js +1 -0
- package/static/dist/assets/_app.skills-jzoi1ivd.js +1 -0
- package/static/dist/assets/_app.tasks-ijphsw56.js +2 -0
- package/static/dist/assets/_app.todos-emyh8inv.js +1 -0
- package/static/dist/assets/_app.workspaces-fwr1cum8.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-i4bgzu83.js → abnfDiagram-VCTEODGH-egykbz8h.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-l6y92mbn.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-mx21lwqa.js → architectureDiagram-5GKGNRK7-b2e0wk3y.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-o1hhchxv.js → blockDiagram-I7D4REHJ-i3cq67dn.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-jygqtvd8.js → c4Diagram-7LVT6UL2-lg1p1s7a.js} +1 -1
- package/static/dist/assets/channel-gq8cwu71.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-hjcpubgk.js → chunk-4HAMMTFA-i0s1s7zi.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-eibnex6m.js → chunk-75Z2AOVW-kwinpapo.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-k1tp3qnp.js → chunk-DU6HZSFF-hb8qrbc3.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-lvcgn16f.js → chunk-F27PBJKO-dy2tqtcs.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-ldlzruv1.js → chunk-GMAD6QVW-fqox5yvt.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-hs1mva9d.js → chunk-GVQU2GXP-bo3x6ze4.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-o7eqx3yb.js → chunk-IMKFNOWR-nxs6rrcq.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-bp1ofl70.js → chunk-L3NEJ4N5-f9yeq1fi.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-o8m6mdz5.js → chunk-OSK3NFVY-c7df44qv.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-kx83fti2.js → chunk-P2QGCYS3-eyjzf5o5.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-mwl99e9y.js → chunk-POPQ4Y6H-iy3hp9rc.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-oriuokxy.js → chunk-PWAF6VOD-co2yoefz.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-ab5cf00v.js → chunk-SHT3W25Y-obzqmn9k.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-kqdrpr17.js → chunk-SVP7TREG-hqwdo6vt.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-kfbnjabk.js → chunk-TICWLB2K-l8t1mdfd.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-fg9sc5r8.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-fg9sc5r8.js +1 -0
- package/static/dist/assets/{copied-dp33szhq.js → copied-hurn8vn4.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-mm5sj9s7.js → cynefin-OW5HDTMX-ngx2ihay.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-nfon5q38.js → cynefinDiagram-5FMLGOSQ-fiyooys3.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-dzdp9ypb.js → dagre-GXQ25YYZ-fctryel6.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-ckxzqo01.js → diagram-S7CK7UJ4-m27jopok.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-hw16h1um.js → diagram-UQ7AKVKN-ojgkbmp4.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-mu7wu2zg.js → diagram-VSXAHHWV-gfafpgyj.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-k9uprwtj.js → diagram-VX7I27RA-daobicg4.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-csdjzrg9.js → diagram-Z3DM3KII-mi475zvk.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-n8mbw2jj.js → ebnfDiagram-PWID7BFC-daf4zvsy.js} +1 -1
- package/static/dist/assets/endpoints-cyjhnrbn.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fsfe1uj0.js → erDiagram-RLTQ6QDP-gxfhxvfp.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-manga0bq.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-bpt13uny.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-gsba3iq9.js → ganttDiagram-EL5Y4UJY-k3208e0p.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-na86q1pu.js → gitGraph-4MIJSDKK-gjuhj4zk.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-g72ew3xz.js → gitGraphDiagram-WWUBYQGX-jglxvux1.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-okn18cp1.js → highlighted-body-KPVGNVTW-gh24xc9c.js} +1 -1
- package/static/dist/assets/index-gsg78cb4.css +2 -0
- package/static/dist/assets/index-jb47los5.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-i01jeltk.js → info-A6RAGUB7-huf0xaoq.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-cayiv6fa.js → infoDiagram-27XIBGKW-jk3n96ix.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-g4rf14bv.js → ishikawaDiagram-5VMMS53U-o95uo5dx.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-fusb2424.js → journeyDiagram-3NMN7TZE-h5ye2l70.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-g831pwlr.js → kanban-definition-UXKFOSKX-lgrpk5yi.js} +1 -1
- package/static/dist/assets/{lazyRouteComponent-ol965zwk.js → lazyRouteComponent-owywngyt.js} +1 -1
- package/static/dist/assets/{line-md6em6gc.js → line-cihmc0pn.js} +1 -1
- package/static/dist/assets/{login-kfclsqd2.js → login-g063apx6.js} +1 -1
- package/static/dist/assets/mermaid-HWGCJPDP-okd1mnvj.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-mq3uns9m.js → mermaid-parser.core-d1609aga.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-oxafrned.js → mindmap-definition-YA3MSWOX-lbvtir31.js} +1 -1
- package/static/dist/assets/{onboarding-fo6hnkyv.js → onboarding-ngw0x0pr.js} +1 -1
- package/static/dist/assets/{packet-AYTQ26CC-ggxyvbym.js → packet-AYTQ26CC-o4cp5z3k.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-nk7xcqgp.js → pegDiagram-XKGWAZYB-e8xqu1qj.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-ito2razh.js → pie-WAS4IAKB-hm9icuov.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-g02xexar.js → pieDiagram-E7YTZNPT-hj96x7hn.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-d4jn4xq5.js → quadrantDiagram-AXDQQJYC-80utbxhm.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-nebvfdzt.js → radar-RG4KPBEZ-k39b6100.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-ny3gebse.js → railroad-74A4TZTK-ewyqz67j.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-dx2q0evz.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-l8xt49t5.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-kuxha9yz.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-m09mmu67.js → railroadDiagram-O6MQD6OU-iwv9dby8.js} +1 -1
- package/static/dist/assets/{refresh-cw-eitd7yva.js → refresh-cw-hjzomwlo.js} +1 -1
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-evggpcbk.js → requirementDiagram-BXWQKSXE-cdgh5jha.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-bun89039.js → sankeyDiagram-P5KCCOFB-dbu6p97m.js} +1 -1
- package/static/dist/assets/{saved-culx14ii.js → saved-feqhycco.js} +1 -1
- package/static/dist/assets/{search-nu6kzq55.js → search-n3jdc20p.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-hhs9f09o.js → sequenceDiagram-WJ2MYXX4-b0wchifg.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-mj0gcozh.js → settings_section_extensions_title-mtyviexd.js} +1 -1
- package/static/dist/assets/{share._token-k4pcnxqu.js → share._token-cwov8uqh.js} +2 -2
- package/static/dist/assets/{sliders-horizontal-b9ljga1o.js → sliders-horizontal-i1lseyg0.js} +1 -1
- package/static/dist/assets/{stateDiagram-D77RDMKH-gi7t1tro.js → stateDiagram-D77RDMKH-mb4b38xh.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-cn1fnqrg.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-ea5urvph.js → swimlanes-42K2YHIH-ofrpfwj0.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d652ummv.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-mq64rm3b.js → timeline-definition-24CTP7MA-hlw0jg6o.js} +1 -1
- package/static/dist/assets/todoStore-mfy9lpio.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-cimbtxlz.js → treeView-Q6P3EWNA-d93wr19c.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-wig5gi1y.js → treemap-WGGIJYW6-g7ezdedz.js} +1 -1
- package/static/dist/assets/{untitled-mbwemvxf.js → untitled-mqxhd1eh.js} +1 -1
- package/static/dist/assets/{useForm-li4bljlv.js → useForm-hh33yrwa.js} +1 -1
- package/static/dist/assets/useLocale-cr30vl7r.js +1 -0
- package/static/dist/assets/{vennDiagram-4TSXK5OY-ez63kh7n.js → vennDiagram-4TSXK5OY-m5vgeba5.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-r7rftz31.js → wardley-WFR3VGLG-gpao9bxv.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-mvqg9eqc.js → wardleyDiagram-VM6X3IG4-g2yzwshm.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-eqxdfc91.js → xychartDiagram-S5SC5T6Z-itakhpic.js} +1 -1
- package/static/dist/index.html +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/RightPanel-febk3dkd.js +0 -1
- 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/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, unlinkSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, 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';
|
|
@@ -116,6 +117,15 @@ function fetchFailureMessage(out, network) {
|
|
|
116
117
|
const lower = detail.toLowerCase();
|
|
117
118
|
return NETWORK_FAILURES.some((s) => lower.includes(s)) ? network : `fetch failed: ${detail}`;
|
|
118
119
|
}
|
|
120
|
+
/** One delayed retry absorbs a transient network blip; a lock conflict is not transient. */
|
|
121
|
+
export const FETCH_RETRY_MS = 1000;
|
|
122
|
+
async function fetchWithRetry(git, args, cwd, timeoutMs) {
|
|
123
|
+
const first = await git(args, cwd, timeoutMs);
|
|
124
|
+
if (first.ok || isGitLockError(first.out))
|
|
125
|
+
return first;
|
|
126
|
+
await new Promise((r) => setTimeout(r, FETCH_RETRY_MS));
|
|
127
|
+
return git(args, cwd, timeoutMs);
|
|
128
|
+
}
|
|
119
129
|
function gitFailure(out, message) {
|
|
120
130
|
if (isGitLockError(out))
|
|
121
131
|
return { ok: false, lock_conflict: true, message: 'Web update is blocked by a repository lock. Wait for the other Git operation or inspect the checkout manually.' };
|
|
@@ -155,7 +165,13 @@ function sortKeys(value) {
|
|
|
155
165
|
}
|
|
156
166
|
const same = (a, b) => canon(a) === canon(b);
|
|
157
167
|
// ── published release sets ───────────────────────────────────────────────────
|
|
168
|
+
/** `transient`: GitHub was unreachable, slow, or overloaded, so a retry can succeed; the rest describe published state. */
|
|
158
169
|
export class ReleaseUnavailable extends Error {
|
|
170
|
+
transient;
|
|
171
|
+
constructor(message, transient = false) {
|
|
172
|
+
super(message);
|
|
173
|
+
this.transient = transient;
|
|
174
|
+
}
|
|
159
175
|
}
|
|
160
176
|
/** 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
177
|
export function githubJson(fetchImpl, env) {
|
|
@@ -164,17 +180,25 @@ export function githubJson(fetchImpl, env) {
|
|
|
164
180
|
const token = (env.TALARIA_RELEASE_TOKEN ?? '').trim();
|
|
165
181
|
if (token)
|
|
166
182
|
headers.Authorization = `Bearer ${token}`;
|
|
167
|
-
|
|
183
|
+
const send = async (url, init) => {
|
|
184
|
+
try {
|
|
185
|
+
return await fetchImpl(url, init);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw new ReleaseUnavailable(`GitHub is unreachable: ${error.message}`, true);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
let res = await send(API_ROOT + path, { headers, redirect: 'manual', signal: AbortSignal.timeout(5000) });
|
|
168
192
|
if (res.status >= 300 && res.status < 400) {
|
|
169
193
|
const target = new URL(res.headers.get('location') ?? '', API_ROOT + path);
|
|
170
194
|
if (target.protocol !== 'https:' || target.hostname !== 'release-assets.githubusercontent.com')
|
|
171
195
|
throw new ReleaseUnavailable('Unexpected release download redirect');
|
|
172
196
|
const anonymous = { ...headers };
|
|
173
197
|
delete anonymous.Authorization;
|
|
174
|
-
res = await
|
|
198
|
+
res = await send(target.toString(), { headers: anonymous, signal: AbortSignal.timeout(5000) });
|
|
175
199
|
}
|
|
176
200
|
if (!res.ok)
|
|
177
|
-
throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}
|
|
201
|
+
throw new ReleaseUnavailable(`GitHub answered ${String(res.status)}`, res.status >= 500 || res.status === 429);
|
|
178
202
|
const body = await readCapped(res, 2_000_000);
|
|
179
203
|
if (!body)
|
|
180
204
|
throw new ReleaseUnavailable('Release metadata exceeds the download limit');
|
|
@@ -187,7 +211,7 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
|
|
|
187
211
|
const deadline = now() + 15_000;
|
|
188
212
|
const fetchJson = (path, asset = false) => {
|
|
189
213
|
if (now() >= deadline)
|
|
190
|
-
throw new ReleaseUnavailable('Release lookup exceeded its deadline; retry or update manually');
|
|
214
|
+
throw new ReleaseUnavailable('Release lookup exceeded its deadline; retry or update manually', true);
|
|
191
215
|
return getJson(path, { asset });
|
|
192
216
|
};
|
|
193
217
|
const published = [];
|
|
@@ -263,6 +287,81 @@ export async function publishedWebRelease(channel, getJson, now = () => performa
|
|
|
263
287
|
}
|
|
264
288
|
throw new ReleaseUnavailable('No completed Talaria Web release is available on this channel');
|
|
265
289
|
}
|
|
290
|
+
// ── Experimental artifacts (TAL-343) ─────────────────────────────────────────
|
|
291
|
+
const GHCR = 'https://ghcr.io';
|
|
292
|
+
const EXPERIMENTAL_REPOSITORY = 'maudecode/talaria-web-experimental';
|
|
293
|
+
const EXPERIMENTAL_ARTIFACT_TYPE = 'application/vnd.maudecode.talaria-web.experimental.v1';
|
|
294
|
+
const EXPERIMENTAL_LAYER_TYPE = 'application/vnd.maudecode.talaria-web.npm.tgz';
|
|
295
|
+
/** GHCR serves blobs by redirecting here; the pull token never follows. */
|
|
296
|
+
const EXPERIMENTAL_BLOB_HOST = 'pkg-containers.githubusercontent.com';
|
|
297
|
+
export const EXPERIMENTAL_TARBALL_CAP = 200_000_000;
|
|
298
|
+
const EXPERIMENTAL_VERSION = new RegExp(`^${VERSION}-exp\\.([a-f0-9]{12})$`);
|
|
299
|
+
/** Anonymous GHCR client with `githubJson`'s timeouts, caps, and transient classification. */
|
|
300
|
+
export function ghcrExperimental(fetchImpl) {
|
|
301
|
+
const send = async (url, init) => {
|
|
302
|
+
try {
|
|
303
|
+
return await fetchImpl(url, { redirect: 'manual', ...init });
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
throw new ReleaseUnavailable(`GHCR is unreachable: ${error.message}`, true);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const check = (res, what) => {
|
|
310
|
+
if (!res.ok)
|
|
311
|
+
throw new ReleaseUnavailable(`GHCR answered ${String(res.status)} for the Experimental ${what}`, res.status >= 500 || res.status === 429);
|
|
312
|
+
};
|
|
313
|
+
const json = async (res, what) => {
|
|
314
|
+
check(res, what);
|
|
315
|
+
const body = await readCapped(res, 2_000_000);
|
|
316
|
+
if (!body)
|
|
317
|
+
throw new ReleaseUnavailable(`Experimental ${what} exceeds the download limit`);
|
|
318
|
+
try {
|
|
319
|
+
return dict(JSON.parse(body.toString('utf8')));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
throw new ReleaseUnavailable(`Invalid Experimental ${what}`);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const token = async () => {
|
|
326
|
+
const body = await json(await send(`${GHCR}/token?scope=repository:${EXPERIMENTAL_REPOSITORY}:pull`, { signal: AbortSignal.timeout(5000) }), 'pull token');
|
|
327
|
+
if (typeof body.token !== 'string' || !body.token)
|
|
328
|
+
throw new ReleaseUnavailable('GHCR did not issue an anonymous pull token');
|
|
329
|
+
return body.token;
|
|
330
|
+
};
|
|
331
|
+
return {
|
|
332
|
+
release: async () => {
|
|
333
|
+
const auth = { Authorization: `Bearer ${await token()}` };
|
|
334
|
+
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');
|
|
335
|
+
const layers = Array.isArray(m.layers) ? m.layers.map(dict) : [];
|
|
336
|
+
const layer = layers[0];
|
|
337
|
+
if (m.artifactType !== EXPERIMENTAL_ARTIFACT_TYPE || layers.length !== 1 || layer?.mediaType !== EXPERIMENTAL_LAYER_TYPE)
|
|
338
|
+
throw new ReleaseUnavailable('The Experimental artifact has an unexpected format');
|
|
339
|
+
const annotations = dict(m.annotations);
|
|
340
|
+
const sourceRevision = annotations['org.opencontainers.image.revision'];
|
|
341
|
+
const version = annotations['org.opencontainers.image.version'];
|
|
342
|
+
const size = layer.size;
|
|
343
|
+
if (typeof sourceRevision !== 'string' || !SHA.test(sourceRevision) || typeof version !== 'string' || EXPERIMENTAL_VERSION.exec(version)?.[4] !== sourceRevision.slice(0, 12)
|
|
344
|
+
|| typeof layer.digest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(layer.digest) || typeof size !== 'number' || !Number.isInteger(size) || size < 1 || size > EXPERIMENTAL_TARBALL_CAP) {
|
|
345
|
+
throw new ReleaseUnavailable('The Experimental artifact has malformed provenance');
|
|
346
|
+
}
|
|
347
|
+
return { tag: `web-exp-v${version}`, version, sourceRevision, digest: layer.digest, size };
|
|
348
|
+
},
|
|
349
|
+
download: async (release) => {
|
|
350
|
+
let res = await send(`${GHCR}/v2/${EXPERIMENTAL_REPOSITORY}/blobs/${release.digest}`, { headers: { Authorization: `Bearer ${await token()}` }, signal: AbortSignal.timeout(120_000) });
|
|
351
|
+
if (res.status >= 300 && res.status < 400) {
|
|
352
|
+
const target = new URL(res.headers.get('location') ?? '', GHCR);
|
|
353
|
+
if (target.protocol !== 'https:' || target.hostname !== EXPERIMENTAL_BLOB_HOST)
|
|
354
|
+
throw new ReleaseUnavailable('Unexpected Experimental download redirect');
|
|
355
|
+
res = await send(target.toString(), { redirect: 'error', signal: AbortSignal.timeout(120_000) });
|
|
356
|
+
}
|
|
357
|
+
check(res, 'download');
|
|
358
|
+
const body = await readCapped(res, release.size);
|
|
359
|
+
if (body?.length !== release.size || `sha256:${createHash('sha256').update(body).digest('hex')}` !== release.digest)
|
|
360
|
+
throw new ReleaseUnavailable('The Experimental download does not match its digest');
|
|
361
|
+
return body;
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
266
365
|
// ── Web checkout ─────────────────────────────────────────────────────────────
|
|
267
366
|
/** The git root when `webRoot` is `<root>/web` of a Talaria checkout with a GitHub `maudecode/talaria` origin; else null. */
|
|
268
367
|
export async function checkoutRoot(webRoot, git) {
|
|
@@ -305,7 +404,7 @@ export async function npmInstallInfo(webRoot, npm = runPackageNpm, container = e
|
|
|
305
404
|
catch {
|
|
306
405
|
return null;
|
|
307
406
|
}
|
|
308
|
-
if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !new RegExp(`^${VERSION}$`).test(metadata.version))
|
|
407
|
+
if (metadata.name !== WEB_NPM_PACKAGE || typeof metadata.version !== 'string' || !(new RegExp(`^${VERSION}$`).test(metadata.version) || EXPERIMENTAL_VERSION.test(metadata.version)))
|
|
309
408
|
return null;
|
|
310
409
|
const global = await npm(['root', '--global'], webRoot, 10_000);
|
|
311
410
|
if (!global.ok || !isAbsolute(global.out))
|
|
@@ -336,6 +435,30 @@ function diskRelease(webRoot) {
|
|
|
336
435
|
return null;
|
|
337
436
|
}
|
|
338
437
|
}
|
|
438
|
+
/** The channel an npm install came from: its release tag prefix (`web-exp-v` or `web-v`). */
|
|
439
|
+
function npmInstallChannel(installed, id) {
|
|
440
|
+
return str(diskRelease(installed.packageRoot)?.tag ?? id.release().tag).startsWith('web-exp-v') ? 'experimental' : 'stable';
|
|
441
|
+
}
|
|
442
|
+
/** An npm install on Experimental: behind when its source differs from the `experimental` tag, or when switching channels. */
|
|
443
|
+
async function checkNpmExperimental(result, installed, id, registry) {
|
|
444
|
+
let release;
|
|
445
|
+
try {
|
|
446
|
+
release = await registry.release();
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
const unavailable = error instanceof ReleaseUnavailable ? error : new ReleaseUnavailable('The Experimental artifact is unavailable');
|
|
450
|
+
return { ...result, install_kind: 'npm', manual_update: true, error: unavailable.message, ...(unavailable.transient ? { stale_check: true } : {}) };
|
|
451
|
+
}
|
|
452
|
+
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 });
|
|
453
|
+
const disk = diskRelease(installed.packageRoot);
|
|
454
|
+
const current = str(disk?.sourceRevision ?? id.release().sourceRevision) || null;
|
|
455
|
+
if (npmInstallChannel(installed, id) !== 'experimental')
|
|
456
|
+
return { ...result, current_sha: current, behind: 1, channel_switch: true, message: `Switching to Experimental installs ${release.version}.` };
|
|
457
|
+
if (disk?.sourceRevision !== release.sourceRevision)
|
|
458
|
+
return { ...result, current_sha: current, behind: 1, message: `Experimental update ${release.version} is available.` };
|
|
459
|
+
const metadataRepair = !same(id.release(), disk);
|
|
460
|
+
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.' } : {}) };
|
|
461
|
+
}
|
|
339
462
|
/** Python `verify_release_source`: the stamp Web expects for `release`, computed from immutable blobs at its source revision. */
|
|
340
463
|
export async function verifyReleaseSource(root, release, git) {
|
|
341
464
|
const files = {};
|
|
@@ -390,9 +513,12 @@ function mainStamp(root, id) {
|
|
|
390
513
|
return data;
|
|
391
514
|
}
|
|
392
515
|
async function mainRevision(root, git) {
|
|
393
|
-
const fetched = await git
|
|
516
|
+
const fetched = await fetchWithRetry(git, ['fetch', '--no-tags', 'origin', 'refs/heads/main:refs/remotes/origin/main'], root, 30_000);
|
|
394
517
|
if (!fetched.ok)
|
|
395
518
|
return [null, fetched.out];
|
|
519
|
+
// Single-branch clones never receive tags, so `git describe` would name a stale Web release.
|
|
520
|
+
// Best effort and unforced: a conflicting local tag is kept and never fails the main fetch.
|
|
521
|
+
await git(['fetch', '--no-tags', 'origin', 'refs/tags/web-v*:refs/tags/web-v*', 'refs/tags/web-exp-v*:refs/tags/web-exp-v*'], root, 30_000);
|
|
396
522
|
const source = await git(['rev-parse', 'refs/remotes/origin/main^{commit}'], root);
|
|
397
523
|
return source.ok && SHA.test(source.out) ? [source.out, ''] : [null, ''];
|
|
398
524
|
}
|
|
@@ -423,8 +549,10 @@ async function checkMainUpdate(root, result, git, id) {
|
|
|
423
549
|
if (root === null)
|
|
424
550
|
return { ...result, manual_update: true, message: 'Main updates require an authenticated Talaria source checkout with Web under web/.' };
|
|
425
551
|
const [source, error] = await mainRevision(root, git);
|
|
426
|
-
if (source === null)
|
|
427
|
-
|
|
552
|
+
if (source === null) {
|
|
553
|
+
const detail = sanitizeGitDiagnostic(error);
|
|
554
|
+
return { ...result, stale_check: true, error: gitFailure(error, detail ? `Could not fetch origin/main: ${detail}` : 'Could not fetch origin/main; check Git read access.').message };
|
|
555
|
+
}
|
|
428
556
|
Object.assign(result, { latest_sha: source, latest_version: `main@${source.slice(0, 12)}` });
|
|
429
557
|
const head = await git(['rev-parse', 'HEAD'], root);
|
|
430
558
|
const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
|
|
@@ -454,23 +582,29 @@ async function checkMainUpdate(root, result, git, id) {
|
|
|
454
582
|
return result;
|
|
455
583
|
}
|
|
456
584
|
/** Python `check_web_update`. */
|
|
457
|
-
export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm) {
|
|
585
|
+
export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJson, id, npmRun = runPackageNpm, registry) {
|
|
458
586
|
const root = await checkoutRoot(webRoot, git);
|
|
459
587
|
const npmInstall = root === null ? await npmInstallInfo(webRoot, npmRun) : null;
|
|
460
588
|
const result = { name: 'webui', channel, repo_url: REPOSITORY_URL, current_version: currentVersion, behind: null, no_git: root === null };
|
|
461
589
|
if (channel === 'experimental' && root !== null)
|
|
462
590
|
return checkMainUpdate(root, result, git, id);
|
|
591
|
+
if (channel === 'experimental' && npmInstall && registry)
|
|
592
|
+
return checkNpmExperimental(result, npmInstall, id, registry);
|
|
463
593
|
let release;
|
|
464
594
|
try {
|
|
465
595
|
release = await publishedWebRelease(channel, getJson);
|
|
466
596
|
}
|
|
467
597
|
catch (error) {
|
|
468
598
|
if (error instanceof ReleaseUnavailable)
|
|
469
|
-
return { ...result, manual_update: true, error: error.message };
|
|
599
|
+
return { ...result, manual_update: true, error: error.message, ...(error.transient ? { stale_check: true } : {}) };
|
|
470
600
|
return { ...result, manual_update: true, error: 'Talaria release metadata is unavailable. Private repositories require TALARIA_RELEASE_TOKEN with Contents read access.' };
|
|
471
601
|
}
|
|
472
602
|
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
603
|
if (root === null) {
|
|
604
|
+
if (npmInstall && channel === 'stable' && release.npm && npmInstallChannel(npmInstall, id) === 'experimental') {
|
|
605
|
+
// A channel switch installs the newest Stable package regardless of version order (TAL-343).
|
|
606
|
+
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}.` };
|
|
607
|
+
}
|
|
474
608
|
if (npmInstall && channel === 'stable' && release.npm && !str(id.release().tag).startsWith('web-exp-v')) {
|
|
475
609
|
const comparison = compareVersions(npmInstall.version, release.version);
|
|
476
610
|
if (comparison > 0)
|
|
@@ -536,34 +670,82 @@ export async function checkWebUpdate(webRoot, currentVersion, channel, git, getJ
|
|
|
536
670
|
Object.assign(result, { manual_update: true, message: 'Commit or remove local changes before updating; Web updates never discard them.' });
|
|
537
671
|
return result;
|
|
538
672
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
673
|
+
export const CHANNEL_SWITCH_BACKUPS = 5;
|
|
674
|
+
/** Web's persisted stores (`settings.json`, the project store, the workspace store) as seen from the state dir. */
|
|
675
|
+
const PERSISTED_STORES = ['settings.json', 'projects.json', 'workspaces.json', 'last_workspace.txt'];
|
|
676
|
+
/** Copy the persisted stores to `<stateDir>/backups/channel-switch-<UTC timestamp>/`, keeping the newest few; throws on failure. */
|
|
677
|
+
export function backupPersistedStores(stateDir, now = new Date(), keep = CHANNEL_SWITCH_BACKUPS) {
|
|
678
|
+
const root = join(stateDir, 'backups');
|
|
679
|
+
mkdirSync(root, { recursive: true });
|
|
680
|
+
const name = `channel-switch-${now.toISOString().replace(/[:.]/g, '-')}`;
|
|
681
|
+
const target = join(root, name);
|
|
682
|
+
if (existsSync(target))
|
|
683
|
+
throw new Error(`${name} already exists`);
|
|
684
|
+
// Only a complete copy is published under the name that counts toward the retained backups.
|
|
685
|
+
const partial = mkdtempSync(join(root, `.${name}-`));
|
|
686
|
+
try {
|
|
687
|
+
for (const store of PERSISTED_STORES)
|
|
688
|
+
if (existsSync(join(stateDir, store)))
|
|
689
|
+
copyFileSync(join(stateDir, store), join(partial, store));
|
|
690
|
+
renameSync(partial, target);
|
|
691
|
+
}
|
|
692
|
+
catch (error) {
|
|
693
|
+
rmSync(partial, { recursive: true, force: true });
|
|
694
|
+
throw error;
|
|
695
|
+
}
|
|
696
|
+
const backups = readdirSync(root).filter((name) => name.startsWith('channel-switch-')).sort();
|
|
697
|
+
for (const old of backups.slice(0, Math.max(0, backups.length - keep)))
|
|
698
|
+
rmSync(join(root, old), { recursive: true, force: true });
|
|
699
|
+
return target;
|
|
700
|
+
}
|
|
701
|
+
async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged) {
|
|
544
702
|
const installed = await npmInstallInfo(webRoot, npmRun);
|
|
545
703
|
if (!installed)
|
|
546
704
|
return { ok: false, manual_update: true, message: 'Automatic npm updates require a direct global @maudecode/talaria-web installation.' };
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
705
|
+
// Switching channels installs the selected channel's newest artifact regardless of version order (TAL-343).
|
|
706
|
+
const switching = npmInstallChannel(installed, id) !== channel;
|
|
707
|
+
const onDisk = diskRelease(installed.packageRoot);
|
|
708
|
+
let target;
|
|
709
|
+
if (channel === 'stable') {
|
|
710
|
+
let release;
|
|
711
|
+
try {
|
|
712
|
+
release = await publishedWebRelease('stable', getJson);
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
return { ok: false, message: 'Cannot resolve a completed Talaria release. Check private-repository read access.' };
|
|
716
|
+
}
|
|
717
|
+
if (!release.npm)
|
|
718
|
+
return { ok: false, manual_update: true, message: 'The completed Stable release does not include an npm package identity.' };
|
|
719
|
+
target = { version: release.version, sourceRevision: release.sourceRevision, label: `npm release ${release.version}`, matches: (r) => same(r, release.runtime), npm: release.npm };
|
|
720
|
+
if (!switching) {
|
|
721
|
+
if (!canApply())
|
|
722
|
+
return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
|
|
723
|
+
const comparison = compareVersions(installed.version, release.version);
|
|
724
|
+
if (comparison > 0)
|
|
725
|
+
return { ok: false, manual_update: true, message: 'This npm installation is ahead of the selected Stable release.' };
|
|
726
|
+
if (comparison === 0 && !target.matches(onDisk))
|
|
727
|
+
return { ok: false, manual_update: true, message: 'Installed npm release metadata does not match the completed release.' };
|
|
728
|
+
}
|
|
550
729
|
}
|
|
551
|
-
|
|
552
|
-
|
|
730
|
+
else {
|
|
731
|
+
if (!packaged.registry)
|
|
732
|
+
return { ok: false, manual_update: true, message: 'Packaged Experimental updates are unavailable in this installation.' };
|
|
733
|
+
let release;
|
|
734
|
+
try {
|
|
735
|
+
release = await packaged.registry.release();
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
return { ok: false, message: error instanceof ReleaseUnavailable ? error.message : 'The Experimental artifact is unavailable.' };
|
|
739
|
+
}
|
|
740
|
+
const identity = { tag: release.tag, version: release.version, sourceRevision: release.sourceRevision, releaseSet: release.sourceRevision };
|
|
741
|
+
// validateReleaseInfo (in diskRelease) already applies Stable's contract and Agent-pin checks to the packaged files.
|
|
742
|
+
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
743
|
}
|
|
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}.` };
|
|
744
|
+
const current = !switching && target.matches(onDisk);
|
|
745
|
+
if (current) {
|
|
746
|
+
if (same(id.release(), onDisk))
|
|
747
|
+
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}.` };
|
|
748
|
+
return { ok: true, target: 'webui', channel, sourceRevision: target.sourceRevision, ...(target.npm ? { npm: target.npm } : {}), message: `Restarting Talaria Web with ${target.label}.` };
|
|
567
749
|
}
|
|
568
750
|
if (!canApply())
|
|
569
751
|
return { ok: false, message: 'Web update deferred because its settings or lifecycle changed.' };
|
|
@@ -573,19 +755,42 @@ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply
|
|
|
573
755
|
const candidate = join(staging, 'lib/node_modules/@maudecode/talaria-web');
|
|
574
756
|
const backup = join(staging, 'previous');
|
|
575
757
|
let keepBackup = false;
|
|
758
|
+
let stores = null;
|
|
576
759
|
try {
|
|
577
|
-
|
|
760
|
+
let spec = target.npm ?? '';
|
|
761
|
+
if (target.experimental) {
|
|
762
|
+
let tarball;
|
|
763
|
+
try {
|
|
764
|
+
tarball = await packaged.registry.download(target.experimental);
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
return { ok: false, verification_failed: true, target: 'webui', channel, message: `${error instanceof ReleaseUnavailable ? error.message : 'The Experimental download failed'}. The installed package was preserved.` };
|
|
768
|
+
}
|
|
769
|
+
spec = join(staging, 'talaria-web-experimental.tgz');
|
|
770
|
+
writeFileSync(spec, tarball);
|
|
771
|
+
}
|
|
772
|
+
const update = await npmRun(['install', '--global', '--prefix', staging, '--no-audit', '--no-fund', spec], staging, WEB_BUILD_TIMEOUT_MS);
|
|
578
773
|
if (!update.ok)
|
|
579
774
|
return { ok: false, install_failed: true, target: 'webui', channel, message: `npm update failed: ${sanitizeGitDiagnostic(update.out, 1000) || 'unknown error'}` };
|
|
580
775
|
const metadata = dict(JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')));
|
|
581
776
|
const bins = dict(metadata.bin);
|
|
582
|
-
if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !==
|
|
777
|
+
if (metadata.name !== WEB_NPM_PACKAGE || metadata.version !== target.version || !target.matches(diskRelease(candidate))
|
|
583
778
|
|| !['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:
|
|
779
|
+
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
780
|
}
|
|
586
|
-
const
|
|
587
|
-
if (!canApply() ||
|
|
781
|
+
const recheck = await npmInstallInfo(webRoot, npmRun);
|
|
782
|
+
if (!canApply() || recheck?.packageRoot !== installed.packageRoot || recheck.version !== installed.version)
|
|
588
783
|
return { ok: false, message: 'The npm installation or update settings changed; retry the update.' };
|
|
784
|
+
if (switching) {
|
|
785
|
+
try {
|
|
786
|
+
if (!packaged.stateDir)
|
|
787
|
+
throw new Error('the Web state directory is unknown');
|
|
788
|
+
stores = backupPersistedStores(packaged.stateDir);
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
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.` };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
589
794
|
renameSync(installed.packageRoot, backup);
|
|
590
795
|
try {
|
|
591
796
|
renameSync(candidate, installed.packageRoot);
|
|
@@ -609,13 +814,14 @@ async function applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply
|
|
|
609
814
|
catch { /* a leftover staging directory must not prevent restart after a verified switch */ }
|
|
610
815
|
}
|
|
611
816
|
}
|
|
612
|
-
return { ok: true, target: 'webui', channel, sourceRevision:
|
|
817
|
+
return { ok: true, target: 'webui', channel, sourceRevision: target.sourceRevision, ...(target.npm ? { npm: target.npm } : {}),
|
|
818
|
+
...(switching ? { channel_switch: true, backup_dir: stores } : {}), message: `Updated Talaria Web to ${target.label}.${stores ? ` Settings were backed up to ${stores}.` : ''}` };
|
|
613
819
|
}
|
|
614
820
|
/** Python `apply_web_update`: fast-forward a recognized clean checkout to main or a published Stable tag. */
|
|
615
|
-
export async function applyWebUpdate(webRoot, channel, git, getJson, id, build = runNpm, npmRun = runPackageNpm, canApply = () => true) {
|
|
821
|
+
export async function applyWebUpdate(webRoot, channel, git, getJson, id, build = runNpm, npmRun = runPackageNpm, canApply = () => true, packaged = {}) {
|
|
616
822
|
const root = await checkoutRoot(webRoot, git);
|
|
617
823
|
if (root === null)
|
|
618
|
-
return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply);
|
|
824
|
+
return applyNpmWebUpdate(webRoot, channel, getJson, id, npmRun, canApply, packaged);
|
|
619
825
|
const status = await git(['status', '--porcelain', '--untracked-files=all'], root);
|
|
620
826
|
if (!status.ok || status.out)
|
|
621
827
|
return { ok: false, dirty: true, message: 'Web update refused: the checkout must be clean, including untracked files.' };
|
|
@@ -739,6 +945,12 @@ async function currentReleaseTag(path, git) {
|
|
|
739
945
|
const out = await git(['describe', '--tags', '--exact-match', '--match', AGENT_TAG_GLOB, 'HEAD'], path);
|
|
740
946
|
return out.ok && out.out ? out.out : null;
|
|
741
947
|
}
|
|
948
|
+
async function verifiedAgentIdentity(path, expectedRevision, git) {
|
|
949
|
+
const head = await git(['rev-parse', 'HEAD'], path);
|
|
950
|
+
if (!head.ok || head.out !== expectedRevision || !SHA.test(head.out))
|
|
951
|
+
return null;
|
|
952
|
+
return { verified_revision: head.out, verified_version: await currentReleaseTag(path, git) };
|
|
953
|
+
}
|
|
742
954
|
const headContainsRef = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', ref, 'HEAD'], path)).ok;
|
|
743
955
|
const canFastForwardTo = async (path, ref, git) => (await git(['merge-base', '--is-ancestor', 'HEAD', ref], path)).ok;
|
|
744
956
|
async function detectDefaultBranch(path, git) {
|
|
@@ -795,7 +1007,7 @@ async function isDirty(path, git) {
|
|
|
795
1007
|
export async function checkAgentUpdate(path, git, channel = DEFAULT_CHANNEL) {
|
|
796
1008
|
if (!path || !existsSync(join(path, '.git')))
|
|
797
1009
|
return { name: 'agent', behind: null, no_git: true };
|
|
798
|
-
const fetched = await git
|
|
1010
|
+
const fetched = await fetchWithRetry(git, ['fetch', 'origin', '--tags', '--force'], path, 15_000);
|
|
799
1011
|
if (!fetched.ok) {
|
|
800
1012
|
const message = fetched.out ? `fetch failed: ${sanitizeGitDiagnostic(fetched.out)}` : 'fetch failed';
|
|
801
1013
|
return { name: 'agent', channel, behind: null, error: message, stale_check: true, dirty: await isDirty(path, git) };
|
|
@@ -824,8 +1036,12 @@ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, pol
|
|
|
824
1036
|
catch (error) {
|
|
825
1037
|
return { ok: false, message: error.message };
|
|
826
1038
|
}
|
|
827
|
-
if (info.current_sha === info.latest_sha)
|
|
828
|
-
|
|
1039
|
+
if (info.current_sha === info.latest_sha) {
|
|
1040
|
+
const verified = await verifiedAgentIdentity(path, str(info.latest_sha), git);
|
|
1041
|
+
if (!verified)
|
|
1042
|
+
return { ok: false, message: 'The installed Agent revision could not be verified.' };
|
|
1043
|
+
return { ok: true, up_to_date: true, target: 'agent', message: 'Agent is up to date.', ...verified };
|
|
1044
|
+
}
|
|
829
1045
|
if (info.manual_update || info.error)
|
|
830
1046
|
return { ...info, ok: false, message: info.message ?? info.error };
|
|
831
1047
|
const warning = confirmAgent(info, policy);
|
|
@@ -864,7 +1080,10 @@ export async function applyAgentUpdate(path, git, channel = DEFAULT_CHANNEL, pol
|
|
|
864
1080
|
if (!popped.ok)
|
|
865
1081
|
message += '. Local changes remain in `git stash list`; resolve them manually.';
|
|
866
1082
|
}
|
|
867
|
-
|
|
1083
|
+
const verified = await verifiedAgentIdentity(path, revision, git);
|
|
1084
|
+
if (!verified)
|
|
1085
|
+
return { ok: false, message: 'The Agent update completed, but the installed revision could not be verified.', target: 'agent' };
|
|
1086
|
+
return { ok: true, message, target: 'agent', ref, ...verified };
|
|
868
1087
|
}
|
|
869
1088
|
async function restoreStash(path, git, pullOut) {
|
|
870
1089
|
if ((await git(['stash', 'pop'], path)).ok)
|
|
@@ -903,7 +1122,10 @@ export async function forceAgentUpdate(path, git, log, channel = DEFAULT_CHANNEL
|
|
|
903
1122
|
log(`[updates] force update: git clean -fd failed (continuing to reset --hard): ${cleaned.out}`);
|
|
904
1123
|
if (!(await git(['reset', '--hard', revision], path)).ok)
|
|
905
1124
|
return { ok: false, message: `Force reset to ${ref} failed` };
|
|
906
|
-
|
|
1125
|
+
const verified = await verifiedAgentIdentity(path, revision, git);
|
|
1126
|
+
if (!verified)
|
|
1127
|
+
return { ok: false, message: 'The Agent force update completed, but the installed revision could not be verified.', target: 'agent' };
|
|
1128
|
+
return { ok: true, message: `agent force-updated to ${ref}`, target: 'agent', ref, ...verified };
|
|
907
1129
|
}
|
|
908
1130
|
/** Python `_inventory_locks`: report `.git/**\/*.lock` without touching any of them. */
|
|
909
1131
|
export function inventoryLocks(path) {
|
|
@@ -1058,6 +1280,7 @@ export class UpdateService {
|
|
|
1058
1280
|
cache = { webui: null, agent: null, checked_at: 0, include_agent: true, channel: DEFAULT_CHANNEL, agent_channel: DEFAULT_CHANNEL };
|
|
1059
1281
|
checking = null;
|
|
1060
1282
|
checkingKey = null;
|
|
1283
|
+
lastGood = new Map();
|
|
1061
1284
|
applying = false;
|
|
1062
1285
|
autoTimer = null;
|
|
1063
1286
|
autoStarted = false;
|
|
@@ -1117,8 +1340,11 @@ export class UpdateService {
|
|
|
1117
1340
|
this.checkingKey = key;
|
|
1118
1341
|
this.checking = (async () => {
|
|
1119
1342
|
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
|
-
|
|
1343
|
+
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));
|
|
1344
|
+
// The check may have fetched release tags the startup `git describe` never saw.
|
|
1345
|
+
if (this.deps.refreshWebuiVersion)
|
|
1346
|
+
webui.current_version = this.deps.refreshWebuiVersion();
|
|
1347
|
+
const agent = includeAgent ? this.keepLastGood('agent', agentChannel, await checkAgentUpdate(this.deps.agentDir(), this.git, agentChannel)) : ignoredAgent();
|
|
1122
1348
|
if (agent.latest_sha)
|
|
1123
1349
|
Object.assign(agent, agentWarning(agent, this.agentPolicy()));
|
|
1124
1350
|
Object.assign(this.cache, { webui, agent, checked_at: this.now(), include_agent: includeAgent, channel, agent_channel: agentChannel });
|
|
@@ -1131,6 +1357,18 @@ export class UpdateService {
|
|
|
1131
1357
|
})();
|
|
1132
1358
|
return this.checking;
|
|
1133
1359
|
}
|
|
1360
|
+
/** 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. */
|
|
1361
|
+
keepLastGood(target, channel, result) {
|
|
1362
|
+
const key = `${target}:${channel}`;
|
|
1363
|
+
if (!result.stale_check) {
|
|
1364
|
+
if (!result.error)
|
|
1365
|
+
this.lastGood.set(key, { at: this.now(), result });
|
|
1366
|
+
return result;
|
|
1367
|
+
}
|
|
1368
|
+
this.deps.log(`[updates] ${target} fetch failed: ${str(result.error)}`);
|
|
1369
|
+
const prior = this.lastGood.get(key);
|
|
1370
|
+
return prior && this.now() - prior.at < CACHE_TTL_S ? { ...prior.result, stale_check: true } : result;
|
|
1371
|
+
}
|
|
1134
1372
|
startAutoApply(intervalMs = AUTO_UPDATE_INTERVAL_MS) {
|
|
1135
1373
|
if (this.autoStarted || this.autoRestartScheduled)
|
|
1136
1374
|
return;
|
|
@@ -1155,11 +1393,19 @@ export class UpdateService {
|
|
|
1155
1393
|
}
|
|
1156
1394
|
/** Shared turn admission stays closed from update start through the supervisor restart. */
|
|
1157
1395
|
blocksNewWork() { return this.applying || this.autoRestartScheduled; }
|
|
1396
|
+
pendingRestartResponse(target) {
|
|
1397
|
+
if (!this.autoRestartScheduled)
|
|
1398
|
+
return null;
|
|
1399
|
+
if (target === 'webui')
|
|
1400
|
+
return { ok: true, target, restart_scheduled: true, message: 'A Web restart is already scheduled.' };
|
|
1401
|
+
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.' };
|
|
1402
|
+
}
|
|
1158
1403
|
async autoApplyOnce() {
|
|
1159
1404
|
if (this.autoRunning || this.applying || this.autoRestartScheduled || !(this.deps.checkEnabled?.() ?? this.deps.autoApply?.()))
|
|
1160
1405
|
return null;
|
|
1161
1406
|
this.autoRunning = true;
|
|
1162
1407
|
const lifecycle = this.lifecycle;
|
|
1408
|
+
let notificationId = null;
|
|
1163
1409
|
try {
|
|
1164
1410
|
const channel = this.deps.channel();
|
|
1165
1411
|
const stillEnabled = () => lifecycle === this.lifecycle && Boolean(this.deps.autoApply?.()) && (this.deps.checkEnabled?.() ?? true) && channel === this.deps.channel();
|
|
@@ -1173,13 +1419,26 @@ export class UpdateService {
|
|
|
1173
1419
|
}
|
|
1174
1420
|
if (!(Number(web.behind) > 0 || web.metadata_repair === true))
|
|
1175
1421
|
return web;
|
|
1422
|
+
notificationId = this.deps.autoNotification?.begin() ?? null;
|
|
1176
1423
|
const result = await this.apply('webui', channel, stillEnabled);
|
|
1424
|
+
if (notificationId) {
|
|
1425
|
+
if (result.restart_blocked === true)
|
|
1426
|
+
this.deps.autoNotification?.transition(notificationId, 'blocked');
|
|
1427
|
+
else if (result.ok !== true)
|
|
1428
|
+
this.deps.autoNotification?.transition(notificationId, 'failed');
|
|
1429
|
+
else if (result.restart_scheduled === true)
|
|
1430
|
+
this.deps.autoNotification?.transition(notificationId, 'restarting', str(result.sourceRevision || result.candidate_revision));
|
|
1431
|
+
else
|
|
1432
|
+
this.deps.autoNotification?.transition(notificationId, 'succeeded');
|
|
1433
|
+
}
|
|
1177
1434
|
this.deps.log(`[updates] automatic Web update: ${str(result.message || (result.ok ? 'applied' : 'failed'))}`);
|
|
1178
1435
|
if (!result.ok)
|
|
1179
1436
|
this.cache.webui = { ...web, message: result.message, error: result.restart_blocked ? undefined : result.message };
|
|
1180
1437
|
return result;
|
|
1181
1438
|
}
|
|
1182
1439
|
catch (error) {
|
|
1440
|
+
if (notificationId)
|
|
1441
|
+
this.deps.autoNotification?.transition(notificationId, 'failed');
|
|
1183
1442
|
this.deps.log(`[updates] automatic Web update failed: ${error.message}`);
|
|
1184
1443
|
return { ok: false, error: error.message };
|
|
1185
1444
|
}
|
|
@@ -1217,8 +1476,9 @@ export class UpdateService {
|
|
|
1217
1476
|
async apply(target, channel, canApply = () => true, agentOptions = {}) {
|
|
1218
1477
|
if (this.checking)
|
|
1219
1478
|
await this.checking;
|
|
1220
|
-
|
|
1221
|
-
|
|
1479
|
+
const pendingRestart = this.pendingRestartResponse(target);
|
|
1480
|
+
if (pendingRestart)
|
|
1481
|
+
return pendingRestart;
|
|
1222
1482
|
if (!canApply())
|
|
1223
1483
|
return { ok: false, message: 'Web update settings changed; update deferred.' };
|
|
1224
1484
|
const blocked = this.blockedResponse(target);
|
|
@@ -1230,7 +1490,7 @@ export class UpdateService {
|
|
|
1230
1490
|
async applyInner(target, channel, canApply = () => true, agentOptions = {}) {
|
|
1231
1491
|
if (target === 'webui') {
|
|
1232
1492
|
const lifecycle = this.lifecycle;
|
|
1233
|
-
const result = await applyWebUpdate(this.deps.webRoot, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.build ?? runNpm, this.deps.npm ?? runPackageNpm, canApply);
|
|
1493
|
+
const result = await applyWebUpdate(this.deps.webRoot, channel, this.git, this.deps.getJson, this.deps.identity, this.deps.build ?? runNpm, this.deps.npm ?? runPackageNpm, canApply, { registry: this.deps.experimental, stateDir: this.deps.stateDir });
|
|
1234
1494
|
// Settings can cancel before mutation, but a committed replacement must finish restarting unless shutting down.
|
|
1235
1495
|
if (result.ok && !result.up_to_date && lifecycle === this.lifecycle) {
|
|
1236
1496
|
this.cache.checked_at = 0;
|
|
@@ -1286,6 +1546,9 @@ export class UpdateService {
|
|
|
1286
1546
|
await this.checking;
|
|
1287
1547
|
if (target === 'webui')
|
|
1288
1548
|
return this.apply(target, channel);
|
|
1549
|
+
const pendingRestart = this.pendingRestartResponse(target);
|
|
1550
|
+
if (pendingRestart)
|
|
1551
|
+
return pendingRestart;
|
|
1289
1552
|
const blocked = this.blockedResponse(target);
|
|
1290
1553
|
if (blocked)
|
|
1291
1554
|
return Promise.resolve(blocked);
|
|
@@ -1300,6 +1563,9 @@ export class UpdateService {
|
|
|
1300
1563
|
clearLock(target, agentOptions = {}) {
|
|
1301
1564
|
if (target === 'webui')
|
|
1302
1565
|
return this.apply(target).then((r) => ({ ...r, lock_recovery: { action: 'retry-only' } }));
|
|
1566
|
+
const pendingRestart = this.pendingRestartResponse(target);
|
|
1567
|
+
if (pendingRestart)
|
|
1568
|
+
return Promise.resolve(pendingRestart);
|
|
1303
1569
|
const blocked = this.blockedResponse(target);
|
|
1304
1570
|
if (blocked)
|
|
1305
1571
|
return Promise.resolve(blocked);
|