mulmoterminal 7.1.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -7
- package/bin/cli-args.d.ts +1 -0
- package/bin/cli-args.js +5 -0
- package/bin/mulmoterminal.js +23 -3
- package/bin/ssh-hint.d.ts +3 -0
- package/bin/ssh-hint.js +32 -0
- package/bin/stop.d.ts +3 -1
- package/bin/stop.js +33 -6
- package/blueprints/ask/skills/keep/SKILL.md +1 -1
- package/blueprints/cloudflare/checks/page-renders.sh +12 -1
- package/blueprints/cloudflare/skills/spec/SKILL.md +1 -1
- package/blueprints/docs/checks/chaff.sh +1 -1
- package/blueprints/docs/checks/drafts.mjs +9 -6
- package/blueprints/docs/checks/places.mjs +6 -2
- package/blueprints/docs/skills/workspace/SKILL.md +1 -1
- package/blueprints/firebase/checks/page-renders.sh +12 -1
- package/blueprints/firebase/skills/spec/SKILL.md +1 -1
- package/blueprints/from-collection/checks/actions.sh +9 -7
- package/blueprints/from-collection/checks/compare.mjs +20 -10
- package/blueprints/from-collection/checks/import-supabase.sh +35 -0
- package/blueprints/from-collection/checks/supabase-verify.mjs +82 -0
- package/blueprints/from-collection/manifest.json +2 -2
- package/blueprints/from-collection/skills/acceptance-supabase/SKILL.md +30 -0
- package/blueprints/from-collection/skills/actions-supabase/SKILL.md +50 -0
- package/blueprints/from-collection/skills/import-production-supabase/SKILL.md +28 -0
- package/blueprints/from-collection/skills/import-supabase/SKILL.md +60 -0
- package/blueprints/from-collection/spec/conversion.md +21 -2
- package/blueprints/from-collection/steps.json +37 -0
- package/blueprints/local/skills/spec/SKILL.md +1 -1
- package/blueprints/polish/checks/feedback.mjs +3 -1
- package/blueprints/polish/checks/kind.d.mts +9 -0
- package/blueprints/polish/checks/kind.mjs +27 -0
- package/blueprints/polish/checks/named.d.mts +6 -0
- package/blueprints/polish/checks/named.mjs +49 -0
- package/blueprints/polish/checks/report.mjs +12 -0
- package/blueprints/polish/checks/targets.mjs +93 -9
- package/blueprints/polish/checks/targetsView.mjs +4 -1
- package/blueprints/polish/checks/viewpoints.d.mts +22 -0
- package/blueprints/polish/checks/viewpoints.mjs +113 -0
- package/blueprints/polish/hearing.json +28 -1
- package/blueprints/polish/kinds.json +15 -0
- package/blueprints/polish/presets/blog/blog.md +19 -0
- package/blueprints/polish/presets/houkoku/houkoku.md +19 -0
- package/blueprints/polish/presets.json +27 -0
- package/blueprints/polish/skills/polish/SKILL.md +32 -2
- package/blueprints/polish/skills/report/SKILL.md +11 -2
- package/blueprints/polish/skills/survey/SKILL.md +18 -0
- package/blueprints/polish/viewpoints.json +89 -0
- package/blueprints/product/manifest.json +2 -2
- package/blueprints/product/skills/acceptance-supabase/SKILL.md +30 -0
- package/blueprints/product/steps.json +9 -0
- package/blueprints/refactor/skills/report/SKILL.md +2 -1
- package/blueprints/refactor/skills/survey/SKILL.md +1 -1
- package/blueprints/review/skills/report/SKILL.md +1 -1
- package/blueprints/style/skills/report/SKILL.md +1 -1
- package/blueprints/supabase/checks/advisors.mjs +24 -0
- package/blueprints/supabase/checks/client-secrets.mjs +145 -0
- package/blueprints/supabase/checks/deploy-check.sh +33 -0
- package/blueprints/supabase/checks/handover.sh +14 -0
- package/blueprints/supabase/checks/linked-url.mjs +37 -0
- package/blueprints/supabase/checks/local-stack.sh +4 -0
- package/blueprints/supabase/checks/migrations-applied.mjs +49 -0
- package/blueprints/supabase/checks/page-renders.sh +39 -0
- package/blueprints/supabase/checks/prerequisites.sh +8 -0
- package/blueprints/supabase/checks/scaffold.sh +14 -0
- package/blueprints/supabase/checks/security-probe.mjs +244 -0
- package/blueprints/supabase/checks/security-report.sh +11 -0
- package/blueprints/supabase/checks/security.sh +49 -0
- package/blueprints/supabase/checks/serve.sh +25 -0
- package/blueprints/supabase/checks/smoke.sh +11 -0
- package/blueprints/supabase/checks/spec.sh +12 -0
- package/blueprints/supabase/checks/supabase-cli.mjs +31 -0
- package/blueprints/supabase/checks/tests-pass.sh +8 -0
- package/blueprints/supabase/manifest.json +27 -0
- package/blueprints/supabase/plan.json +76 -0
- package/blueprints/supabase/security/checklist.md +10 -0
- package/blueprints/supabase/skills/auth/SKILL.md +35 -0
- package/blueprints/supabase/skills/data/SKILL.md +50 -0
- package/blueprints/supabase/skills/deploy/SKILL.md +44 -0
- package/blueprints/supabase/skills/handover/SKILL.md +39 -0
- package/blueprints/supabase/skills/prerequisites/SKILL.md +24 -0
- package/blueprints/supabase/skills/run-check/SKILL.md +24 -0
- package/blueprints/supabase/skills/scaffold/SKILL.md +62 -0
- package/blueprints/supabase/skills/security/SKILL.md +87 -0
- package/blueprints/supabase/skills/spec/SKILL.md +32 -0
- package/blueprints/supabase/skills/ui/SKILL.md +25 -0
- package/blueprints/supabase/spec/overview.md +35 -0
- package/blueprints/verify/skills/report/SKILL.md +1 -1
- package/common/agentAccounts.ts +5 -0
- package/common/agentEntries.ts +63 -0
- package/common/appActions.ts +62 -0
- package/common/blueprint/personLanguage.ts +26 -0
- package/common/blueprint/run.ts +8 -0
- package/common/blueprint/specRevisionPrompt.ts +3 -0
- package/common/blueprint/stepPrompt.ts +5 -1
- package/common/customAgents.ts +6 -0
- package/common/dirConfigSaveReport.ts +34 -0
- package/common/headerActions.ts +74 -0
- package/common/headerButtonEntries.ts +102 -0
- package/common/headerChips.ts +103 -0
- package/common/hiddenCharacters.ts +75 -0
- package/common/keyRecording.ts +27 -0
- package/common/keymap.ts +40 -0
- package/common/keymapPresets.ts +100 -0
- package/common/mdPreviewMessage.ts +19 -2
- package/common/previewCodeBlocks.ts +47 -0
- package/common/providerEntries.ts +99 -0
- package/common/remoteServer.ts +11 -0
- package/common/themeEntries.ts +79 -0
- package/common/whatsNew.ts +71 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-B5xaEVpw.js → abnfDiagram-O67JEVCF-DfYKie1f.js} +1 -1
- package/dist/assets/architecture-7GRP2DOG-BuiSlwXB.js +1 -0
- package/dist/assets/{architectureDiagram-NJMV4G6O-qBYbXFGp.js → architectureDiagram-NJMV4G6O-vsLzjzQx.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DQioBmlX.js → blockDiagram-BEXU5L5S-DssDT1TO.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-cyT63OdJ.js → c4Diagram-YGBWAQC7-P0CrW8QT.js} +1 -1
- package/dist/assets/channel-BrsbZpfz.js +1 -0
- package/dist/assets/{chunk-3FUC2YCW-GAIwUw3Q.js → chunk-3FUC2YCW-CvTm_ho-.js} +1 -1
- package/dist/assets/{chunk-5DYCD2WN-DMiZMIIB.js → chunk-5DYCD2WN-BVnziItI.js} +1 -1
- package/dist/assets/{chunk-742MDFTN-6zizfpHe.js → chunk-742MDFTN-CccjktcU.js} +1 -1
- package/dist/assets/{chunk-7INBJB4K-C2dZhhKc.js → chunk-7INBJB4K-Bf0Shv2-.js} +1 -1
- package/dist/assets/{chunk-7M6MHVWA-CqPNVLmK.js → chunk-7M6MHVWA-BtwnZqTt.js} +1 -1
- package/dist/assets/{chunk-7PRAP22T-BoBqX9Yo.js → chunk-7PRAP22T-59aFrPRm.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-0N3yAwxf.js → chunk-GTNCS2PH-CT1kVgRW.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-g7RRSruF.js → chunk-GWA4HPMP-C94KrS1K.js} +1 -1
- package/dist/assets/{chunk-MBY4JIJT-C3i2TuVa.js → chunk-MBY4JIJT-D9Nn3Npg.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CH7vEM98.js → chunk-NETBCI7D-C-jbM8IP.js} +1 -1
- package/dist/assets/{chunk-O7XYJQB3-D1JLIcoV.js → chunk-O7XYJQB3-BQYzKLhh.js} +2 -2
- package/dist/assets/{chunk-UA2S7LBM-CpyAfTb8.js → chunk-UA2S7LBM-D7ovzGAr.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BGgzQY1u.js → chunk-WEXAMYUT-BmlxOjG3.js} +1 -1
- package/dist/assets/{chunk-Z7XXMR3K-CEMmXuqC.js → chunk-Z7XXMR3K-BSVBhxk7.js} +1 -1
- package/dist/assets/{chunk-ZIGJFQKS-g1Kaf-uj.js → chunk-ZIGJFQKS-x3IHLAdW.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-B54sxnuH.js → classDiagram-v2-NBCMYWYE-C6eb81l4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D796zEZZ.js → cynefin-OW5HDTMX-CM1PFDYP.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-WHBkoilE.js → cynefinDiagram-VND7K2PF-CxhcOZTy.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-2gQ61gKB.js → dagre-6A5THRUB-D5O9prrg.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-yDp0rHwO.js → diagram-22UHCM2B-h7R_GutV.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-pnng1LdB.js → diagram-3UASUU5V-DPmc5Ai4.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-D5Jkesja.js → diagram-ATOU4E4O-COI1PRUd.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DkH9QxZ2.js → diagram-CDSNMT55-ehWBAAIO.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CDjGhKHq.js → diagram-MLGK6HIB-CXxA6YAe.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-DAAr_c_l.js → diagram-MPIPVDR6-B6ANSQ5G.js} +1 -1
- package/dist/assets/{dist-C1cqjsKD.js → dist-B-lTjmBE.js} +1 -1
- package/dist/assets/{dist-CeWYOYWG.js → dist-B-mUmtv8.js} +1 -1
- package/dist/assets/{dist-CgC34u2d.js → dist-BnY3MUXM.js} +1 -1
- package/dist/assets/dist-Bx7SWRAa.js +199 -0
- package/dist/assets/{dist-BSQTHmad.js → dist-CPuq5mCf.js} +1 -1
- package/dist/assets/{dist-CWSTMA5n.js → dist-CQ6YfKVL.js} +1 -1
- package/dist/assets/{dist-C1s7S4KF.js → dist-CYrIpcYc.js} +1 -1
- package/dist/assets/{dist-C3s_Ij_z.js → dist-DHjjPgmc.js} +5 -5
- package/dist/assets/dist-DeIGO4lT.js +9 -0
- package/dist/assets/{dist-DbLWBcKv.js → dist-Dn4ta1sK.js} +1 -1
- package/dist/assets/{dist-B0sdAm9J.js → dist-Z8kzHarM.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DKJbg6sg.js → ebnfDiagram-ZINNZB2B-ByHtvU2q.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CgwsWSX7.js → elk-276RUBZZ-BF00ltRi.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-tCyjW7gV.js → erDiagram-OPXOYQCR-nW_7R9Lb.js} +1 -1
- package/dist/assets/eventmodeling-NTZA5JFV-C74VV9Z5.js +1 -0
- package/dist/assets/flowDiagram-KWPJA3E3-dKJG6rhg.js +1 -0
- package/dist/assets/{ganttDiagram-FUAMR5RP-C3CZ2yCC.js → ganttDiagram-FUAMR5RP-DaWxnbRm.js} +1 -1
- package/dist/assets/{gitGraph-4MIJSDKK-n2QHyQg8.js → gitGraph-4MIJSDKK-F7iDxaID.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dtk0GaqE.js → gitGraphDiagram-X574FWY7-D9fTMDaK.js} +1 -1
- package/dist/assets/{index-CyHGyZiL.js → index-ffQ8fSEK.js} +294 -290
- package/dist/assets/index-xE6PHMoe.css +1 -0
- package/dist/assets/{info-A6RAGUB7-Dt0N-ywn.js → info-A6RAGUB7-BIvkp14y.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-DtjU_EzA.js → infoDiagram-VRGFBTTK-CF3pSadX.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-xOGev5sa.js → ishikawaDiagram-OU5B5YK6-CnStwZlQ.js} +1 -1
- package/dist/assets/javascript-CppQSorr.js +1 -0
- package/dist/assets/{journeyDiagram-ZHPQQLJL-Disti4Wm.js → journeyDiagram-ZHPQQLJL-X6FpP2mU.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-CB5dROof.js → kanban-definition-PNTS6WVX-DGW7MkGx.js} +1 -1
- package/dist/assets/{line-D6MSXT7u.js → line-G8J9PhiP.js} +1 -1
- package/dist/assets/{marp-BASsSHAw.js → marp-BZuPB_AE.js} +61 -61
- package/dist/assets/{mermaid-parser.core-C29lKtTF.js → mermaid-parser.core-DlsoX-YC.js} +3 -3
- package/dist/assets/{mermaid.core-CWf9rghw.js → mermaid.core-DPC-93k6.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-BfBZCMx_.js → mindmap-definition-NLK3R4M7-D-K5bSnd.js} +1 -1
- package/dist/assets/{packet-AYTQ26CC-BRpJC2Bl.js → packet-AYTQ26CC-CmDsNYF8.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-xck9NPib.js → pegDiagram-GJSIUBJH-3rBm1VWr.js} +1 -1
- package/dist/assets/{pie-WAS4IAKB-De38-oiC.js → pie-WAS4IAKB-DwWpCstp.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DI7hJbHF.js → pieDiagram-5QR66LMP-IolT-z46.js} +1 -1
- package/dist/assets/punycode.es6-BGceFjA3.js +1 -0
- package/dist/assets/{quadrantDiagram-O4NWA36T-Bxqm6tTX.js → quadrantDiagram-O4NWA36T-Biq9fhdq.js} +1 -1
- package/dist/assets/{radar-RG4KPBEZ-CS8aCQ7S.js → radar-RG4KPBEZ-DH0gPMqI.js} +1 -1
- package/dist/assets/{railroad-74A4TZTK-DmloOzOC.js → railroad-74A4TZTK-SDx9VkwS.js} +1 -1
- package/dist/assets/railroad-abnf-HS5TGJTU-CCSPvBtI.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-DTWbjX18.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC--dNY7xHy.js +1 -0
- package/dist/assets/{railroadDiagram-XR7U4H2S-D8ruOuhL.js → railroadDiagram-XR7U4H2S-kQzwizD6.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-zWCW1gnc.js → requirementDiagram-PLB6GJNP-B3spDS6j.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-D1ndcb5q.js → sankeyDiagram-IPEJSGJF-DkXWUHlz.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-zO_f3a5l.js → sequenceDiagram-PO4LG4MO-Dt80gEIt.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DjHTP1Un.js → stateDiagram-v2-GCMORJYK-CG5xmXxG.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-3TYxXfqC.js → swimlanes-2SLR337P-B3S4pJ9E.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-5BPq1RA-.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-D2rwOmgp.js → timeline-definition-EJHVYXUP-DuKW3pM-.js} +1 -1
- package/dist/assets/{treeView-Q6P3EWNA-pJAjZYyX.js → treeView-Q6P3EWNA-CeeuwKmy.js} +1 -1
- package/dist/assets/{treemap-WGGIJYW6-BkBiLnEF.js → treemap-WGGIJYW6-CFK6V3lz.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DZQYTp5w.js → usecaseDiagram-POWQR4AR-CYbKz4kt.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CLFk8_TU.js → vennDiagram-UO4OBE2U-De8JmnbH.js} +1 -1
- package/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/dist/assets/{wardley-WFR3VGLG-UZLdcCMt.js → wardley-WFR3VGLG-BUvlimn0.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DOCJDdFu.js → wardleyDiagram-VNRHLVJA-e3jrcLF3.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DaRQvK2Z.js → xychartDiagram-PMCCYNJV-C1Q2Y2pt.js} +1 -1
- package/dist/index.html +3 -3
- package/docs/guide/en/v1.10.0.md +51 -0
- package/docs/guide/en/v1.11.0.md +44 -0
- package/docs/guide/en/v1.11.1.md +35 -0
- package/docs/guide/en/v1.12.0.md +46 -0
- package/docs/guide/en/v1.7.1.md +24 -0
- package/docs/guide/en/v1.7.2.md +27 -0
- package/docs/guide/en/v1.8.0.md +57 -0
- package/docs/guide/en/v1.9.0.md +31 -0
- package/docs/guide/en/v1.9.1.md +40 -0
- package/docs/guide/en/v1.9.2.md +27 -0
- package/docs/guide/en/v2.0.0.md +198 -0
- package/docs/guide/en/v2.1.0.md +159 -0
- package/docs/guide/en/v2.1.1.md +77 -0
- package/docs/guide/en/v2.2.0.md +150 -0
- package/docs/guide/en/v2.3.0.md +117 -0
- package/docs/guide/en/v2.4.0.md +135 -0
- package/docs/guide/en/v2.5.0.md +120 -0
- package/docs/guide/en/v2.5.1.md +108 -0
- package/docs/guide/en/v2.5.2.md +113 -0
- package/docs/guide/en/v2.5.3.md +158 -0
- package/docs/guide/en/v2.6.0.md +194 -0
- package/docs/guide/en/v2.7.0.md +228 -0
- package/docs/guide/en/v2.8.0.md +293 -0
- package/docs/guide/en/v2.9.0.md +207 -0
- package/docs/guide/en/v2.9.1.md +84 -0
- package/docs/guide/en/v3.0.0.md +121 -0
- package/docs/guide/en/v4.0.0.md +142 -0
- package/docs/guide/en/v4.1.0.md +114 -0
- package/docs/guide/en/v4.1.1.md +109 -0
- package/docs/guide/en/v4.10.0.md +99 -0
- package/docs/guide/en/v4.10.1.md +98 -0
- package/docs/guide/en/v4.11.0.md +177 -0
- package/docs/guide/en/v4.12.0.md +134 -0
- package/docs/guide/en/v4.13.0.md +106 -0
- package/docs/guide/en/v4.14.0.md +143 -0
- package/docs/guide/en/v4.15.0.md +131 -0
- package/docs/guide/en/v4.16.0.md +90 -0
- package/docs/guide/en/v4.16.1.md +110 -0
- package/docs/guide/en/v4.17.0.md +127 -0
- package/docs/guide/en/v4.18.0.md +107 -0
- package/docs/guide/en/v4.19.0.md +150 -0
- package/docs/guide/en/v4.2.0.md +201 -0
- package/docs/guide/en/v4.20.0.md +142 -0
- package/docs/guide/en/v4.21.0.md +159 -0
- package/docs/guide/en/v4.22.0.md +122 -0
- package/docs/guide/en/v4.23.0.md +113 -0
- package/docs/guide/en/v4.24.0.md +71 -0
- package/docs/guide/en/v4.25.0.md +113 -0
- package/docs/guide/en/v4.26.0.md +113 -0
- package/docs/guide/en/v4.27.0.md +125 -0
- package/docs/guide/en/v4.3.0.md +157 -0
- package/docs/guide/en/v4.3.1.md +73 -0
- package/docs/guide/en/v4.4.0.md +261 -0
- package/docs/guide/en/v4.5.0.md +241 -0
- package/docs/guide/en/v4.5.1.md +137 -0
- package/docs/guide/en/v4.6.0.md +175 -0
- package/docs/guide/en/v4.6.1.md +177 -0
- package/docs/guide/en/v4.7.0.md +179 -0
- package/docs/guide/en/v4.7.1.md +120 -0
- package/docs/guide/en/v4.7.2.md +195 -0
- package/docs/guide/en/v4.7.3.md +148 -0
- package/docs/guide/en/v4.7.4.md +123 -0
- package/docs/guide/en/v4.7.5.md +126 -0
- package/docs/guide/en/v4.7.6.md +81 -0
- package/docs/guide/en/v4.8.0.md +187 -0
- package/docs/guide/en/v4.8.1.md +149 -0
- package/docs/guide/en/v4.8.2.md +202 -0
- package/docs/guide/en/v4.8.3.md +97 -0
- package/docs/guide/en/v4.8.4.md +115 -0
- package/docs/guide/en/v4.8.5.md +163 -0
- package/docs/guide/en/v4.9.0.md +100 -0
- package/docs/guide/en/v5.0.0.md +106 -0
- package/docs/guide/en/v5.0.1.md +69 -0
- package/docs/guide/en/v5.1.0.md +110 -0
- package/docs/guide/en/v5.2.0.md +108 -0
- package/docs/guide/en/v5.3.0.md +107 -0
- package/docs/guide/en/v5.4.0.md +131 -0
- package/docs/guide/en/v5.5.0.md +71 -0
- package/docs/guide/en/v5.6.0.md +58 -0
- package/docs/guide/en/v5.7.0.md +60 -0
- package/docs/guide/en/v5.8.0.md +94 -0
- package/docs/guide/en/v6.0.0.md +44 -0
- package/docs/guide/en/v6.1.0.md +45 -0
- package/docs/guide/en/v6.2.0.md +65 -0
- package/docs/guide/en/v6.3.0.md +65 -0
- package/docs/guide/en/v6.4.0.md +85 -0
- package/docs/guide/en/v6.5.0.md +62 -0
- package/docs/guide/en/v6.6.0.md +133 -0
- package/docs/guide/en/v6.7.0.md +91 -0
- package/docs/guide/en/v6.8.0.md +235 -0
- package/docs/guide/en/v7.0.0.md +117 -0
- package/docs/guide/en/v7.1.0.md +106 -0
- package/docs/guide/en/v7.2.0.md +182 -0
- package/docs/guide/ja/v1.10.0.md +51 -0
- package/docs/guide/ja/v1.11.0.md +44 -0
- package/docs/guide/ja/v1.11.1.md +37 -0
- package/docs/guide/ja/v1.12.0.md +48 -0
- package/docs/guide/ja/v1.7.1.md +26 -0
- package/docs/guide/ja/v1.7.2.md +29 -0
- package/docs/guide/ja/v1.8.0.md +57 -0
- package/docs/guide/ja/v1.9.0.md +33 -0
- package/docs/guide/ja/v1.9.1.md +40 -0
- package/docs/guide/ja/v1.9.2.md +31 -0
- package/docs/guide/ja/v2.0.0.md +173 -0
- package/docs/guide/ja/v2.1.0.md +132 -0
- package/docs/guide/ja/v2.1.1.md +62 -0
- package/docs/guide/ja/v2.2.0.md +150 -0
- package/docs/guide/ja/v2.3.0.md +108 -0
- package/docs/guide/ja/v2.4.0.md +130 -0
- package/docs/guide/ja/v2.5.0.md +115 -0
- package/docs/guide/ja/v2.5.1.md +103 -0
- package/docs/guide/ja/v2.5.2.md +112 -0
- package/docs/guide/ja/v2.5.3.md +155 -0
- package/docs/guide/ja/v2.6.0.md +192 -0
- package/docs/guide/ja/v2.7.0.md +228 -0
- package/docs/guide/ja/v2.8.0.md +284 -0
- package/docs/guide/ja/v2.9.0.md +198 -0
- package/docs/guide/ja/v2.9.1.md +80 -0
- package/docs/guide/ja/v3.0.0.md +116 -0
- package/docs/guide/ja/v4.0.0.md +139 -0
- package/docs/guide/ja/v4.1.0.md +111 -0
- package/docs/guide/ja/v4.1.1.md +107 -0
- package/docs/guide/ja/v4.10.0.md +98 -0
- package/docs/guide/ja/v4.10.1.md +95 -0
- package/docs/guide/ja/v4.11.0.md +174 -0
- package/docs/guide/ja/v4.12.0.md +135 -0
- package/docs/guide/ja/v4.13.0.md +101 -0
- package/docs/guide/ja/v4.14.0.md +140 -0
- package/docs/guide/ja/v4.15.0.md +126 -0
- package/docs/guide/ja/v4.16.0.md +86 -0
- package/docs/guide/ja/v4.16.1.md +106 -0
- package/docs/guide/ja/v4.17.0.md +125 -0
- package/docs/guide/ja/v4.18.0.md +102 -0
- package/docs/guide/ja/v4.19.0.md +148 -0
- package/docs/guide/ja/v4.2.0.md +196 -0
- package/docs/guide/ja/v4.20.0.md +135 -0
- package/docs/guide/ja/v4.21.0.md +156 -0
- package/docs/guide/ja/v4.22.0.md +120 -0
- package/docs/guide/ja/v4.23.0.md +109 -0
- package/docs/guide/ja/v4.24.0.md +69 -0
- package/docs/guide/ja/v4.25.0.md +111 -0
- package/docs/guide/ja/v4.26.0.md +110 -0
- package/docs/guide/ja/v4.27.0.md +121 -0
- package/docs/guide/ja/v4.3.0.md +157 -0
- package/docs/guide/ja/v4.3.1.md +72 -0
- package/docs/guide/ja/v4.4.0.md +256 -0
- package/docs/guide/ja/v4.5.0.md +235 -0
- package/docs/guide/ja/v4.5.1.md +140 -0
- package/docs/guide/ja/v4.6.0.md +172 -0
- package/docs/guide/ja/v4.6.1.md +174 -0
- package/docs/guide/ja/v4.7.0.md +174 -0
- package/docs/guide/ja/v4.7.1.md +120 -0
- package/docs/guide/ja/v4.7.2.md +196 -0
- package/docs/guide/ja/v4.7.3.md +143 -0
- package/docs/guide/ja/v4.7.4.md +119 -0
- package/docs/guide/ja/v4.7.5.md +124 -0
- package/docs/guide/ja/v4.7.6.md +78 -0
- package/docs/guide/ja/v4.8.0.md +190 -0
- package/docs/guide/ja/v4.8.1.md +147 -0
- package/docs/guide/ja/v4.8.2.md +198 -0
- package/docs/guide/ja/v4.8.3.md +95 -0
- package/docs/guide/ja/v4.8.4.md +116 -0
- package/docs/guide/ja/v4.8.5.md +162 -0
- package/docs/guide/ja/v4.9.0.md +99 -0
- package/docs/guide/ja/v5.0.0.md +104 -0
- package/docs/guide/ja/v5.0.1.md +69 -0
- package/docs/guide/ja/v5.1.0.md +106 -0
- package/docs/guide/ja/v5.2.0.md +106 -0
- package/docs/guide/ja/v5.3.0.md +103 -0
- package/docs/guide/ja/v5.4.0.md +125 -0
- package/docs/guide/ja/v5.5.0.md +69 -0
- package/docs/guide/ja/v5.6.0.md +57 -0
- package/docs/guide/ja/v5.7.0.md +60 -0
- package/docs/guide/ja/v5.8.0.md +75 -0
- package/docs/guide/ja/v6.0.0.md +44 -0
- package/docs/guide/ja/v6.1.0.md +45 -0
- package/docs/guide/ja/v6.2.0.md +65 -0
- package/docs/guide/ja/v6.3.0.md +42 -0
- package/docs/guide/ja/v6.4.0.md +72 -0
- package/docs/guide/ja/v6.5.0.md +49 -0
- package/docs/guide/ja/v6.6.0.md +101 -0
- package/docs/guide/ja/v6.7.0.md +88 -0
- package/docs/guide/ja/v6.8.0.md +234 -0
- package/docs/guide/ja/v7.0.0.md +114 -0
- package/docs/guide/ja/v7.1.0.md +100 -0
- package/docs/guide/ja/v7.2.0.md +174 -0
- package/package.json +6 -2
- package/server/backends/scheduler-boot.ts +27 -24
- package/server/backends/scheduler.ts +81 -3
- package/server/blueprint/executor.ts +25 -1
- package/server/blueprint/routes.ts +20 -3
- package/server/blueprint/wiring.ts +1 -0
- package/server/config/agent-entry-routes.ts +123 -0
- package/server/config/app-config.ts +30 -15
- package/server/config/config-reload.ts +60 -0
- package/server/config/config-routes.ts +105 -23
- package/server/config/config-schema.ts +23 -8
- package/server/config/dir-config.ts +7 -3
- package/server/config/header-button-routes.ts +56 -0
- package/server/config/header-chip-routes.ts +56 -0
- package/server/config/header-config.ts +5 -6
- package/server/config/keymap-binding-route.ts +52 -0
- package/server/config/system-task-settings.ts +21 -0
- package/server/config/theme-entry-routes.ts +60 -0
- package/server/files/codeHighlight.ts +151 -0
- package/server/files/files-browse.ts +89 -14
- package/server/files/files-tree-routes.ts +102 -0
- package/server/files/mdPreviewReporter.ts +64 -19
- package/server/files/pathContainment.ts +10 -0
- package/server/files/previewCodeFence.ts +20 -0
- package/server/files/renderedDoc.ts +54 -0
- package/server/files/tree-ops.ts +143 -0
- package/server/index.ts +1 -1
- package/server/infra/on-listening.ts +6 -3
- package/server/routes/app-routes.ts +10 -1
- package/server/routes/dir-routes.ts +7 -0
- package/server/session/reap-schedule.ts +42 -14
- package/server/skills/mulmoterminal-config/SKILL.md +43 -18
- package/server/skills/mulmoterminal-dirs/SKILL.md +3 -1
- package/server/skills/mulmoterminal-header/SKILL.md +35 -8
- package/server/skills/mulmoterminal-keys/SKILL.md +29 -13
- package/server/skills/mulmoterminal-model/SKILL.md +12 -7
- package/server/skills/mulmoterminal-theme/SKILL.md +4 -2
- package/server/whatsNew/guidePage.ts +69 -0
- package/server/whatsNew/routes.ts +63 -0
- package/server/whatsNew/state.ts +43 -0
- package/dist/assets/architecture-7GRP2DOG-BxUYapfa.js +0 -1
- package/dist/assets/channel-DLqe-NE3.js +0 -1
- package/dist/assets/dist-DHSqnwKI.js +0 -9
- package/dist/assets/eventmodeling-NTZA5JFV-Bj6FbHv7.js +0 -1
- package/dist/assets/flowDiagram-KWPJA3E3-C3CneunC.js +0 -1
- package/dist/assets/index-BkbFlsCM.css +0 -1
- package/dist/assets/railroad-abnf-HS5TGJTU-lxjdNIS_.js +0 -1
- package/dist/assets/railroad-ebnf-LZEXJU2U-BvnO_c-5.js +0 -1
- package/dist/assets/railroad-peg-WCYAUIDC-Bco3H4cO.js +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-D0Ef5eY1.js +0 -8
package/README.md
CHANGED
|
@@ -308,7 +308,8 @@ mulmoterminal
|
|
|
308
308
|
|
|
309
309
|
**Stopping it.** `Ctrl+C` in the terminal that started it — or, if you can no longer find that
|
|
310
310
|
terminal, **Settings → Quit MulmoTerminal** in the browser, or **`npx mulmoterminal@latest stop`**
|
|
311
|
-
from any terminal (installed globally, just `mulmoterminal stop
|
|
311
|
+
from any terminal (installed globally, just `mulmoterminal stop`; it stops every server running on
|
|
312
|
+
this machine, and `--port <port>` stops only that one). All three run the same shutdown:
|
|
312
313
|
with `tmux` installed the agent sessions survive and come back under **Settings → Sessions that
|
|
313
314
|
survived a restart**; without it they end with the server.
|
|
314
315
|
|
|
@@ -634,6 +635,17 @@ up for Codex, loaded by description.
|
|
|
634
635
|
|
|
635
636
|
---
|
|
636
637
|
|
|
638
|
+
## Running the server on another machine
|
|
639
|
+
|
|
640
|
+
The server can live on a Linux box, a VPS or a container while your browser stays on your laptop:
|
|
641
|
+
start it there over SSH (`npx mulmoterminal`), and reach it through an SSH tunnel
|
|
642
|
+
(`ssh -N -L 34567:127.0.0.1:34567 you@server`, then open `http://localhost:34567`). Started over
|
|
643
|
+
SSH, the launcher opens no browser on the server and prints that command. It still listens on
|
|
644
|
+
loopback only — nothing is opened to the network. Logging the agents in on the server, Docker, and
|
|
645
|
+
the few actions that act on the server's machine: [Run the server on another machine](https://receptron.github.io/mulmoterminal/guide/en/remote.html).
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
637
649
|
## Session persistence (tmux)
|
|
638
650
|
|
|
639
651
|
If **`tmux` is installed**, MulmoTerminal runs each Claude session and launcher inside
|
|
@@ -777,7 +789,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
|
|
|
777
789
|
|
|
778
790
|

|
|
779
791
|
|
|
780
|
-
*Open it from the gear button in the toolbar. The **sidebar** groups the sections — Appearance, Projects, Header & launch, Input, Models & servers, Notifications, Integrations, Sessions, Help — and one is on screen at a time; on a phone the sidebar becomes a picker above the section. The interface is available in **English, 日本語, 简体中文, 繁體中文 and 한국어**: it follows your browser's language unless you pick one under **Language** (per browser, like the theme). Translated so far: this modal, the grid's status words, and every button's hover tip and screen-reader label; the rest of the app's words are still English. Under the title, a **Version** row shows what is running: the version from the shipped `package.json`, plus a `commit <sha>` chip on a git checkout — there the version is only whatever was last released, so the commit is what identifies the build. When something newer exists, the row is followed by the header badge's update notice, command included. Pick a **theme**, set the **terminal font size**, **font** and **scroll speed** (and whether sending **returns to the latest output**), set a custom **attention sound**, list the repos the cross-repo **PRs & Issues** view should aggregate, add **launch commands** for grid cells, register your own **MCP servers**, and turn on the switches for what this app writes on your behalf — **issue work comments**, the **PR clone footer**, the **closing summary**, the **decision digest**, the **dev worklog** — no need to hand-edit the config file.
|
|
792
|
+
*Open it from the gear button in the toolbar. The **sidebar** groups the sections — Appearance, Projects, Header & launch, Input, Models & servers, Notifications, Integrations, Sessions, Help — and one is on screen at a time; on a phone the sidebar becomes a picker above the section. The interface is available in **English, 日本語, 简体中文, 繁體中文 and 한국어**: it follows your browser's language unless you pick one under **Language** (per browser, like the theme). Translated so far: this modal, the grid's status words, and every button's hover tip and screen-reader label; the rest of the app's words are still English. Under the title, a **Version** row shows what is running: the version from the shipped `package.json`, plus a `commit <sha>` chip on a git checkout — there the version is only whatever was last released, so the commit is what identifies the build. When something newer exists, the row is followed by the header badge's update notice, command included. Pick a **theme**, set the **terminal font size**, **font** and **scroll speed** (and whether sending **returns to the latest output**), set a custom **attention sound**, list the repos the cross-repo **PRs & Issues** view should aggregate, add **launch commands** for grid cells, register your own **MCP servers**, and turn on the switches for what this app writes on your behalf — **issue work comments**, the **PR clone footer**, the **closing summary**, the **decision digest**, the **dev worklog** — no need to hand-edit the config file. Three settings stay with their skill because a form would be the wrong tool for them (`keymap`, `themes`, `buttons`/`chips`); Settings shows what each is doing now and launches that skill. Backends (`providers`), custom agents and accounts are added and removed in **Models and backends**. Note that **theme, font size, scroll speed and the return-to-latest switch are stored per browser** (they're display preferences, so a phone and a desktop keep their own); the rest live in `~/.mulmoterminal/config.json` and are shared by every client.*
|
|
781
793
|
|
|
782
794
|
| Field | Meaning |
|
|
783
795
|
| ------------ | ------- |
|
|
@@ -799,7 +811,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
|
|
|
799
811
|
| `pushKinds` | Which moments push: `"finished"` (a turn ended, ✅) and/or `"waiting"` (the agent stopped to ask — a permission prompt or a question, ❓, **once per prompt**). Omit to keep both; `[]` for none. A kind added in a later version stays off until you tick it. |
|
|
800
812
|
| `worklogEnabled` | `true` to run the built-in **dev worklog** batch (see below). Off by default (each run spawns an LLM session, so it costs tokens). Editable in Settings → **Sessions and background tasks**. |
|
|
801
813
|
| `worklogIntervalHours` | Worklog cadence in hours (default `6`, clamped to `1`–`168`). A stepper in the same Settings section covers the range. |
|
|
802
|
-
| `feedRefreshEnabled` | `false` stops the hourly **collection/feed refresh** (one scheduled task per root — the workspace and every saved project directory). **On by default**; only an explicit `false` turns it off, so an existing config keeps the behaviour it has.
|
|
814
|
+
| `feedRefreshEnabled` | `false` stops the hourly **collection/feed refresh** (one scheduled task per root — the workspace and every saved project directory). **On by default**; only an explicit `false` turns it off, so an existing config keeps the behaviour it has. Saved from Settings (or any `POST /api/config`), it applies at once — the running scheduler rebuilds its built-in tasks; a hand-edit applies after **Settings → Reload config file** (or a restart). Feeds still refresh on demand. Checkbox in Settings → **Sessions**. |
|
|
803
815
|
| `calendarSyncEnabled` | `false` stops the hourly **Google Calendar sync** (pulls changed events into collections that declare a calendar). Same default and the same restart rule as `feedRefreshEnabled`; calendar collections still sync on demand. Checkbox in the same Settings section. With both off and `worklogEnabled` off, no **built-in** task is registered. Your own tasks in `config/scheduler/tasks.json` are unaffected — these switches do not touch them, and an enabled one still registers and still runs the tick loop. Only when there is no enabled user task either does the scheduler stop writing its state file and run logs. |
|
|
804
816
|
| `terminalSubmit` | Which bytes Claude reads as **submit** vs **newline**: `"cr"` (default — Enter submits, Shift+Enter makes a newline) or `"esc-cr"` (for a Claude Code rebound the other way). Applies to the keyboard **and** the phone remote-view submit, for **Claude sessions only** (shell/codex keep plain Enter). See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#terminal-submit). Settings → **Terminal keys** offers both, worded as behaviour. |
|
|
805
817
|
| `copyOnSelect` | `true` puts a **mouse selection on the clipboard the moment it settles**, with no key pressed (the PuTTY / iTerm2 behaviour). **Off by default** — it changes the clipboard when you may only have meant to highlight something. There is a checkbox in Settings → **Terminal keys**, applied at once; a hand edit of the file needs a **server restart, then a tab reload** (the server reads this file once at startup, and the browser reads the value from it on load). Composes with the `copy` keymap action rather than replacing it. Over plain `http://` the browser gives a page no clipboard access, so a fallback asks xterm to copy instead; see the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#copy-on-select). |
|
|
@@ -811,6 +823,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
|
|
|
811
823
|
| `autoDirIcon` | Whether a project that sets no `icon` shows the favicon its repository already ships (`public/favicon.svg`, `apple-touch-icon.png`, a web manifest — first hit wins, ordered by how the image survives at 14px). **On by default**; Settings → **Directory appearance** has the switch. A single project opts out with `"icon": false` in its own `.mulmoterminal.json`, which this does not override. A key that was written and got it wrong shows nothing rather than falling back — a broken setting has to look broken. `true` / `false` only. |
|
|
812
824
|
| `cockpitLines` | `{ summary, prompt, response }` — how many lines each **cockpit-roster** row shows before it clamps (default `2` / `2` / `3`, each clamped to `1`–`20`). Raising them trades how many sessions fit on screen for reading a long one in place. Three steppers in Settings → **Waiting rows**. |
|
|
813
825
|
| `showLoadAverage` | Whether the grid header draws this machine's **load average** beside the 5h / 7d usage windows, as a percentage of its cores (`load 334%` = a 20-core machine with 66.8 runnable processes). **On by default**; amber at 100%, red at 200%, hover for the raw 1 / 5 / 15-minute figures. Settings → **Grid header read-outs** has the switch. A host that keeps no load average (Windows) shows nothing rather than `0%`, whatever this says. `true` / `false` only. |
|
|
826
|
+
| `remoteServer` | **Experimental.** The server runs on another machine than your browser (an SSH tunnel — [guide](https://receptron.github.io/mulmoterminal/guide/en/remote.html)): the actions that would act on the server's screen (the file dialog, the file manager, the OS app) are hidden or say why, and a dropped file is always uploaded. **Off unless `true`**; set it in the server's config. |
|
|
814
827
|
| `fontFamily` | The **terminal font** every session renders in — a CSS font-family stack, e.g. `"'Cica', 'MS Gothic', monospace"`. Set it in Settings → **Terminal font**, applied at once; editing the file instead needs a **restart** (this config is read once at startup). Unset uses the built-in stack (JetBrains Mono / Fira Code / Menlo / Consolas, then CJK faces for Japanese, Korean and Chinese). Unlike the per-browser font **size**, this is one value for the whole host — it names fonts, and which fonts exist is a property of the machine. A directory can override it. See the [Configuration guide](https://receptron.github.io/mulmoterminal/guide/en/config.html#font-family). |
|
|
815
828
|
|
|
816
829
|
Every MulmoTerminal on the machine shares this one file, so an older build could save over a key a
|
|
@@ -829,9 +842,14 @@ are items in the **path menu** (click the directory path on the terminal header)
|
|
|
829
842
|
set** with your list (it is not merged on top), so listing your own — even a **shorter** one — is how
|
|
830
843
|
you drop, reorder, or swap them.
|
|
831
844
|
A button has an `id`, `label`, and a `run` of `"shell"` (run a command), `"input"` (send text to the
|
|
832
|
-
agent), `"open"`, or `"action"` (act on the cell itself
|
|
833
|
-
|
|
834
|
-
|
|
845
|
+
agent), `"open"`, or `"action"` (act on the cell itself, named like the `keymap` actions so one operation is
|
|
846
|
+
a button, a key and a command-palette entry — `action: "terminal-restart"` ends the agent and starts
|
|
847
|
+
it again in the same cell on the same conversation, which is how an MCP / config / plugin change
|
|
848
|
+
takes effect; it costs a resume and asks nothing first. `"terminal-new-here"` opens the launch panel
|
|
849
|
+
on the cell's directory, and `"pane-files"` / `"pane-prompts"` / … / `"terminal-timeline"` /
|
|
850
|
+
`"terminal-talk"` / `"terminal-park"` / `"terminal-copy-code"` / `"terminal-insert-path"` / `"terminal-reveal"` /
|
|
851
|
+
`"terminal-voice"` / `"terminal-diff"` / `"terminal-note"` do what the cell's menus and buttons do, and `"screen-*"` / `"settings-open"` /
|
|
852
|
+
`"sound-toggle"` / `"view-toggle"` / `"order-*"` / `"page-next"` / `"page-prev"` do what the toolbar and the page tabs do; the old name `"restart"` still works). An `open` button targets one of `url` / `reveal` (OS file manager) / `files`
|
|
835
853
|
(in-app explorer) / `view` (a built-in overlay) / `terminal` (a dir → a new cell running `$SHELL`,
|
|
836
854
|
opened next to the current one) / `pr: true` (open the current branch's PR — the button is hidden when
|
|
837
855
|
there's no open PR) / `pickFile: true` (OS file dialog → insert the path).
|
|
@@ -891,7 +909,7 @@ channel is connected (its Google sign-in supplies the notification auth). With R
|
|
|
891
909
|
disconnected, or with no device registered, the toggle is a no-op.
|
|
892
910
|
|
|
893
911
|
**Dev worklog (cross-clone).** Set `worklogEnabled: true` in
|
|
894
|
-
`~/.mulmoterminal/config.json` (
|
|
912
|
+
`~/.mulmoterminal/config.json` (or switch it on in Settings, which applies at once; after a hand-edit use Settings → **Reload config file**)
|
|
895
913
|
to register a built-in scheduled task. Every `worklogIntervalHours` (default 6) it spawns
|
|
896
914
|
a Claude session that reviews the work you did across **all your saved working dirs**
|
|
897
915
|
(`cwdPresets`) since it last ran, and writes it up as a short manager-style report.
|
package/bin/cli-args.d.ts
CHANGED
|
@@ -25,4 +25,5 @@ export interface RunningInstance {
|
|
|
25
25
|
port: number | null;
|
|
26
26
|
}
|
|
27
27
|
export declare function stopCommandFor(pkgDir: string): string;
|
|
28
|
+
export declare function stopCommandForThis(stopCommand: string, port: number, instances: readonly { port: number | null }[]): string;
|
|
28
29
|
export declare function runningInstancesPrompt(instances: readonly RunningInstance[], stopCommand?: string): string;
|
package/bin/cli-args.js
CHANGED
|
@@ -366,6 +366,11 @@ export function saysYes(answer) {
|
|
|
366
366
|
// they are: npx unpacks into `<cache>/_npx/<hash>/node_modules/mulmoterminal`.
|
|
367
367
|
const NPX_INSTALL = /[/\\]_npx[/\\]/;
|
|
368
368
|
|
|
369
|
+
/** The stop command the ready banner shows for the server on `port`: the plain one while it is the
|
|
370
|
+
* only server, and one narrowed to its port once another is running, which the plain one would stop
|
|
371
|
+
* too (#2683). `instances` is the registry, this server included. */
|
|
372
|
+
export const stopCommandForThis = (stopCommand, port, instances) => (instances.some((i) => i.port !== port) ? `${stopCommand} --port ${port}` : stopCommand);
|
|
373
|
+
|
|
369
374
|
export const stopCommandFor = (pkgDir) => (NPX_INSTALL.test(String(pkgDir)) ? "npx mulmoterminal@latest stop" : "mulmoterminal stop");
|
|
370
375
|
|
|
371
376
|
export function runningInstancesPrompt(instances, stopCommand = "mulmoterminal stop") {
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { computeUpdateNotice, isUpdateCheckDisabled } from "./update-check.js";
|
|
16
16
|
import { detectNpxCacheDir, npxCacheHintLines } from "./npx-cache-hint.js";
|
|
17
|
+
import { configuredRemoteServer, sshTunnelHintLines } from "./ssh-hint.js";
|
|
17
18
|
import { planAfterServerExit } from "./server-supervision.js";
|
|
18
19
|
import { waitUntilReady } from "./wait-ready.js";
|
|
19
20
|
import {
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
secondInstancePrompt,
|
|
30
31
|
runningInstancesPrompt,
|
|
31
32
|
stopCommandFor,
|
|
33
|
+
stopCommandForThis,
|
|
32
34
|
SECOND_INSTANCE_NOTE,
|
|
33
35
|
nodeMeetsMinimum,
|
|
34
36
|
unsupportedNodeMessage,
|
|
@@ -109,6 +111,17 @@ function readConfiguredDefaultAgent() {
|
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
// The experimental `remoteServer` (#2669): the browser is on another machine, so the launcher opens
|
|
115
|
+
// none here even when started without SSH (a service, a remote desktop). A missing or unreadable
|
|
116
|
+
// file is simply "not set".
|
|
117
|
+
function readRemoteServer() {
|
|
118
|
+
try {
|
|
119
|
+
return configuredRemoteServer(JSON.parse(readFileSync(CONFIG_FILE, "utf8")));
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
// The command for `agent`, honouring its `<AGENT>_BIN` override.
|
|
113
126
|
//
|
|
114
127
|
// The override is the point rather than a detail: the gate used to look for the literal word
|
|
@@ -478,11 +491,16 @@ async function choosePort(requested, explicit) {
|
|
|
478
491
|
//
|
|
479
492
|
// `url` is where the BROWSER goes and `note` is what launchTarget wants said about that choice —
|
|
480
493
|
// two different facts since #1889, and the note is null whenever the URL already covers it.
|
|
481
|
-
function announceReady(url, note, noOpen) {
|
|
482
|
-
printReadyBanner(url, STOP_COMMAND);
|
|
494
|
+
function announceReady(url, note, noOpen, sshHint, port) {
|
|
495
|
+
printReadyBanner(url, stopCommandForThis(STOP_COMMAND, port, liveInstances()));
|
|
483
496
|
// Either the address a widened bind serves other machines on, or why the browser was NOT sent
|
|
484
497
|
// to `localhost`. Null whenever the URL above already said everything.
|
|
485
498
|
if (note) log(note);
|
|
499
|
+
// Over SSH the browser is on the other machine (see ssh-hint.js): say how to reach it, open nothing.
|
|
500
|
+
if (sshHint) {
|
|
501
|
+
sshHint.forEach((line) => log(line));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
486
504
|
if (noOpen) return;
|
|
487
505
|
try {
|
|
488
506
|
// The command is a hardcoded literal; url is built by browserUrl from a numeric port, so it
|
|
@@ -549,7 +567,9 @@ function runServer({ port, probedAddress, localhostIsUnambiguous, noOpen, launch
|
|
|
549
567
|
readyStarted = true;
|
|
550
568
|
const localhostIsOurs = localhostIsUnambiguous && serverSaysLocalhostIsOurs !== false;
|
|
551
569
|
const { url, note } = launchTarget(reachHost, port, localhostIsOurs);
|
|
552
|
-
cancelReady = waitUntilReady(port, () => announceReady(url, note, noOpen
|
|
570
|
+
cancelReady = waitUntilReady(port, () => announceReady(url, note, noOpen, sshTunnelHintLines(process.env, port, readRemoteServer()), port), {
|
|
571
|
+
host: reachHost,
|
|
572
|
+
});
|
|
553
573
|
};
|
|
554
574
|
// The same message answers a second question now: whether this lifetime ever bound at all,
|
|
555
575
|
// which is what a restart is allowed to depend on. Recorded BEFORE the address is looked at —
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function inSshSession(env: Record<string, string | undefined>): boolean;
|
|
2
|
+
export declare const configuredRemoteServer: (config: unknown) => boolean;
|
|
3
|
+
export declare function sshTunnelHintLines(env: Record<string, string | undefined>, port: number, remoteServer?: boolean): string[] | null;
|
package/bin/ssh-hint.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// A launcher started over SSH is on a machine whose screen nobody is looking at: the browser that
|
|
2
|
+
// should open this is on the other end of the connection. So it does not try to open one here — on
|
|
3
|
+
// a remote Mac that would put a window on a display no one sees — and it says how to reach the
|
|
4
|
+
// server from there instead: an SSH port forward, the route the FAQ recommends. Nothing here reads
|
|
5
|
+
// the process; the caller passes its environment.
|
|
6
|
+
|
|
7
|
+
/** Whether this process runs inside an SSH login. sshd sets both variables; either is enough. */
|
|
8
|
+
export function inSshSession(env) {
|
|
9
|
+
return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The experimental `remoteServer` out of a parsed `~/.mulmoterminal/config.json`: true only for a
|
|
13
|
+
* literal `true`, like the server's own reading of it (common/remoteServer.ts). */
|
|
14
|
+
export const configuredRemoteServer = (config) => config !== null && typeof config === "object" && config.remoteServer === true;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The lines that tell someone on another machine how to open this server, or null when the browser
|
|
18
|
+
* is expected here: not an SSH login, and `remoteServer` not set. The host is left for them to fill
|
|
19
|
+
* in: SSH_CONNECTION names the address THIS side answered on, which behind NAT or a container is not
|
|
20
|
+
* one their machine can reach.
|
|
21
|
+
*/
|
|
22
|
+
export function sshTunnelHintLines(env, port, remoteServer = false) {
|
|
23
|
+
const ssh = inSshSession(env);
|
|
24
|
+
if (!ssh && !remoteServer) return null;
|
|
25
|
+
const user = env.USER || env.LOGNAME || "<user>";
|
|
26
|
+
const why = ssh ? "Started over SSH" : "remoteServer is set in config.json";
|
|
27
|
+
return [
|
|
28
|
+
`${why}, so no browser is opened here. On your own machine, run:`,
|
|
29
|
+
` ssh -N -L ${port}:127.0.0.1:${port} ${user}@<this-host>`,
|
|
30
|
+
`then open http://localhost:${port} there.`,
|
|
31
|
+
];
|
|
32
|
+
}
|
package/bin/stop.d.ts
CHANGED
|
@@ -16,10 +16,12 @@ export interface StopResult {
|
|
|
16
16
|
stubborn: StubbornInstance[];
|
|
17
17
|
unconfirmed: InstanceEntry[];
|
|
18
18
|
}
|
|
19
|
-
export type StopArgs = { help: true } | { error: string } | { force: boolean };
|
|
19
|
+
export type StopArgs = { help: true } | { error: string } | { force: boolean; port: number | null };
|
|
20
20
|
|
|
21
21
|
export declare const STOP_USAGE: string;
|
|
22
22
|
export declare function parseStopArgs(args: readonly string[]): StopArgs;
|
|
23
|
+
export declare function selectByPort(instances: readonly InstanceEntry[], port: number | null): InstanceEntry[];
|
|
24
|
+
export declare function noServerOnPortReport(port: number, instances: readonly InstanceEntry[]): string[];
|
|
23
25
|
export interface ConfirmDeps {
|
|
24
26
|
owners?: (port: number) => Promise<number[] | null>;
|
|
25
27
|
}
|
package/bin/stop.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Windows user find or kill anything.
|
|
15
15
|
import { dirname } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { stopCommandFor } from "./cli-args.js";
|
|
17
|
+
import { parsePortArg, stopCommandFor } from "./cli-args.js";
|
|
18
18
|
import { isProcessAlive, liveInstances } from "./instances.js";
|
|
19
19
|
import { portOwners } from "./port-owner.js";
|
|
20
20
|
|
|
@@ -140,10 +140,11 @@ export function stopReport({ stopped, stubborn, unconfirmed }, platform = proces
|
|
|
140
140
|
export const stopExitCode = ({ stubborn, unconfirmed }) => (stubborn.length + unconfirmed.length ? 1 : 0);
|
|
141
141
|
|
|
142
142
|
export const STOP_USAGE = [
|
|
143
|
-
"Usage: mulmoterminal stop [--force]",
|
|
143
|
+
"Usage: mulmoterminal stop [--port <port>] [--force]",
|
|
144
144
|
"",
|
|
145
145
|
"Stops every running MulmoTerminal server on this machine, from any terminal.",
|
|
146
146
|
"",
|
|
147
|
+
" --port Stop only the server on that port (the one at http://localhost:<port>).",
|
|
147
148
|
" --force Also stop a registered server that is no longer answering. Off by default:",
|
|
148
149
|
" a server that crashed leaves its entry behind, and that pid may since have",
|
|
149
150
|
" been given to an unrelated program.",
|
|
@@ -153,9 +154,26 @@ export const STOP_USAGE = [
|
|
|
153
154
|
* argument handling is testable — and so `stop --help` can never be read as "stop everything". */
|
|
154
155
|
export function parseStopArgs(args) {
|
|
155
156
|
if (args.includes("--help") || args.includes("-h")) return { help: true };
|
|
156
|
-
const
|
|
157
|
+
const portAt = args.indexOf("--port");
|
|
158
|
+
const portArgs = portAt === -1 ? [] : [portAt, portAt + 1];
|
|
159
|
+
const unknown = args.filter((a, i) => a !== "--force" && !portArgs.includes(i));
|
|
157
160
|
if (unknown.length) return { error: `Unknown argument for stop: ${unknown.join(" ")}` };
|
|
158
|
-
|
|
161
|
+
const force = args.includes("--force");
|
|
162
|
+
if (portAt === -1) return { force, port: null };
|
|
163
|
+
// The launcher's own reading of --port, so the two accept the same values. The environment's
|
|
164
|
+
// PORT is not consulted: a `stop` narrowed by a variable nobody typed here would surprise.
|
|
165
|
+
const parsed = parsePortArg(args, {}, null);
|
|
166
|
+
return "error" in parsed ? { error: parsed.error } : { force, port: parsed.port };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The servers a `stop` is about: all of them, or only the one on `port` (#2683). */
|
|
170
|
+
export const selectByPort = (instances, port) => (port === null ? instances : instances.filter((i) => i.port === port));
|
|
171
|
+
|
|
172
|
+
/** What `stop --port` says when nothing is registered there — and what IS running, since a wrong
|
|
173
|
+
* port is the likeliest reason. */
|
|
174
|
+
export function noServerOnPortReport(port, instances) {
|
|
175
|
+
const running = instances.map(describeInstance);
|
|
176
|
+
return [`No MulmoTerminal server is running on port ${port}.`, running.length ? `Running: ${running.join(", ")}` : "MulmoTerminal is not running."];
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
export async function runStop(args = []) {
|
|
@@ -169,7 +187,16 @@ export async function runStop(args = []) {
|
|
|
169
187
|
console.error(STOP_USAGE);
|
|
170
188
|
process.exit(2);
|
|
171
189
|
}
|
|
172
|
-
const
|
|
173
|
-
|
|
190
|
+
const all = liveInstances();
|
|
191
|
+
const targets = selectByPort(all, parsed.port);
|
|
192
|
+
// Nothing on that port is the state asked for, like "nothing was running": not a failure.
|
|
193
|
+
if (parsed.port !== null && !targets.length) {
|
|
194
|
+
noServerOnPortReport(parsed.port, all).forEach((line) => console.log(line));
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
const result = await stopInstances(targets, { force: parsed.force });
|
|
198
|
+
// A retry must name the same server, or `--force` would stop every one.
|
|
199
|
+
const stopCommand = parsed.port === null ? selfStopCommand() : `${selfStopCommand()} --port ${parsed.port}`;
|
|
200
|
+
stopReport(result, process.platform, stopCommand).forEach((line) => console.log(line));
|
|
174
201
|
process.exit(stopExitCode(result));
|
|
175
202
|
}
|
|
@@ -18,7 +18,7 @@ Otherwise add to `FAQ.md` in this folder (create it if there is none). **Add aft
|
|
|
18
18
|
never edit or reorder what is there** — the check compares the beginning of the file with what it held
|
|
19
19
|
before. For each question: the question as a `##` heading, word for word; the answer; and where it is
|
|
20
20
|
written (the document, and the place named as in the answer: 第4条第2項, or a section's heading in 「」 —
|
|
21
|
-
never chaff's `h1.3`). For a question the documents do not answer, say so — that is worth keeping
|
|
21
|
+
in an English FAQ, in "…" — never chaff's `h1.3`). For a question the documents do not answer, say so — that is worth keeping
|
|
22
22
|
too.
|
|
23
23
|
|
|
24
24
|
## Done when
|
|
@@ -15,12 +15,23 @@ RENDER_LIMIT_SECONDS=60
|
|
|
15
15
|
work=$(mktemp -d)
|
|
16
16
|
"$chrome" --headless=new --disable-gpu --no-first-run --user-data-dir="$work/profile" --virtual-time-budget=15000 --dump-dom "$url" >"$work/dom.html" 2>/dev/null &
|
|
17
17
|
pid=$!
|
|
18
|
-
|
|
18
|
+
# Chrome can still be writing its profile when it is stopped, and under set -e a failed rm in this trap would turn a page
|
|
19
|
+
# that rendered into a failed check; the scratch folder is left behind instead.
|
|
20
|
+
trap 'kill "$pid" 2>/dev/null || true; rm -rf "$work" 2>/dev/null || true' EXIT
|
|
19
21
|
waited=0
|
|
20
22
|
until grep -q "</html>" "$work/dom.html" 2>/dev/null || [ "$waited" -ge "$RENDER_LIMIT_SECONDS" ] || ! kill -0 "$pid" 2>/dev/null; do
|
|
21
23
|
sleep 1
|
|
22
24
|
waited=$((waited + 1))
|
|
23
25
|
done
|
|
26
|
+
# A Chrome that crashed before printing the page says nothing about the app: like no Chrome, it cannot judge.
|
|
27
|
+
if ! grep -q "</html>" "$work/dom.html" 2>/dev/null && ! kill -0 "$pid" 2>/dev/null; then
|
|
28
|
+
status=0
|
|
29
|
+
wait "$pid" || status=$?
|
|
30
|
+
if [ "$status" -ne 0 ]; then
|
|
31
|
+
echo "Chrome ($chrome) exited with status $status before printing the page; could not confirm that $url renders" >&2
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
24
35
|
node -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{
|
|
25
36
|
const body = (s.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, ""])[1];
|
|
26
37
|
const text = body.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
@@ -17,7 +17,7 @@ The answers are in `.blueprint/answers.json`; the prompt names both pack directo
|
|
|
17
17
|
4. Something the answers do not settle is not guessed: write "未定" and add a line to
|
|
18
18
|
`.blueprint/open-questions.md` saying what is needed and why it matters.
|
|
19
19
|
5. Append both packs' `security/` checklists under a heading "必ず詰める点", unchanged.
|
|
20
|
-
6. Write in the language the user answered in. Nothing is built in this step; the next step stops for the user
|
|
20
|
+
6. Write in the language the build's prompt says the user reads (without one, the language the user answered in). Nothing is built in this step; the next step stops for the user
|
|
21
21
|
to read this spec.
|
|
22
22
|
|
|
23
23
|
Done when the check passes: `.blueprint/spec.md` exists and no `{{` placeholder is left in it.
|
|
@@ -11,13 +11,16 @@ const DIR = ".blueprint/chaff-feedback";
|
|
|
11
11
|
const INDEX = join(DIR, "index.json");
|
|
12
12
|
const DRAFTS_SECTION = [["chaff への報告の下書き", "Drafts for chaff"]];
|
|
13
13
|
|
|
14
|
-
const argsFor = (entry) =>
|
|
14
|
+
const argsFor = (entry, extra) =>
|
|
15
15
|
entry.kind === "wrong"
|
|
16
|
-
? ["feedback", entry.file, "--rule", entry.rule, "--line", String(entry.line), "--experimental"]
|
|
17
|
-
: ["feedback", entry.file, "--missed", "--line", String(entry.line), "--experimental"];
|
|
16
|
+
? ["feedback", entry.file, "--rule", entry.rule, "--line", String(entry.line), "--experimental", ...extra]
|
|
17
|
+
: ["feedback", entry.file, "--missed", "--line", String(entry.line), "--experimental", ...extra];
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Drafts one report per case and writes the index. Stops, leaving nothing behind, when a draft fails. `extra` adds
|
|
21
|
+
* arguments (such as `--genre`), so a draft reproduces the finding under the same measure the check used.
|
|
22
|
+
*/
|
|
23
|
+
export const draftReports = (cases, extra = []) => {
|
|
21
24
|
if (existsSync(DRAFT)) fail(`${DRAFT} already exists in this folder: it may be the person's own draft, so move it away first`);
|
|
22
25
|
const supported = runChaff(["--help"]).stdout.includes("chaff feedback");
|
|
23
26
|
mkdirSync(DIR, { recursive: true });
|
|
@@ -30,7 +33,7 @@ export const draftReports = (cases) => {
|
|
|
30
33
|
};
|
|
31
34
|
const drafted = supported
|
|
32
35
|
? cases.map((entry) => {
|
|
33
|
-
const run = runChaff(argsFor(entry));
|
|
36
|
+
const run = runChaff(argsFor(entry, extra));
|
|
34
37
|
if (run.code !== 0 || !existsSync(DRAFT)) undo(`chaff feedback did not draft ${entry.id}:\n${run.stderr || run.stdout}`);
|
|
35
38
|
const draft = join(DIR, `${entry.id}.md`);
|
|
36
39
|
renameSync(DRAFT, draft);
|
|
@@ -24,13 +24,17 @@ function japaneseName(label, own, above, article) {
|
|
|
24
24
|
return `${above}第${label ?? levels.at(-1)}号`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// A heading is quoted the way its own language quotes: 「」 when it holds kana or kanji, "…" otherwise.
|
|
28
|
+
const JAPANESE = /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u;
|
|
29
|
+
const quoted = (heading) => (JAPANESE.test(heading) ? `「${heading}」` : `"${heading}"`);
|
|
30
|
+
|
|
27
31
|
// A part (an article, a section) is named by its label, or by its quoted heading when it has none (a Markdown
|
|
28
32
|
// section); anything below it (an item, a paragraph) adds its own label to the name of what holds it.
|
|
29
33
|
function nameOf(node, own, above, article) {
|
|
30
34
|
const label = text(node?.attrs?.label);
|
|
31
35
|
if (PARTS.has(node?.kind)) {
|
|
32
36
|
const heading = text(node?.attrs?.heading);
|
|
33
|
-
return label ?? (heading === null ? own :
|
|
37
|
+
return label ?? (heading === null ? own : quoted(heading));
|
|
34
38
|
}
|
|
35
39
|
if (above === null) return label ?? own;
|
|
36
40
|
return article?.japanese && own.startsWith(`${article.address}.`) ? japaneseName(label, own, above, article) : `${above} ${label ?? own}`;
|
|
@@ -46,7 +50,7 @@ const namesIn = (node, above, article) => {
|
|
|
46
50
|
return [...here, ...(Array.isArray(node?.children) ? node.children.flatMap((child) => namesIn(child, name, within)) : [])];
|
|
47
51
|
};
|
|
48
52
|
|
|
49
|
-
/** Every address in a chaff tree, with the name a person reads for it: 第4条第2項, Section 3.2 (a) (i),
|
|
53
|
+
/** Every address in a chaff tree, with the name a person reads for it: 第4条第2項, Section 3.2 (a) (i), 「用意するもの」, "Getting started". */
|
|
50
54
|
export const placeNamesIn = (tree) => new Map(namesIn(tree, null, null));
|
|
51
55
|
|
|
52
56
|
/** The tree of `file` (`chaff tree --format json`); null when chaff cannot read it. */
|
|
@@ -12,7 +12,7 @@ repository, `.git/info/exclude`) until the person has approved the models.
|
|
|
12
12
|
1. `.blueprint/` already exists (the build created it). Everything this build records goes there.
|
|
13
13
|
2. Run chaff once through the base pack's wrapper, so its first download happens now and not in the
|
|
14
14
|
middle of a later step: `sh <base pack>/checks/chaff.sh rules --json > /dev/null`. The wrapper runs the
|
|
15
|
-
pinned chaff (`npx -y chaffjs@0.
|
|
15
|
+
pinned chaff (`npx -y chaffjs@0.15`) unless `CHAFF_BIN` names another. If it fails, show the person the
|
|
16
16
|
error and stop — do not work around it.
|
|
17
17
|
3. If the folder is a git repository, add `.blueprint/` to `.git/info/exclude`. That keeps the build's
|
|
18
18
|
working files out of commits without touching `.gitignore`, which belongs to the person.
|
|
@@ -15,12 +15,23 @@ RENDER_LIMIT_SECONDS=60
|
|
|
15
15
|
work=$(mktemp -d)
|
|
16
16
|
"$chrome" --headless=new --disable-gpu --no-first-run --user-data-dir="$work/profile" --virtual-time-budget=15000 --dump-dom "$url" >"$work/dom.html" 2>/dev/null &
|
|
17
17
|
pid=$!
|
|
18
|
-
|
|
18
|
+
# Chrome can still be writing its profile when it is stopped, and under set -e a failed rm in this trap would turn a page
|
|
19
|
+
# that rendered into a failed check; the scratch folder is left behind instead.
|
|
20
|
+
trap 'kill "$pid" 2>/dev/null || true; rm -rf "$work" 2>/dev/null || true' EXIT
|
|
19
21
|
waited=0
|
|
20
22
|
until grep -q "</html>" "$work/dom.html" 2>/dev/null || [ "$waited" -ge "$RENDER_LIMIT_SECONDS" ] || ! kill -0 "$pid" 2>/dev/null; do
|
|
21
23
|
sleep 1
|
|
22
24
|
waited=$((waited + 1))
|
|
23
25
|
done
|
|
26
|
+
# A Chrome that crashed before printing the page says nothing about the app: like no Chrome, it cannot judge.
|
|
27
|
+
if ! grep -q "</html>" "$work/dom.html" 2>/dev/null && ! kill -0 "$pid" 2>/dev/null; then
|
|
28
|
+
status=0
|
|
29
|
+
wait "$pid" || status=$?
|
|
30
|
+
if [ "$status" -ne 0 ]; then
|
|
31
|
+
echo "Chrome ($chrome) exited with status $status before printing the page; could not confirm that $url renders" >&2
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
24
35
|
node -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{
|
|
25
36
|
const body = (s.match(/<body[^>]*>([\s\S]*)<\/body>/i) || [, ""])[1];
|
|
26
37
|
const text = body.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
@@ -15,7 +15,7 @@ directories.
|
|
|
15
15
|
implication (the collections from `appPurpose`, for example), write your proposal and mark it `(提案)`.
|
|
16
16
|
3. Append both packs' `security/` checklists under a heading "必ず詰める点", unchanged — they are what the later
|
|
17
17
|
steps are held to.
|
|
18
|
-
4. Write in the language the user answered in.
|
|
18
|
+
4. Write in the language the build's prompt says the user reads (without one, the language the user answered in).
|
|
19
19
|
5. Nothing else is built in this step. The next step stops for the user to read this spec before anything is
|
|
20
20
|
created in their cloud account.
|
|
21
21
|
|
|
@@ -8,19 +8,21 @@ file=.blueprint/actions.json
|
|
|
8
8
|
# The record itself first: a build that got here without the spec check is held to it all the same.
|
|
9
9
|
node --no-warnings "$(dirname "$0")/decisions.mjs"
|
|
10
10
|
[ -s "$file" ] || { echo "no $file: the source has no actions or ingests to handle"; exit 0; }
|
|
11
|
-
# A key for a model call lives in .env (.dev.vars on a Worker
|
|
12
|
-
# git, and
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
# A key for a model call lives in .env (.dev.vars on a Worker, supabase/functions/.env for Supabase's functions); the
|
|
12
|
+
# folder becomes a repository whenever its person adds git, and none of them must go with it. A line naming the file
|
|
13
|
+
# alone (.env) ignores it at any depth, as git reads it.
|
|
14
|
+
for secrets in .env .dev.vars supabase/functions/.env; do
|
|
15
|
+
escape() { printf '%s' "$1" | sed 's/\./\\./g'; }
|
|
16
|
+
pattern="/?$(escape "$secrets")(\*)?|$(escape "$(basename "$secrets")")(\*)?"
|
|
15
17
|
if [ -f "$secrets" ] && ! grep -qxE "$pattern" .gitignore 2>/dev/null; then
|
|
16
18
|
echo "$secrets exists and .gitignore does not ignore it; add a line $secrets to .gitignore before anything secret goes in" >&2
|
|
17
19
|
exit 1
|
|
18
20
|
fi
|
|
19
21
|
done
|
|
20
22
|
case "$base" in
|
|
21
|
-
local|cloudflare) tests="test/actions.test.ts" ;;
|
|
23
|
+
local|cloudflare|supabase) tests="test/actions.test.ts" ;;
|
|
22
24
|
firebase) tests="test/blueprint/actions.spec.ts" ;;
|
|
23
|
-
*) echo "usage: actions.sh local|firebase|cloudflare" >&2; exit 2 ;;
|
|
25
|
+
*) echo "usage: actions.sh local|firebase|cloudflare|supabase" >&2; exit 2 ;;
|
|
24
26
|
esac
|
|
25
27
|
# The titles of the tests the file really declares — parsed, so a name in a comment or a string is not a test. Only read
|
|
26
28
|
# when something is to be built.
|
|
@@ -47,6 +49,6 @@ process.stdout.write(String(entries.filter((entry) => entry.decision === "featur
|
|
|
47
49
|
' "$file" "$tests" "$titles")
|
|
48
50
|
[ "$features" -gt 0 ] || exit 0
|
|
49
51
|
case "$base" in
|
|
50
|
-
local|cloudflare) sh "$BLUEPRINT_BASE/checks/tests-pass.sh" actions ;;
|
|
52
|
+
local|cloudflare|supabase) sh "$BLUEPRINT_BASE/checks/tests-pass.sh" actions ;;
|
|
51
53
|
firebase) sh "$BLUEPRINT_BASE/checks/emulator-test.sh" actions ;;
|
|
52
54
|
esac
|
|
@@ -23,12 +23,20 @@ export const isFileField = (spec) => spec.type === "image" || spec.type === "fil
|
|
|
23
23
|
export const hasValue = (value) => value !== undefined && value !== null && value !== "";
|
|
24
24
|
export const labelOf = (schema, record) => `${schema.primaryKey}=${JSON.stringify(record[schema.primaryKey])}`;
|
|
25
25
|
|
|
26
|
+
const DATE_TYPES = new Set(["date", "datetime"]);
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
29
|
* Whether a stored value is the source's. `booleanAs` says how the store holds a boolean — 0/1 in SQLite, itself in
|
|
28
|
-
* Firestore; a number is compared as a number, anything else as its text.
|
|
30
|
+
* Firestore; a number is compared as a number, anything else as its text. `datesAsInstants` is for a store that keeps
|
|
31
|
+
* dates as dates and writes them back in its own format (Postgres: 2026-01-02 as 2026-01-02T00:00:00Z): a date is then
|
|
32
|
+
* compared as the moment it names.
|
|
29
33
|
*/
|
|
30
|
-
export function sameValue(type, sourceValue, stored, booleanAs) {
|
|
34
|
+
export function sameValue(type, sourceValue, stored, booleanAs, datesAsInstants = false) {
|
|
31
35
|
if (type === "number") return Number(sourceValue) === Number(stored);
|
|
36
|
+
if (datesAsInstants && DATE_TYPES.has(type)) {
|
|
37
|
+
const [source, back] = [Date.parse(String(sourceValue)), Date.parse(String(stored))];
|
|
38
|
+
if (!Number.isNaN(source) && !Number.isNaN(back)) return source === back;
|
|
39
|
+
}
|
|
32
40
|
const want = type === "boolean" ? booleanAs(sourceValue === true) : sourceValue;
|
|
33
41
|
return String(want) === String(stored);
|
|
34
42
|
}
|
|
@@ -47,18 +55,20 @@ export const pointedAtFiles = () => [
|
|
|
47
55
|
),
|
|
48
56
|
];
|
|
49
57
|
|
|
50
|
-
// A SQL store
|
|
51
|
-
//
|
|
58
|
+
// A SQL store: the table is the collection's slug with `-` as `_`, and a column is the field's key (spec/conversion.md).
|
|
59
|
+
// How a value is held differs by store: SQLite and D1 keep a boolean as 0/1 and a date as its text; Postgres keeps both
|
|
60
|
+
// as themselves.
|
|
52
61
|
export const tableOf = (slug) => slug.replaceAll("-", "_");
|
|
53
|
-
const
|
|
62
|
+
export const SQLITE_STORE = { booleanAs: (flag) => (flag ? 1 : 0), datesAsInstants: false };
|
|
63
|
+
export const POSTGRES_STORE = { booleanAs: (flag) => flag, datesAsInstants: true };
|
|
54
64
|
|
|
55
65
|
// `at` is where the value sits: the collection, the record and its label, and how a missing file is told.
|
|
56
66
|
function columnProblems(at, key, spec, row) {
|
|
57
|
-
const { slug, record, label, fileMissing } = at;
|
|
67
|
+
const { slug, record, label, fileMissing, store } = at;
|
|
58
68
|
const value = record[key];
|
|
59
69
|
if (!hasValue(value)) return [];
|
|
60
70
|
if (!(key in row)) return [`${slug}: table ${tableOf(slug)} has no column ${key}`];
|
|
61
|
-
const problems = sameValue(spec.type, value, row[key],
|
|
71
|
+
const problems = sameValue(spec.type, value, row[key], store.booleanAs, store.datesAsInstants)
|
|
62
72
|
? []
|
|
63
73
|
: [`${slug} ${label}: ${key} is ${JSON.stringify(row[key])}, the source had ${JSON.stringify(value)}`];
|
|
64
74
|
const missing = isFileField(spec) && existsSync(`${SOURCE}/files/${value}`) && fileMissing(value);
|
|
@@ -69,9 +79,9 @@ function columnProblems(at, key, spec, row) {
|
|
|
69
79
|
* The rows of a collection's table held against its records: as many rows as records, and every stored field of every
|
|
70
80
|
* record reads back equal. `rows` is every row of the table, or null when there is no such table. `fileMissing(path)`
|
|
71
81
|
* says where a file the source holds is missing from ("not in data/files/"), or false when it is there; a store whose
|
|
72
|
-
* files are checked elsewhere leaves it out.
|
|
82
|
+
* files are checked elsewhere leaves it out. `store` is how the store holds its values (SQLITE_STORE, POSTGRES_STORE).
|
|
73
83
|
*/
|
|
74
|
-
export function tableProblems(slug, rows, fileMissing = () => false) {
|
|
84
|
+
export function tableProblems(slug, rows, fileMissing = () => false, store = SQLITE_STORE) {
|
|
75
85
|
const table = tableOf(slug);
|
|
76
86
|
if (rows === null) return [`${slug}: there is no table ${table}`];
|
|
77
87
|
const schema = schemaOf(slug);
|
|
@@ -83,7 +93,7 @@ export function tableProblems(slug, rows, fileMissing = () => false) {
|
|
|
83
93
|
const perRecord = records.flatMap((record) => {
|
|
84
94
|
const row = byKey.get(String(record[primary]));
|
|
85
95
|
if (!row) return [`${slug}: no row for ${primary} = ${JSON.stringify(record[primary])}`];
|
|
86
|
-
return plainFields(schema).flatMap(([key, spec]) => columnProblems({ slug, record, label: labelOf(schema, record), fileMissing }, key, spec, row));
|
|
96
|
+
return plainFields(schema).flatMap(([key, spec]) => columnProblems({ slug, record, label: labelOf(schema, record), fileMissing, store }, key, spec, row));
|
|
87
97
|
});
|
|
88
98
|
return [...counted, ...perRecord];
|
|
89
99
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The records the build copied are in Postgres, and the files they point at in Storage — read back through the Supabase
|
|
3
|
+
# CLI, not through the app. `local`: the local database is reset to the migrations and the seed, `yarn import-source
|
|
4
|
+
# --local` runs twice, then the stack is read. `linked`: production is read through the person's supabase login (the
|
|
5
|
+
# step itself did the import, after its approval). A build that copied only the shape has nothing to move.
|
|
6
|
+
set -eu
|
|
7
|
+
target="$1"
|
|
8
|
+
here="$(dirname "$0")"
|
|
9
|
+
source=.blueprint/source/source.json
|
|
10
|
+
[ -s "$source" ] || { echo "missing $source; the build was started without a collection" >&2; exit 1; }
|
|
11
|
+
if [ "$(node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).records === true))' "$source")" != true ]; then
|
|
12
|
+
echo "only the shape was copied; there are no records to move"
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
grep -q '"import-source"' package.json || { echo "package.json has no import-source script" >&2; exit 1; }
|
|
16
|
+
case "$target" in
|
|
17
|
+
local)
|
|
18
|
+
. "$BLUEPRINT_BASE/checks/local-stack.sh"
|
|
19
|
+
seeded=$(mktemp)
|
|
20
|
+
trap 'rm -f "$seeded"' EXIT INT TERM
|
|
21
|
+
node --no-warnings "$here/supabase-verify.mjs" --local --save-seeded "$seeded"
|
|
22
|
+
yarn -s import-source --local >/dev/null || { echo "yarn import-source --local failed" >&2; yarn -s import-source --local >&2 || true; exit 1; }
|
|
23
|
+
# Twice: moving the records again must leave the same rows, not a second copy of each.
|
|
24
|
+
yarn -s import-source --local >/dev/null || { echo "yarn import-source failed the second time over the same database" >&2; exit 1; }
|
|
25
|
+
node --no-warnings "$here/supabase-verify.mjs" --local --seeded "$seeded"
|
|
26
|
+
yarn test >/dev/null || { echo "yarn test fails" >&2; yarn test >&2 || true; exit 1; }
|
|
27
|
+
;;
|
|
28
|
+
linked)
|
|
29
|
+
node --no-warnings "$here/supabase-verify.mjs" --linked
|
|
30
|
+
;;
|
|
31
|
+
*)
|
|
32
|
+
echo "usage: import-supabase.sh local|linked" >&2
|
|
33
|
+
exit 2
|
|
34
|
+
;;
|
|
35
|
+
esac
|