@loicngr/kobo 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-server/kobo-tasks-handlers.js +8 -1
- package/dist/server/routes/health.js +12 -6
- package/dist/server/routes/settings.js +16 -0
- package/dist/server/routes/workspaces.js +206 -3
- package/dist/server/services/agent/engines/claude-code/engine.js +6 -0
- package/dist/server/services/agent/engines/claude-code/resolve-binary.js +50 -0
- package/dist/server/services/agent/orchestrator.js +17 -5
- package/dist/server/services/auto-loop-service.js +7 -1
- package/dist/server/services/dev-server-service.js +2 -2
- package/dist/server/services/initial-prompt-template-service.js +48 -0
- package/dist/server/services/pr-watcher-service.js +63 -13
- package/dist/server/services/review-template-service.js +58 -0
- package/dist/server/services/settings-service.js +94 -2
- package/dist/server/services/wakeup-service.js +9 -1
- package/dist/server/services/workspace-service.js +21 -0
- package/dist/server/services/worktree-service.js +2 -2
- package/dist/server/utils/git-ops.js +94 -4
- package/dist/server/utils/project-slug.js +52 -0
- package/dist/server/utils/worktree-paths.js +12 -10
- package/package.json +1 -1
- package/src/client/dist/spa/assets/ActivityFeed-CKSqMR2v.js +7 -0
- package/src/client/dist/spa/assets/{ActivityFeed-LXnbg3ff.css → ActivityFeed-CroojlsI.css} +1 -1
- package/src/client/dist/spa/assets/ClosePopup-D_UAdwkA.js +1 -0
- package/src/client/dist/spa/assets/CreatePage-7cP4h19f.js +2 -0
- package/src/client/dist/spa/assets/CreatePage-DssmsAsV.css +1 -0
- package/src/client/dist/spa/assets/DiffViewer-CdamEwIg.js +7 -0
- package/src/client/dist/spa/assets/{DiffViewer-D1Sdu307.css → DiffViewer-wFfQ9tcY.css} +1 -1
- package/src/client/dist/spa/assets/{HealthPage-CKyf7ky6.js → HealthPage-m4z-x5bo.js} +1 -1
- package/src/client/dist/spa/assets/MainLayout-CQBqYFNx.js +37 -0
- package/src/client/dist/spa/assets/MainLayout-DKurmqtk.css +1 -0
- package/src/client/dist/spa/assets/{QBadge-fsQ2AokU.js → QBadge-DWH42dbo.js} +1 -1
- package/src/client/dist/spa/assets/{QBtn-DHwAb18J.js → QBtn-a6jxWjmW.js} +1 -1
- package/src/client/dist/spa/assets/{QCheckbox-CcY7ZSk9.js → QCheckbox-D5jfsxLV.js} +1 -1
- package/src/client/dist/spa/assets/{QChip-BhT0W2Dg.js → QChip-ByxK0Tuf.js} +1 -1
- package/src/client/dist/spa/assets/QExpansionItem-CH1ipL9n.js +1 -0
- package/src/client/dist/spa/assets/{QIcon-B0-pH3Qs.js → QIcon-BJuyqdsT.js} +1 -1
- package/src/client/dist/spa/assets/QInput-Cm5-AGQ4.js +1 -0
- package/src/client/dist/spa/assets/{QItemLabel-DWwenW2S.js → QItemLabel-DrTxqTqV.js} +1 -1
- package/src/client/dist/spa/assets/{QItemSection-KFAnxzMK.js → QItemSection-5YpFpPDm.js} +1 -1
- package/src/client/dist/spa/assets/{QList-NmIE6Rd9.js → QList-D0FtnQJI.js} +1 -1
- package/src/client/dist/spa/assets/QMenu-B4xMxMGd.js +1 -0
- package/src/client/dist/spa/assets/QPage-ChUKoaKe.js +1 -0
- package/src/client/dist/spa/assets/{QRadio-DaZhdLCg.js → QRadio-B3aKjCVu.js} +1 -1
- package/src/client/dist/spa/assets/{QSpace-COlmM_4F.js → QSpace-CLtL3aPy.js} +1 -1
- package/src/client/dist/spa/assets/{QSpinnerDots-DwtnRN2r.js → QSpinnerDots-CszPQQ9J.js} +1 -1
- package/src/client/dist/spa/assets/QTabPanels-D2ks0UIA.js +1 -0
- package/src/client/dist/spa/assets/{QToggle-CGpiJLDJ.js → QToggle-1-N9qWq4.js} +1 -1
- package/src/client/dist/spa/assets/QTooltip-fDNzBEfN.js +1 -0
- package/src/client/dist/spa/assets/{SearchPage-Ce8Uc7Ol.js → SearchPage-DCRSQycR.js} +1 -1
- package/src/client/dist/spa/assets/SettingsPage-CMyeQ9_u.css +1 -0
- package/src/client/dist/spa/assets/SettingsPage-DStBGwIj.js +1 -0
- package/src/client/dist/spa/assets/TouchPan-DoE24Io3.js +1 -0
- package/src/client/dist/spa/assets/WorkspacePage-BstBxgN8.js +4 -0
- package/src/client/dist/spa/assets/{WorkspacePage-DQxGe62K.css → WorkspacePage-eymEd4kx.css} +1 -1
- package/src/client/dist/spa/assets/{build-path-tree-DRViYT3t.js → build-path-tree-B1Lvvqto.js} +1 -1
- package/src/client/dist/spa/assets/{cssMode-uAfRqG2Q.js → cssMode-o7NS-Oil.js} +1 -1
- package/src/client/dist/spa/assets/{documents-D6A3wRry.js → documents-kx0vLfSG.js} +1 -1
- package/src/client/dist/spa/assets/{editor.api-5GUlxvcL.js → editor.api-CNo9KwlJ.js} +1 -1
- package/src/client/dist/spa/assets/{editor.main-CSTJjBIa.js → editor.main-UyvgnhP6.js} +3 -3
- package/src/client/dist/spa/assets/expand-template-DqZgks9E.js +1 -0
- package/src/client/dist/spa/assets/{formatters-ejxELb0M.js → formatters-BD0_hovB.js} +1 -1
- package/src/client/dist/spa/assets/{freemarker2-BxBnI8Nb.js → freemarker2-BKWtNRQ9.js} +1 -1
- package/src/client/dist/spa/assets/{handlebars-DrbIsXmT.js → handlebars-BUhKrn3k.js} +1 -1
- package/src/client/dist/spa/assets/{html-DH7u_g5l.js → html-CrcvRgdj.js} +1 -1
- package/src/client/dist/spa/assets/{htmlMode-BlY9QO3f.js → htmlMode-Djjp-0pZ.js} +1 -1
- package/src/client/dist/spa/assets/i18n-DD341qPX.js +1 -0
- package/src/client/dist/spa/assets/index-DR1y9t94.js +2 -0
- package/src/client/dist/spa/assets/{javascript-B-AL31ke.js → javascript-DN_zCJwt.js} +1 -1
- package/src/client/dist/spa/assets/{jsonMode-Dx7CA4ag.js → jsonMode-B7uIpwZ9.js} +1 -1
- package/src/client/dist/spa/assets/{liquid--H7Vomnm.js → liquid-f3BGSOBM.js} +1 -1
- package/src/client/dist/spa/assets/{mdx-BOackeU6.js → mdx-jpEqsFXp.js} +1 -1
- package/src/client/dist/spa/assets/models-Bj-hfPO2.js +1 -0
- package/src/client/dist/spa/assets/{monaco.contribution-ydrMjZwK.js → monaco.contribution-D-UK6jlz.js} +2 -2
- package/src/client/dist/spa/assets/notifications-OnPq4FrH.js +1 -0
- package/src/client/dist/spa/assets/purify.es-DyEEb_DH.js +60 -0
- package/src/client/dist/spa/assets/{python-BWGSV-nk.js → python-CoiTKs0q.js} +1 -1
- package/src/client/dist/spa/assets/{razor-BGnl83cS.js → razor-BubwMw_m.js} +1 -1
- package/src/client/dist/spa/assets/render-chat-markdown-DwKtHD8J.js +1 -0
- package/src/client/dist/spa/assets/{tsMode-Chjqq1f3.js → tsMode-k_tAkDr_.js} +1 -1
- package/src/client/dist/spa/assets/{typescript-By7Y7PAP.js → typescript-DQQR6Y6R.js} +1 -1
- package/src/client/dist/spa/assets/use-checkbox-D7zmRxGI.js +1 -0
- package/src/client/dist/spa/assets/{use-id-CDuXkR0Z.js → use-id-CuaR1RiE.js} +1 -1
- package/src/client/dist/spa/assets/use-panel-D-8nAQns.js +1 -0
- package/src/client/dist/spa/assets/{xml-DoAeCRiy.js → xml-CaSyI8p6.js} +1 -1
- package/src/client/dist/spa/assets/{yaml-DlT7YOhG.js → yaml-BYsGcXIZ.js} +1 -1
- package/src/client/dist/spa/index.html +11 -13
- package/src/client/dist/spa/sounds/ca_va_peter.mp3 +0 -0
- package/src/client/dist/spa/sounds/dry-fart.mp3 +0 -0
- package/src/client/dist/spa/sounds/faaah.mp3 +0 -0
- package/src/client/dist/spa/sounds/for-shure.mp3 +0 -0
- package/src/client/dist/spa/sounds/travail_termine.mp3 +0 -0
- package/src/mcp-server/kobo-tasks-handlers.ts +10 -1
- package/src/client/dist/spa/assets/ActivityFeed-Chn8aZvi.js +0 -7
- package/src/client/dist/spa/assets/ClosePopup-BUlGXTqh.js +0 -1
- package/src/client/dist/spa/assets/CreatePage-BE3xfQsC.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-BGtqoZ8d.js +0 -2
- package/src/client/dist/spa/assets/DiffViewer-qjJ-biOw.js +0 -7
- package/src/client/dist/spa/assets/MainLayout-B07zv82Z.css +0 -1
- package/src/client/dist/spa/assets/MainLayout-Br3jmaOw.js +0 -37
- package/src/client/dist/spa/assets/QExpansionItem-BnIPCzXR.js +0 -1
- package/src/client/dist/spa/assets/QInput-D4WJro4e.js +0 -1
- package/src/client/dist/spa/assets/QMenu-0LsqhRZT.js +0 -1
- package/src/client/dist/spa/assets/QPage-Cu7zkfc6.js +0 -1
- package/src/client/dist/spa/assets/QResizeObserver-Cf79V-VZ.js +0 -1
- package/src/client/dist/spa/assets/QScrollArea-BDCKOKuE.js +0 -1
- package/src/client/dist/spa/assets/QTabPanels-Ctnrqvp9.js +0 -1
- package/src/client/dist/spa/assets/QTooltip-B3CmRx4j.js +0 -1
- package/src/client/dist/spa/assets/SettingsPage-8N0X7B7o.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-BaaSJ3eJ.js +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-wZUUTDzp.js +0 -4
- package/src/client/dist/spa/assets/expand-template-zA3pTyIP.js +0 -1
- package/src/client/dist/spa/assets/i18n-B41j--A3.js +0 -1
- package/src/client/dist/spa/assets/index-DoYBJtQA.js +0 -2
- package/src/client/dist/spa/assets/is-BbsvEMaT.js +0 -1
- package/src/client/dist/spa/assets/marked.esm-DLCrAGtO.js +0 -60
- package/src/client/dist/spa/assets/models-BPfFBcxr.js +0 -1
- package/src/client/dist/spa/assets/settings-lT4GB-uB.js +0 -1
- package/src/client/dist/spa/assets/symbols-BVRrMH2r.js +0 -1
- package/src/client/dist/spa/assets/use-checkbox-DzHmcu7s.js +0 -1
- package/src/client/dist/spa/assets/use-panel-DWX2aNMM.js +0 -1
- /package/src/client/dist/spa/assets/{_plugin-vue_export-helper-B8bB5DBd.js → _plugin-vue_export-helper-Cj6tcsj6.js} +0 -0
- /package/src/client/dist/spa/assets/{abap-DzK-OTGh.js → abap-DiwvWnMr.js} +0 -0
- /package/src/client/dist/spa/assets/{apex-Bj60_dRt.js → apex-CmtZjKlf.js} +0 -0
- /package/src/client/dist/spa/assets/{azcli-B6NwaBAZ.js → azcli-DL2My_i-.js} +0 -0
- /package/src/client/dist/spa/assets/{bat-bf7wXV68.js → bat-B-nC98wG.js} +0 -0
- /package/src/client/dist/spa/assets/{bicep-C_bg8UgA.js → bicep-Ju5MwOgh.js} +0 -0
- /package/src/client/dist/spa/assets/{cameligo-CTWw4D4B.js → cameligo-8Eu1TyBr.js} +0 -0
- /package/src/client/dist/spa/assets/{clojure-CgdPoH0r.js → clojure-u-RpMkH3.js} +0 -0
- /package/src/client/dist/spa/assets/{coffee-gHQfdA5M.js → coffee-CdA7bbTe.js} +0 -0
- /package/src/client/dist/spa/assets/{cpp-BM4Jj4aW.js → cpp-CzNFP8ks.js} +0 -0
- /package/src/client/dist/spa/assets/{csharp-D8-bh4Cd.js → csharp-j1LThmcE.js} +0 -0
- /package/src/client/dist/spa/assets/{csp-CXBxRx0n.js → csp-CLRC61y6.js} +0 -0
- /package/src/client/dist/spa/assets/{css-DKjIxrmY.js → css-r6rC_7P2.js} +0 -0
- /package/src/client/dist/spa/assets/{cypher-C5e5inIh.js → cypher-CW08XVUh.js} +0 -0
- /package/src/client/dist/spa/assets/{dart-BhRHHm4x.js → dart-Cs9aL5T_.js} +0 -0
- /package/src/client/dist/spa/assets/{dockerfile-DW5REF8E.js → dockerfile-BWM0M184.js} +0 -0
- /package/src/client/dist/spa/assets/{ecl-Bw4Hg3n_.js → ecl-MJJuer5P.js} +0 -0
- /package/src/client/dist/spa/assets/{elixir-DHmoBvpZ.js → elixir-D2AIuXqn.js} +0 -0
- /package/src/client/dist/spa/assets/{flow9-BsFExz3v.js → flow9-B2H24giC.js} +0 -0
- /package/src/client/dist/spa/assets/{fsharp-BaeLhgfq.js → fsharp-CMk2OIJN.js} +0 -0
- /package/src/client/dist/spa/assets/{go-Bd-NFKIC.js → go-BrMkuJg0.js} +0 -0
- /package/src/client/dist/spa/assets/{graphql-DZVerJfy.js → graphql-PSR1UKGv.js} +0 -0
- /package/src/client/dist/spa/assets/{hcl-CAVzrZfH.js → hcl-DAQrbDOW.js} +0 -0
- /package/src/client/dist/spa/assets/{ini-CyXdX58t.js → ini-0TG5BxW0.js} +0 -0
- /package/src/client/dist/spa/assets/{java-B5pNgvhy.js → java-rgorz17v.js} +0 -0
- /package/src/client/dist/spa/assets/{julia-XRhmV3AN.js → julia-C8VMdHm8.js} +0 -0
- /package/src/client/dist/spa/assets/{kobo-commands-DiUm1Y34.js → kobo-commands-w8VepGvD.js} +0 -0
- /package/src/client/dist/spa/assets/{kotlin-DOd3J5vr.js → kotlin-CllWo3gX.js} +0 -0
- /package/src/client/dist/spa/assets/{less-veZSnyw6.js → less-Cgca25AP.js} +0 -0
- /package/src/client/dist/spa/assets/{lexon-QWGkuK0H.js → lexon-D0GHdBaw.js} +0 -0
- /package/src/client/dist/spa/assets/{lua-CYGpjuO5.js → lua-DmRsNG-P.js} +0 -0
- /package/src/client/dist/spa/assets/{m3-yNnrZkdc.js → m3-BgL5dNKT.js} +0 -0
- /package/src/client/dist/spa/assets/{markdown-BCSWEPSX.js → markdown-BuJfycGS.js} +0 -0
- /package/src/client/dist/spa/assets/{mips-OpYmcC30.js → mips-C9m_93PR.js} +0 -0
- /package/src/client/dist/spa/assets/{msdax-2oxoTO9Z.js → msdax-CpFHC9OI.js} +0 -0
- /package/src/client/dist/spa/assets/{mysql-5KlC-K_9.js → mysql-qFvltsqN.js} +0 -0
- /package/src/client/dist/spa/assets/{objective-c-CcDCgtLx.js → objective-c-Bnmr858J.js} +0 -0
- /package/src/client/dist/spa/assets/{pascal-BZGsbaEV.js → pascal-WP0_D5AO.js} +0 -0
- /package/src/client/dist/spa/assets/{pascaligo-DtD5qU3G.js → pascaligo-Blom4Rij.js} +0 -0
- /package/src/client/dist/spa/assets/{perl-C1jNNS3E.js → perl-B-vk8g64.js} +0 -0
- /package/src/client/dist/spa/assets/{pgsql-CT0fhiZa.js → pgsql-Cgvz6v67.js} +0 -0
- /package/src/client/dist/spa/assets/{php-D6DrXoPM.js → php-8a3Lrw9m.js} +0 -0
- /package/src/client/dist/spa/assets/{pla-b3-HN2pF.js → pla-DuFqEZ8V.js} +0 -0
- /package/src/client/dist/spa/assets/{postiats-Bin2ApVS.js → postiats-DkLtSgkp.js} +0 -0
- /package/src/client/dist/spa/assets/{powerquery-7ASnn-ZG.js → powerquery-BJ1aNepW.js} +0 -0
- /package/src/client/dist/spa/assets/{powershell-t4p7sU1H.js → powershell-rE98k687.js} +0 -0
- /package/src/client/dist/spa/assets/{protobuf-BUGeWa_j.js → protobuf-CUheFacr.js} +0 -0
- /package/src/client/dist/spa/assets/{pug-BuKcgC9s.js → pug-LDcAMD8w.js} +0 -0
- /package/src/client/dist/spa/assets/{qsharp-DSMtI_O7.js → qsharp-DUKSQoR1.js} +0 -0
- /package/src/client/dist/spa/assets/{r-DMlFgn7A.js → r-D-QApv87.js} +0 -0
- /package/src/client/dist/spa/assets/{redis-cXItkC5u.js → redis-SXdDyWR9.js} +0 -0
- /package/src/client/dist/spa/assets/{redshift-BZVbW7HE.js → redshift-Y6lsCryn.js} +0 -0
- /package/src/client/dist/spa/assets/{restructuredtext-BzjxwS8h.js → restructuredtext-edObr9a8.js} +0 -0
- /package/src/client/dist/spa/assets/{ruby-C5nyLV4l.js → ruby-CNnUfF-8.js} +0 -0
- /package/src/client/dist/spa/assets/{rust-BcmMsHdf.js → rust-IHUZWzBr.js} +0 -0
- /package/src/client/dist/spa/assets/{sb-Dnb1iy6B.js → sb-DrUvY44N.js} +0 -0
- /package/src/client/dist/spa/assets/{scala-anMIFYpA.js → scala-B4hbXGLM.js} +0 -0
- /package/src/client/dist/spa/assets/{scheme-BItQTe08.js → scheme-BGrd12j3.js} +0 -0
- /package/src/client/dist/spa/assets/{scss-BOv51BJ5.js → scss-x5G1ES4U.js} +0 -0
- /package/src/client/dist/spa/assets/{shell-BsRYRTNN.js → shell-DOehe2Y8.js} +0 -0
- /package/src/client/dist/spa/assets/{solidity-BtuLgGDx.js → solidity-BeRvcwWV.js} +0 -0
- /package/src/client/dist/spa/assets/{sophia-B0Vkc5MF.js → sophia-DZbkUNjy.js} +0 -0
- /package/src/client/dist/spa/assets/{sparql-B7lvkZQM.js → sparql-B7_oi5-h.js} +0 -0
- /package/src/client/dist/spa/assets/{sql-DvP5MpA3.js → sql-CTlsFWVE.js} +0 -0
- /package/src/client/dist/spa/assets/{st-GVUeyB3U.js → st-DJVEJdPE.js} +0 -0
- /package/src/client/dist/spa/assets/{swift-DSPIoCjm.js → swift-CwhT3fYa.js} +0 -0
- /package/src/client/dist/spa/assets/{systemverilog-Icj2-k23.js → systemverilog-BQN63pkN.js} +0 -0
- /package/src/client/dist/spa/assets/{tcl-Cd8KQcm-.js → tcl-DqwfpskA.js} +0 -0
- /package/src/client/dist/spa/assets/{touch-Co9pfjUU.js → touch-HRdTUO2o.js} +0 -0
- /package/src/client/dist/spa/assets/{twig-CBHmt8z3.js → twig-BiyenUgc.js} +0 -0
- /package/src/client/dist/spa/assets/{typespec-Ckc037mq.js → typespec-CWOJribt.js} +0 -0
- /package/src/client/dist/spa/assets/{use-quasar-Cc4smfg5.js → use-quasar-Sdcq6zzV.js} +0 -0
- /package/src/client/dist/spa/assets/{vb-B97GW9Wb.js → vb-Cq5F87m3.js} +0 -0
- /package/src/client/dist/spa/assets/{vue-i18n-eUDnMrPl.js → vue-i18n-BcfTCFFS.js} +0 -0
- /package/src/client/dist/spa/assets/{wgsl-DIKmb3YH.js → wgsl-BAvW2lVr.js} +0 -0
- /package/src/client/dist/spa/{notification.mp3 → sounds/hey.mp3} +0 -0
|
@@ -2,7 +2,7 @@ import { getPrStatusAsync } from '../utils/git-ops.js';
|
|
|
2
2
|
import { stopDevServer } from './dev-server-service.js';
|
|
3
3
|
import { destroyTerminal } from './terminal-service.js';
|
|
4
4
|
import { emitEphemeral } from './websocket-service.js';
|
|
5
|
-
import { archiveWorkspace, listWorkspaces } from './workspace-service.js';
|
|
5
|
+
import { archiveWorkspace, listWorkspaces, updateWorkspaceSourceBranch } from './workspace-service.js';
|
|
6
6
|
// ── PR Watcher ────────────────────────────────────────────────────────────────
|
|
7
7
|
// Polls GitHub every POLL_INTERVAL_MS to detect merged/closed PRs and
|
|
8
8
|
// automatically archive the corresponding workspace.
|
|
@@ -14,8 +14,7 @@ import { archiveWorkspace, listWorkspaces } from './workspace-service.js';
|
|
|
14
14
|
const POLL_INTERVAL_MS = 30 * 1000; // 30 seconds
|
|
15
15
|
let timer = null;
|
|
16
16
|
let checking = false;
|
|
17
|
-
|
|
18
|
-
const lastKnownState = new Map();
|
|
17
|
+
const lastKnownPr = new Map();
|
|
19
18
|
/**
|
|
20
19
|
* Read-only snapshot of PR states known to the watcher, keyed by workspace id.
|
|
21
20
|
* Used by the drawer to show a small PR-open indicator without N separate
|
|
@@ -25,17 +24,24 @@ const lastKnownState = new Map();
|
|
|
25
24
|
*/
|
|
26
25
|
export function getAllPrStates() {
|
|
27
26
|
const out = {};
|
|
28
|
-
for (const [id,
|
|
29
|
-
out[id] = state;
|
|
27
|
+
for (const [id, known] of lastKnownPr) {
|
|
28
|
+
out[id] = known.state;
|
|
30
29
|
}
|
|
31
30
|
return out;
|
|
32
31
|
}
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Test-only escape hatch — drops the in-memory cache so each test starts
|
|
34
|
+
* from a clean slate. Not part of the public API.
|
|
35
|
+
*/
|
|
36
|
+
export function _resetForTest() {
|
|
37
|
+
lastKnownPr.clear();
|
|
38
|
+
}
|
|
39
|
+
export async function checkPrStatuses() {
|
|
34
40
|
const workspaces = listWorkspaces(false); // non-archived only
|
|
35
41
|
// Clean up entries for workspaces that no longer exist
|
|
36
|
-
for (const id of
|
|
42
|
+
for (const id of lastKnownPr.keys()) {
|
|
37
43
|
if (!workspaces.some((ws) => ws.id === id)) {
|
|
38
|
-
|
|
44
|
+
lastKnownPr.delete(id);
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
for (const ws of workspaces) {
|
|
@@ -46,10 +52,15 @@ async function checkPrStatuses() {
|
|
|
46
52
|
const pr = await getPrStatusAsync(ws.projectPath, ws.workingBranch);
|
|
47
53
|
if (!pr)
|
|
48
54
|
continue;
|
|
49
|
-
const prev =
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
55
|
+
const prev = lastKnownPr.get(ws.id);
|
|
56
|
+
// We delay updating `lastKnownPr` until after the actions succeed.
|
|
57
|
+
// Setting it eagerly would poison the cache: if updateWorkspaceSourceBranch
|
|
58
|
+
// throws (transient DB issue, race with workspace deletion), the cache
|
|
59
|
+
// already holds the new base and the user never sees the toast — the
|
|
60
|
+
// next tick computes `prev.base === pr.base` and treats it as no-op.
|
|
61
|
+
// Archive on a transition FROM OPEN to CLOSED/MERGED. Skips the
|
|
62
|
+
// base-change detection below — archiving wins.
|
|
63
|
+
if (prev?.state === 'OPEN' && (pr.state === 'MERGED' || pr.state === 'CLOSED')) {
|
|
53
64
|
console.log(`[pr-watcher] PR ${pr.state.toLowerCase()} for workspace '${ws.name}' — archiving`);
|
|
54
65
|
// Best-effort cleanup (same as manual archive): stop dev server + terminal.
|
|
55
66
|
// Agent is already not running here (guarded above).
|
|
@@ -66,12 +77,51 @@ async function checkPrStatuses() {
|
|
|
66
77
|
// Terminal may not exist — ignore
|
|
67
78
|
}
|
|
68
79
|
archiveWorkspace(ws.id);
|
|
69
|
-
|
|
80
|
+
lastKnownPr.delete(ws.id);
|
|
70
81
|
emitEphemeral(ws.id, 'workspace:archived', {
|
|
71
82
|
reason: `PR ${pr.state.toLowerCase()}`,
|
|
72
83
|
prUrl: pr.url,
|
|
73
84
|
});
|
|
85
|
+
continue; // do not run base-change detection on a workspace we just archived
|
|
86
|
+
}
|
|
87
|
+
// Base-branch change detection. Only relevant for OPEN PRs — closed/
|
|
88
|
+
// merged PRs don't accept base changes. Skip if the GitHub response
|
|
89
|
+
// didn't include a baseRefName (defensive against malformed data).
|
|
90
|
+
if (pr.state !== 'OPEN' || !pr.base) {
|
|
91
|
+
// Still update the cache for the state — keeps the OPEN→CLOSED/MERGED
|
|
92
|
+
// archiving logic working on the next tick.
|
|
93
|
+
lastKnownPr.set(ws.id, { state: pr.state, base: prev?.base });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Comparison baseline:
|
|
97
|
+
// - If we've seen this workspace before, use the previous `base`.
|
|
98
|
+
// - Otherwise (first sight after boot/unarchive), compare with the
|
|
99
|
+
// `sourceBranch` recorded in the database — that catches base changes
|
|
100
|
+
// that happened while Kobo was offline.
|
|
101
|
+
const previousBase = prev?.base ?? ws.sourceBranch;
|
|
102
|
+
if (previousBase === pr.base) {
|
|
103
|
+
// No-op path: still record the base so subsequent ticks have a baseline.
|
|
104
|
+
lastKnownPr.set(ws.id, { state: pr.state, base: pr.base });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
console.log(`[pr-watcher] PR base changed for workspace '${ws.name}': ${previousBase} → ${pr.base}`);
|
|
108
|
+
try {
|
|
109
|
+
updateWorkspaceSourceBranch(ws.id, pr.base);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.error(`[pr-watcher] updateWorkspaceSourceBranch failed for '${ws.name}':`, err instanceof Error ? err.message : err);
|
|
113
|
+
// Don't poison the cache: leave the previous entry (or absence) so
|
|
114
|
+
// the next tick retries the detection.
|
|
115
|
+
continue;
|
|
74
116
|
}
|
|
117
|
+
// Both the persistence and the emit are part of "we successfully
|
|
118
|
+
// observed a base change" — only NOW commit the new state to the cache.
|
|
119
|
+
lastKnownPr.set(ws.id, { state: pr.state, base: pr.base });
|
|
120
|
+
emitEphemeral(ws.id, 'pr:base-changed', {
|
|
121
|
+
oldBase: previousBase,
|
|
122
|
+
newBase: pr.base,
|
|
123
|
+
prUrl: pr.url,
|
|
124
|
+
});
|
|
75
125
|
}
|
|
76
126
|
catch (err) {
|
|
77
127
|
console.error(`[pr-watcher] Failed to check PR for workspace '${ws.name}':`, err instanceof Error ? err.message : err);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export const DEFAULT_REVIEW_PROMPT_TEMPLATE = `You are reviewing code changes on workspace "{{workspace_name}}" in project {{project_name}}.
|
|
3
|
+
|
|
4
|
+
Branch: {{branch_name}} (base: {{source_branch}})
|
|
5
|
+
Base commit: {{base_commit}}
|
|
6
|
+
|
|
7
|
+
If a code-review skill is available (e.g. superpowers:requesting-code-review), invoke it to drive this review. Otherwise follow the steps below directly.
|
|
8
|
+
|
|
9
|
+
## Scope
|
|
10
|
+
|
|
11
|
+
Review ALL changes — both committed and uncommitted in the working tree:
|
|
12
|
+
- \`git diff {{base_commit}}..HEAD\` — committed changes on this branch
|
|
13
|
+
- \`git status\` and \`git diff\` — uncommitted changes (staged + unstaged)
|
|
14
|
+
|
|
15
|
+
## Diff summary
|
|
16
|
+
{{diff_stats}}
|
|
17
|
+
|
|
18
|
+
## Commits
|
|
19
|
+
{{commits}}
|
|
20
|
+
|
|
21
|
+
## Additional instructions
|
|
22
|
+
{{additional_instructions}}
|
|
23
|
+
|
|
24
|
+
## Output
|
|
25
|
+
|
|
26
|
+
If no review skill is available, structure your reply as:
|
|
27
|
+
1. Summary — what changed and why
|
|
28
|
+
2. Issues — bugs, regressions, security or perf concerns (with file:line)
|
|
29
|
+
3. Suggestions — refactor / improvement opportunities
|
|
30
|
+
4. Tests — coverage gaps
|
|
31
|
+
5. Verdict — ship / fix-then-ship / blocked
|
|
32
|
+
`;
|
|
33
|
+
function buildVariableMap(ctx) {
|
|
34
|
+
return {
|
|
35
|
+
project_name: path.basename(ctx.workspace.projectPath),
|
|
36
|
+
workspace_name: ctx.workspace.name,
|
|
37
|
+
branch_name: ctx.workspace.workingBranch,
|
|
38
|
+
source_branch: ctx.workspace.sourceBranch,
|
|
39
|
+
base_commit: ctx.baseCommit,
|
|
40
|
+
commits: ctx.commits,
|
|
41
|
+
diff_stats: ctx.diffStats,
|
|
42
|
+
notion_url: ctx.workspace.notionUrl ?? '',
|
|
43
|
+
additional_instructions: ctx.additionalInstructions.length > 0 ? ctx.additionalInstructions : '(none)',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Render a review prompt template by substituting {{variable}} placeholders.
|
|
48
|
+
* Pure: no I/O, no side effects. Unknown variables are left intact.
|
|
49
|
+
*/
|
|
50
|
+
export function renderReviewTemplate(template, ctx) {
|
|
51
|
+
const vars = buildVariableMap(ctx);
|
|
52
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, name) => {
|
|
53
|
+
if (Object.hasOwn(vars, name)) {
|
|
54
|
+
return vars[name];
|
|
55
|
+
}
|
|
56
|
+
return match;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -4,7 +4,9 @@ import { WORKTREES_PATH } from '../../shared/consts.js';
|
|
|
4
4
|
import { listClaudeMcpEntries } from '../utils/mcp-client.js';
|
|
5
5
|
import { getSettingsPath } from '../utils/paths.js';
|
|
6
6
|
import { InvalidWorktreesPathError, resolveGlobalWorktreesRoot, sanitizeWorktreesPath, validateWorktreesPath, } from '../utils/worktree-paths.js';
|
|
7
|
-
|
|
7
|
+
import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT } from './initial-prompt-template-service.js';
|
|
8
|
+
import { DEFAULT_REVIEW_PROMPT_TEMPLATE } from './review-template-service.js';
|
|
9
|
+
export const DEFAULT_GIT_CONVENTIONS = `# Git conventions
|
|
8
10
|
|
|
9
11
|
## Commits
|
|
10
12
|
- Use Conventional Commits: \`type(scope): subject\`
|
|
@@ -29,7 +31,7 @@ const DEFAULT_GIT_CONVENTIONS = `# Git conventions
|
|
|
29
31
|
- Never skip hooks (--no-verify) unless the user explicitly asks
|
|
30
32
|
- Always inspect \`git status\` and \`git diff\` before staging
|
|
31
33
|
`;
|
|
32
|
-
const DEFAULT_PR_PROMPT_TEMPLATE = `A pull request has been opened: {{pr_url}} (#{{pr_number}})
|
|
34
|
+
export const DEFAULT_PR_PROMPT_TEMPLATE = `A pull request has been opened: {{pr_url}} (#{{pr_number}})
|
|
33
35
|
|
|
34
36
|
Context:
|
|
35
37
|
- Workspace: {{workspace_name}}
|
|
@@ -200,6 +202,68 @@ const settingsMigrations = [
|
|
|
200
202
|
global.worktreesPath = sanitizeWorktreesPath(global.worktreesPath);
|
|
201
203
|
},
|
|
202
204
|
},
|
|
205
|
+
{
|
|
206
|
+
version: 12,
|
|
207
|
+
name: 'add-audio-notification-sound',
|
|
208
|
+
migrate({ global }) {
|
|
209
|
+
if (typeof global.audioNotificationSound !== 'string' || global.audioNotificationSound.length === 0) {
|
|
210
|
+
global.audioNotificationSound = 'hey.mp3';
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
version: 13,
|
|
216
|
+
name: 'add-audio-notification-volume',
|
|
217
|
+
migrate({ global }) {
|
|
218
|
+
const v = global.audioNotificationVolume;
|
|
219
|
+
if (typeof v !== 'number' || Number.isNaN(v) || v < 0 || v > 1) {
|
|
220
|
+
global.audioNotificationVolume = 1;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
version: 14,
|
|
226
|
+
name: 'add-worktrees-prefix-by-project',
|
|
227
|
+
migrate({ global }) {
|
|
228
|
+
if (typeof global.worktreesPrefixByProject !== 'boolean') {
|
|
229
|
+
global.worktreesPrefixByProject = false;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
version: 15,
|
|
235
|
+
name: 'add-review-prompt-template',
|
|
236
|
+
migrate({ global, projects }) {
|
|
237
|
+
if (typeof global.reviewPromptTemplate !== 'string') {
|
|
238
|
+
global.reviewPromptTemplate = DEFAULT_REVIEW_PROMPT_TEMPLATE;
|
|
239
|
+
}
|
|
240
|
+
for (const p of projects) {
|
|
241
|
+
if (typeof p.reviewPromptTemplate !== 'string') {
|
|
242
|
+
p.reviewPromptTemplate = '';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
version: 16,
|
|
249
|
+
name: 'add-notion-sentry-initial-prompts',
|
|
250
|
+
migrate({ global, projects }) {
|
|
251
|
+
if (typeof global.notionInitialPromptTemplate !== 'string') {
|
|
252
|
+
global.notionInitialPromptTemplate = DEFAULT_NOTION_INITIAL_PROMPT;
|
|
253
|
+
}
|
|
254
|
+
if (typeof global.sentryInitialPromptTemplate !== 'string') {
|
|
255
|
+
global.sentryInitialPromptTemplate = DEFAULT_SENTRY_INITIAL_PROMPT;
|
|
256
|
+
}
|
|
257
|
+
for (const p of projects) {
|
|
258
|
+
if (typeof p.notionInitialPromptTemplate !== 'string') {
|
|
259
|
+
p.notionInitialPromptTemplate = '';
|
|
260
|
+
}
|
|
261
|
+
if (typeof p.sentryInitialPromptTemplate !== 'string') {
|
|
262
|
+
p.sentryInitialPromptTemplate = '';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
},
|
|
203
267
|
];
|
|
204
268
|
/** Current settings schema version — always equals the highest migration version. */
|
|
205
269
|
export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
|
|
@@ -230,10 +294,15 @@ function defaultSettings() {
|
|
|
230
294
|
defaultModel: 'claude-opus-4-7',
|
|
231
295
|
dangerouslySkipPermissions: true,
|
|
232
296
|
prPromptTemplate: DEFAULT_PR_PROMPT_TEMPLATE,
|
|
297
|
+
reviewPromptTemplate: DEFAULT_REVIEW_PROMPT_TEMPLATE,
|
|
298
|
+
notionInitialPromptTemplate: DEFAULT_NOTION_INITIAL_PROMPT,
|
|
299
|
+
sentryInitialPromptTemplate: DEFAULT_SENTRY_INITIAL_PROMPT,
|
|
233
300
|
gitConventions: DEFAULT_GIT_CONVENTIONS,
|
|
234
301
|
editorCommand: '',
|
|
235
302
|
browserNotifications: true,
|
|
236
303
|
audioNotifications: true,
|
|
304
|
+
audioNotificationSound: 'hey.mp3',
|
|
305
|
+
audioNotificationVolume: 1,
|
|
237
306
|
notionStatusProperty: '',
|
|
238
307
|
notionInProgressStatus: '',
|
|
239
308
|
defaultPermissionMode: 'plan',
|
|
@@ -241,6 +310,7 @@ function defaultSettings() {
|
|
|
241
310
|
sentryMcpKey: '',
|
|
242
311
|
tags: [...DEFAULT_WORKSPACE_TAGS],
|
|
243
312
|
worktreesPath: WORKTREES_PATH,
|
|
313
|
+
worktreesPrefixByProject: false,
|
|
244
314
|
},
|
|
245
315
|
projects: [],
|
|
246
316
|
};
|
|
@@ -253,6 +323,9 @@ function defaultProjectSettings(projectPath) {
|
|
|
253
323
|
defaultModel: '',
|
|
254
324
|
dangerouslySkipPermissions: true,
|
|
255
325
|
prPromptTemplate: '',
|
|
326
|
+
reviewPromptTemplate: '',
|
|
327
|
+
notionInitialPromptTemplate: '',
|
|
328
|
+
sentryInitialPromptTemplate: '',
|
|
256
329
|
gitConventions: '',
|
|
257
330
|
setupScript: '',
|
|
258
331
|
devServer: {
|
|
@@ -426,6 +499,9 @@ export function getEffectiveSettings(projectPath) {
|
|
|
426
499
|
model: settings.global.defaultModel,
|
|
427
500
|
dangerouslySkipPermissions: settings.global.dangerouslySkipPermissions,
|
|
428
501
|
prPromptTemplate: settings.global.prPromptTemplate,
|
|
502
|
+
reviewPromptTemplate: settings.global.reviewPromptTemplate,
|
|
503
|
+
notionInitialPromptTemplate: settings.global.notionInitialPromptTemplate,
|
|
504
|
+
sentryInitialPromptTemplate: settings.global.sentryInitialPromptTemplate,
|
|
429
505
|
gitConventions: settings.global.gitConventions,
|
|
430
506
|
sourceBranch: '',
|
|
431
507
|
devServer: null,
|
|
@@ -438,6 +514,9 @@ export function getEffectiveSettings(projectPath) {
|
|
|
438
514
|
model: project.defaultModel || settings.global.defaultModel,
|
|
439
515
|
dangerouslySkipPermissions: project.dangerouslySkipPermissions ?? settings.global.dangerouslySkipPermissions,
|
|
440
516
|
prPromptTemplate: project.prPromptTemplate || settings.global.prPromptTemplate,
|
|
517
|
+
reviewPromptTemplate: project.reviewPromptTemplate || settings.global.reviewPromptTemplate,
|
|
518
|
+
notionInitialPromptTemplate: project.notionInitialPromptTemplate || settings.global.notionInitialPromptTemplate,
|
|
519
|
+
sentryInitialPromptTemplate: project.sentryInitialPromptTemplate || settings.global.sentryInitialPromptTemplate,
|
|
441
520
|
gitConventions: project.gitConventions || settings.global.gitConventions,
|
|
442
521
|
sourceBranch: project.defaultSourceBranch,
|
|
443
522
|
devServer: project.devServer,
|
|
@@ -453,10 +532,15 @@ export function updateGlobalSettings(data) {
|
|
|
453
532
|
'defaultModel',
|
|
454
533
|
'dangerouslySkipPermissions',
|
|
455
534
|
'prPromptTemplate',
|
|
535
|
+
'reviewPromptTemplate',
|
|
536
|
+
'notionInitialPromptTemplate',
|
|
537
|
+
'sentryInitialPromptTemplate',
|
|
456
538
|
'gitConventions',
|
|
457
539
|
'editorCommand',
|
|
458
540
|
'browserNotifications',
|
|
459
541
|
'audioNotifications',
|
|
542
|
+
'audioNotificationSound',
|
|
543
|
+
'audioNotificationVolume',
|
|
460
544
|
'notionStatusProperty',
|
|
461
545
|
'notionInProgressStatus',
|
|
462
546
|
'defaultPermissionMode',
|
|
@@ -464,6 +548,7 @@ export function updateGlobalSettings(data) {
|
|
|
464
548
|
'sentryMcpKey',
|
|
465
549
|
'tags',
|
|
466
550
|
'worktreesPath',
|
|
551
|
+
'worktreesPrefixByProject',
|
|
467
552
|
];
|
|
468
553
|
const filtered = pickKnownKeys(data, allowedGlobalKeys);
|
|
469
554
|
if (filtered.tags !== undefined) {
|
|
@@ -473,6 +558,10 @@ export function updateGlobalSettings(data) {
|
|
|
473
558
|
.filter((t) => t.length > 0 && t.length <= 50)))
|
|
474
559
|
: settings.global.tags;
|
|
475
560
|
}
|
|
561
|
+
if (filtered.audioNotificationVolume !== undefined) {
|
|
562
|
+
const v = Number(filtered.audioNotificationVolume);
|
|
563
|
+
filtered.audioNotificationVolume = Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
|
|
564
|
+
}
|
|
476
565
|
if (filtered.worktreesPath !== undefined) {
|
|
477
566
|
filtered.worktreesPath = validateWorktreesPath(filtered.worktreesPath, { allowEmpty: false });
|
|
478
567
|
ensureGlobalWorktreesRootExists(filtered.worktreesPath);
|
|
@@ -505,6 +594,9 @@ export function upsertProject(projectPath, data) {
|
|
|
505
594
|
'defaultModel',
|
|
506
595
|
'dangerouslySkipPermissions',
|
|
507
596
|
'prPromptTemplate',
|
|
597
|
+
'reviewPromptTemplate',
|
|
598
|
+
'notionInitialPromptTemplate',
|
|
599
|
+
'sentryInitialPromptTemplate',
|
|
508
600
|
'gitConventions',
|
|
509
601
|
'setupScript',
|
|
510
602
|
'devServer',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getDb } from '../db/index.js';
|
|
2
|
+
import { slugifyProjectName } from '../utils/project-slug.js';
|
|
2
3
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
3
4
|
import * as orchestrator from './agent/orchestrator.js';
|
|
5
|
+
import * as settingsService from './settings-service.js';
|
|
4
6
|
import { emitEphemeral } from './websocket-service.js';
|
|
5
7
|
const MIN_DELAY_SECONDS = 60;
|
|
6
8
|
const MAX_DELAY_SECONDS = 3600;
|
|
@@ -132,7 +134,13 @@ function fire(workspaceId) {
|
|
|
132
134
|
emitEphemeral(workspaceId, 'wakeup:skipped', { reason: 'fire-failed' });
|
|
133
135
|
return;
|
|
134
136
|
}
|
|
135
|
-
const
|
|
137
|
+
const globalSettings = settingsService.getGlobalSettings();
|
|
138
|
+
const projectSettings = settingsService.getProjectSettings(wsRow.project_path);
|
|
139
|
+
const projectSlug = globalSettings.worktreesPrefixByProject
|
|
140
|
+
? slugifyProjectName(projectSettings?.displayName ?? '', wsRow.project_path)
|
|
141
|
+
: undefined;
|
|
142
|
+
const worktreePath = wsRow.worktree_path ??
|
|
143
|
+
resolveWorkspaceWorktreePath(wsRow.project_path, wsRow.working_branch, globalSettings.worktreesPath, projectSlug);
|
|
136
144
|
// Narrow against the four known values; unknowns → 'bypass'.
|
|
137
145
|
const stored = wsRow.agent_permission_mode;
|
|
138
146
|
const agentPermissionMode = stored === 'plan' || stored === 'strict' || stored === 'interactive' ? stored : 'bypass';
|
|
@@ -217,6 +217,27 @@ export function updateWorkingBranch(id, workingBranch) {
|
|
|
217
217
|
}
|
|
218
218
|
return getWorkspace(id);
|
|
219
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Update the source branch in the database. Called by the PR watcher when
|
|
222
|
+
* GitHub reports a different `baseRefName` for the workspace's PR.
|
|
223
|
+
* Does NOT touch the worktree — the user (or the agent) decides when to
|
|
224
|
+
* rebase the local branch onto the new base.
|
|
225
|
+
*/
|
|
226
|
+
export function updateWorkspaceSourceBranch(id, sourceBranch) {
|
|
227
|
+
const sanitized = sourceBranch.trim();
|
|
228
|
+
if (!sanitized) {
|
|
229
|
+
throw new Error('Source branch cannot be empty');
|
|
230
|
+
}
|
|
231
|
+
const db = getDb();
|
|
232
|
+
const now = new Date().toISOString();
|
|
233
|
+
const result = db
|
|
234
|
+
.prepare('UPDATE workspaces SET source_branch = ?, updated_at = ? WHERE id = ?')
|
|
235
|
+
.run(sanitized, now, id);
|
|
236
|
+
if (result.changes === 0) {
|
|
237
|
+
throw new Error(`Workspace '${id}' not found`);
|
|
238
|
+
}
|
|
239
|
+
return getWorkspace(id);
|
|
240
|
+
}
|
|
220
241
|
/** Update the on-disk worktree path. Used by rename / resync-branch on owned worktrees. */
|
|
221
242
|
export function updateWorktreePath(id, newPath) {
|
|
222
243
|
const db = getDb();
|
|
@@ -49,12 +49,12 @@ function removeFromExclude(projectPath, worktreePath) {
|
|
|
49
49
|
fs.writeFileSync(excludeFile, trimmed ? `${trimmed}\n` : '', 'utf-8');
|
|
50
50
|
}
|
|
51
51
|
/** Create a git worktree for the given branch. Returns the worktree path. */
|
|
52
|
-
export function createWorktree(projectPath, branchName, sourceBranch, worktreesPath) {
|
|
52
|
+
export function createWorktree(projectPath, branchName, sourceBranch, worktreesPath, projectSlug) {
|
|
53
53
|
const worktreesDir = resolveWorktreesRoot(projectPath, worktreesPath);
|
|
54
54
|
if (!fs.existsSync(worktreesDir)) {
|
|
55
55
|
fs.mkdirSync(worktreesDir, { recursive: true });
|
|
56
56
|
}
|
|
57
|
-
const worktreePath = resolveWorkspaceWorktreePath(projectPath, branchName, worktreesPath);
|
|
57
|
+
const worktreePath = resolveWorkspaceWorktreePath(projectPath, branchName, worktreesPath, projectSlug);
|
|
58
58
|
try {
|
|
59
59
|
// Use origin/<sourceBranch> as the base so the worktree starts from the
|
|
60
60
|
// freshly-fetched remote ref (fetchSourceBranch is always called first).
|
|
@@ -267,6 +267,22 @@ export function getCommitCount(repoPath, base, head) {
|
|
|
267
267
|
return 0;
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Count commits in `base` that are not in `head` — i.e. how far `head` lags
|
|
272
|
+
* behind `base`. Mirrors `getCommitCount` but in reverse direction.
|
|
273
|
+
* Returns 0 on failure.
|
|
274
|
+
*/
|
|
275
|
+
export function getCommitsBehind(repoPath, base, head) {
|
|
276
|
+
try {
|
|
277
|
+
const ref = resolveBase(repoPath, base);
|
|
278
|
+
const output = git(repoPath, ['rev-list', '--count', `${head}..${ref}`]);
|
|
279
|
+
const n = parseInt(output.trim(), 10);
|
|
280
|
+
return Number.isFinite(n) ? n : 0;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
270
286
|
/** Return structured diff shortstat between two refs (three-dot merge base). */
|
|
271
287
|
export function getStructuredDiffStatsBetween(repoPath, base, head) {
|
|
272
288
|
try {
|
|
@@ -340,6 +356,40 @@ export function listBranchCommits(repoPath, sourceBranch, workingBranch, limit =
|
|
|
340
356
|
}
|
|
341
357
|
return commits;
|
|
342
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* List commits on `sourceBranch` that are NOT yet on `workingBranch` —
|
|
361
|
+
* i.e. commits the working branch is "behind" by. Mirror of `listBranchCommits`
|
|
362
|
+
* in the opposite direction. Up to `limit` commits, most recent first.
|
|
363
|
+
*/
|
|
364
|
+
export function listCommitsBehind(repoPath, sourceBranch, workingBranch, limit = 50) {
|
|
365
|
+
const sourceRef = resolveBase(repoPath, sourceBranch);
|
|
366
|
+
const FORMAT = '--pretty=format:%H%x00%h%x00%s%x00%an%x00%aI';
|
|
367
|
+
let raw;
|
|
368
|
+
try {
|
|
369
|
+
raw = git(repoPath, ['log', `${workingBranch}..${sourceRef}`, `--max-count=${limit}`, FORMAT]);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
if (!raw)
|
|
375
|
+
return [];
|
|
376
|
+
const commits = [];
|
|
377
|
+
for (const line of raw.split('\n')) {
|
|
378
|
+
if (!line)
|
|
379
|
+
continue;
|
|
380
|
+
const [sha, shortSha, subject, author, date] = line.split('\x00');
|
|
381
|
+
if (!sha)
|
|
382
|
+
continue;
|
|
383
|
+
commits.push({
|
|
384
|
+
sha,
|
|
385
|
+
shortSha: shortSha ?? '',
|
|
386
|
+
subject: subject ?? '',
|
|
387
|
+
author: author ?? '',
|
|
388
|
+
date: date ?? '',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return commits;
|
|
392
|
+
}
|
|
343
393
|
/** Get the GitHub PR URL for a branch using `gh pr view`. Returns null if no PR exists. */
|
|
344
394
|
export function getPrUrl(repoPath, branchName) {
|
|
345
395
|
try {
|
|
@@ -355,14 +405,18 @@ export function getPrUrl(repoPath, branchName) {
|
|
|
355
405
|
/** Get the state and URL of the PR for a branch. Returns null if no PR exists. */
|
|
356
406
|
export function getPrStatus(repoPath, branchName) {
|
|
357
407
|
try {
|
|
358
|
-
const raw = execFileSync('gh', ['pr', 'view', branchName, '--json', 'state,url'], {
|
|
408
|
+
const raw = execFileSync('gh', ['pr', 'view', branchName, '--json', 'state,url,baseRefName'], {
|
|
359
409
|
cwd: repoPath,
|
|
360
410
|
encoding: 'utf-8',
|
|
361
411
|
}).trim();
|
|
362
412
|
if (!raw)
|
|
363
413
|
return null;
|
|
364
414
|
const parsed = JSON.parse(raw);
|
|
365
|
-
return {
|
|
415
|
+
return {
|
|
416
|
+
state: parsed.state,
|
|
417
|
+
url: parsed.url,
|
|
418
|
+
base: parsed.baseRefName || undefined,
|
|
419
|
+
};
|
|
366
420
|
}
|
|
367
421
|
catch {
|
|
368
422
|
return null;
|
|
@@ -644,6 +698,21 @@ export function getDiffStatsBetween(repoPath, base, head) {
|
|
|
644
698
|
return '';
|
|
645
699
|
}
|
|
646
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* Return `git diff --stat HEAD` output (working tree vs HEAD) as a single string.
|
|
703
|
+
* Empty string if the working tree is clean or the command fails. Best-effort: never throws.
|
|
704
|
+
*/
|
|
705
|
+
export function getWorkingTreeDiffStats(repoPath) {
|
|
706
|
+
try {
|
|
707
|
+
return execFileSync('git', ['diff', '--stat', 'HEAD'], {
|
|
708
|
+
cwd: repoPath,
|
|
709
|
+
encoding: 'utf-8',
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
catch {
|
|
713
|
+
return '';
|
|
714
|
+
}
|
|
715
|
+
}
|
|
647
716
|
// ── Async versions ───────────────────────────────────────────────────────────
|
|
648
717
|
// Non-blocking alternatives for hot paths (pr-watcher, route handlers).
|
|
649
718
|
/** Async version of getPrUrl. Returns null if no PR exists. */
|
|
@@ -662,7 +731,7 @@ export async function getPrUrlAsync(repoPath, branchName) {
|
|
|
662
731
|
/** Async version of getPrStatus. Returns null if no PR exists. */
|
|
663
732
|
export async function getPrStatusAsync(repoPath, branchName) {
|
|
664
733
|
try {
|
|
665
|
-
const { stdout } = await execFileAsync('gh', ['pr', 'view', branchName, '--json', 'state,url'], {
|
|
734
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'view', branchName, '--json', 'state,url,baseRefName'], {
|
|
666
735
|
cwd: repoPath,
|
|
667
736
|
encoding: 'utf-8',
|
|
668
737
|
});
|
|
@@ -670,7 +739,11 @@ export async function getPrStatusAsync(repoPath, branchName) {
|
|
|
670
739
|
if (!raw)
|
|
671
740
|
return null;
|
|
672
741
|
const parsed = JSON.parse(raw);
|
|
673
|
-
return {
|
|
742
|
+
return {
|
|
743
|
+
state: parsed.state,
|
|
744
|
+
url: parsed.url,
|
|
745
|
+
base: parsed.baseRefName || undefined,
|
|
746
|
+
};
|
|
674
747
|
}
|
|
675
748
|
catch {
|
|
676
749
|
return null;
|
|
@@ -689,3 +762,20 @@ export async function getUnpushedCountAsync(repoPath) {
|
|
|
689
762
|
return -1; // no upstream
|
|
690
763
|
}
|
|
691
764
|
}
|
|
765
|
+
/**
|
|
766
|
+
* Best-effort async `git fetch <remote> <branch>`. Never throws — by contract,
|
|
767
|
+
* suitable for both fire-and-forget and `await` use without try/catch at the
|
|
768
|
+
* call site. Logs a warning on failure but resolves cleanly.
|
|
769
|
+
*
|
|
770
|
+
* Mirrors the sync `fetchSourceBranch` sibling, including the optional `remote`
|
|
771
|
+
* parameter (defaults to `'origin'`).
|
|
772
|
+
*/
|
|
773
|
+
export async function fetchSourceBranchAsync(repoPath, branch, remote = 'origin') {
|
|
774
|
+
try {
|
|
775
|
+
await execFileAsync('git', ['fetch', remote, branch], { cwd: repoPath });
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
779
|
+
console.warn(`[git-ops] fetchSourceBranchAsync(${remote}/${branch}) failed: ${msg}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
const WINDOWS_RESERVED = new Set([
|
|
3
|
+
'con',
|
|
4
|
+
'prn',
|
|
5
|
+
'aux',
|
|
6
|
+
'nul',
|
|
7
|
+
'com1',
|
|
8
|
+
'com2',
|
|
9
|
+
'com3',
|
|
10
|
+
'com4',
|
|
11
|
+
'com5',
|
|
12
|
+
'com6',
|
|
13
|
+
'com7',
|
|
14
|
+
'com8',
|
|
15
|
+
'com9',
|
|
16
|
+
'lpt1',
|
|
17
|
+
'lpt2',
|
|
18
|
+
'lpt3',
|
|
19
|
+
'lpt4',
|
|
20
|
+
'lpt5',
|
|
21
|
+
'lpt6',
|
|
22
|
+
'lpt7',
|
|
23
|
+
'lpt8',
|
|
24
|
+
'lpt9',
|
|
25
|
+
]);
|
|
26
|
+
function slugifyOne(value) {
|
|
27
|
+
return value
|
|
28
|
+
.normalize('NFD')
|
|
29
|
+
.replace(/[̀-ͯ]/g, '')
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-+|-+$/g, '');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Produce a cross-OS-safe directory name (Linux + macOS + Windows) from a
|
|
36
|
+
* project's display name, falling back to the path basename and finally to
|
|
37
|
+
* the literal `'project'`. Output is guaranteed to be non-empty and to avoid
|
|
38
|
+
* Windows reserved names (CON, PRN, COM1..9, LPT1..9, AUX, NUL).
|
|
39
|
+
*/
|
|
40
|
+
export function slugifyProjectName(displayName, projectPath) {
|
|
41
|
+
const fromDisplay = slugifyOne((displayName ?? '').trim());
|
|
42
|
+
if (fromDisplay)
|
|
43
|
+
return guard(fromDisplay);
|
|
44
|
+
const basename = path.basename(projectPath ?? '');
|
|
45
|
+
const fromBasename = slugifyOne(basename);
|
|
46
|
+
if (fromBasename)
|
|
47
|
+
return guard(fromBasename);
|
|
48
|
+
return 'project';
|
|
49
|
+
}
|
|
50
|
+
function guard(slug) {
|
|
51
|
+
return WINDOWS_RESERVED.has(slug) ? `${slug}-project` : slug;
|
|
52
|
+
}
|
|
@@ -115,20 +115,22 @@ export function resolveGlobalWorktreesRoot(configuredPath) {
|
|
|
115
115
|
return flavor.isAbsolute(expanded) ? flavor.normalize(expanded) : null;
|
|
116
116
|
}
|
|
117
117
|
/** Resolve the full on-disk path for a workspace worktree. */
|
|
118
|
-
export function resolveWorkspaceWorktreePath(projectPath, workingBranch, configuredPath) {
|
|
118
|
+
export function resolveWorkspaceWorktreePath(projectPath, workingBranch, configuredPath, projectSlug) {
|
|
119
119
|
const root = resolveWorktreesRoot(projectPath, configuredPath);
|
|
120
|
-
|
|
120
|
+
const flavor = pathFlavor(projectPath, root);
|
|
121
|
+
const slugSegment = projectSlug && projectSlug.length > 0 ? [projectSlug] : [];
|
|
122
|
+
return flavor.join(root, ...slugSegment, ...branchPathSegments(workingBranch));
|
|
121
123
|
}
|
|
122
124
|
/** Resolve a renamed worktree next to its current path when the current path still matches its branch. */
|
|
123
|
-
export function resolveSiblingWorkspaceWorktreePath(projectPath, worktreePath, currentBranch, nextBranch) {
|
|
125
|
+
export function resolveSiblingWorkspaceWorktreePath(projectPath, worktreePath, currentBranch, nextBranch, projectSlug) {
|
|
124
126
|
const flavor = pathFlavor(projectPath, worktreePath);
|
|
125
127
|
const normalizedWorktreePath = flavor.normalize(worktreePath);
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const comparableSuffix = flavor === path.win32 ?
|
|
130
|
-
const root =
|
|
131
|
-
? normalizedWorktreePath.slice(0, -
|
|
128
|
+
const slugSegment = projectSlug && projectSlug.length > 0 ? [projectSlug] : [];
|
|
129
|
+
const currentSuffix = `${flavor.sep}${flavor.join(...slugSegment, ...branchPathSegments(currentBranch))}`;
|
|
130
|
+
const comparablePath = flavor === path.win32 ? normalizedWorktreePath.toLowerCase() : normalizedWorktreePath;
|
|
131
|
+
const comparableSuffix = flavor === path.win32 ? currentSuffix.toLowerCase() : currentSuffix;
|
|
132
|
+
const root = comparablePath.endsWith(comparableSuffix)
|
|
133
|
+
? normalizedWorktreePath.slice(0, -currentSuffix.length)
|
|
132
134
|
: resolveWorktreesRoot(projectPath);
|
|
133
|
-
return flavor.join(root, ...branchPathSegments(nextBranch));
|
|
135
|
+
return flavor.join(root, ...slugSegment, ...branchPathSegments(nextBranch));
|
|
134
136
|
}
|