mulmoterminal 6.7.0 → 7.0.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 +38 -2
- package/bin/check-versions.d.ts +7 -0
- package/bin/check-versions.js +80 -0
- package/bin/has-command.d.ts +4 -0
- package/bin/has-command.js +4 -4
- package/bin/mulmoterminal.js +7 -1
- package/bin/version-drift.d.ts +11 -0
- package/bin/version-drift.js +88 -0
- package/blueprints/ask/checks/replies.mjs +15 -2
- package/blueprints/ask/skills/answer/SKILL.md +7 -3
- package/blueprints/ask/skills/keep/SKILL.md +2 -1
- package/blueprints/ask/steps.json +2 -1
- package/blueprints/docs/checks/chaff.sh +1 -1
- package/blueprints/docs/checks/places.d.mts +7 -0
- package/blueprints/docs/checks/places.mjs +102 -0
- package/blueprints/docs/skills/workspace/SKILL.md +1 -1
- package/blueprints/firebase/checks/security-report.sh +11 -0
- package/blueprints/firebase/checks/security.sh +41 -0
- package/blueprints/firebase/checks/spec.sh +4 -0
- package/blueprints/firebase/checks/start-here.sh +8 -0
- package/blueprints/firebase/manifest.json +2 -1
- package/blueprints/firebase/plan.json +8 -1
- package/blueprints/firebase/skills/deploy-production/SKILL.md +14 -1
- package/blueprints/firebase/skills/security/SKILL.md +80 -0
- package/blueprints/from-collection/checks/actions.sh +48 -0
- package/blueprints/from-collection/checks/compare.mjs +42 -0
- package/blueprints/from-collection/checks/decisions.mjs +69 -0
- package/blueprints/from-collection/checks/firestore-verify.mjs +115 -0
- package/blueprints/from-collection/checks/import-firebase.sh +36 -0
- package/blueprints/from-collection/checks/import-verify.mjs +45 -0
- package/blueprints/from-collection/checks/import.sh +20 -0
- package/blueprints/from-collection/checks/spec.sh +43 -0
- package/blueprints/from-collection/checks/test-titles.mjs +101 -0
- package/blueprints/from-collection/hearing.json +83 -0
- package/blueprints/from-collection/manifest.json +8 -0
- package/blueprints/from-collection/skills/acceptance/SKILL.md +26 -0
- package/blueprints/from-collection/skills/acceptance-firebase/SKILL.md +27 -0
- package/blueprints/from-collection/skills/actions/SKILL.md +47 -0
- package/blueprints/from-collection/skills/actions-firebase/SKILL.md +46 -0
- package/blueprints/from-collection/skills/features-firebase/SKILL.md +28 -0
- package/blueprints/from-collection/skills/import/SKILL.md +39 -0
- package/blueprints/from-collection/skills/import-firebase/SKILL.md +38 -0
- package/blueprints/from-collection/skills/import-production/SKILL.md +29 -0
- package/blueprints/from-collection/spec/conversion.md +85 -0
- package/blueprints/from-collection/spec/requirements.md +74 -0
- package/blueprints/from-collection/steps.json +77 -0
- package/blueprints/local/checks/handover.sh +7 -1
- package/blueprints/local/checks/security-report.sh +11 -0
- package/blueprints/local/checks/security.sh +53 -0
- package/blueprints/local/checks/serve.sh +24 -0
- package/blueprints/local/checks/smoke.sh +1 -22
- package/blueprints/local/checks/spec.sh +4 -0
- package/blueprints/local/manifest.json +2 -1
- package/blueprints/local/plan.json +7 -0
- package/blueprints/local/security/checklist.md +2 -0
- package/blueprints/local/skills/handover/SKILL.md +17 -1
- package/blueprints/local/skills/security/SKILL.md +90 -0
- package/blueprints/polish/checks/targets.mjs +4 -0
- package/blueprints/polish/checks/targetsView.d.mts +2 -0
- package/blueprints/polish/checks/targetsView.mjs +7 -0
- package/blueprints/polish/hearing.json +4 -2
- package/blueprints/polish/presets.json +0 -1
- package/blueprints/polish/skills/polish/SKILL.md +1 -1
- package/blueprints/polish/skills/survey/SKILL.md +2 -1
- package/blueprints/polish/steps.json +2 -1
- package/blueprints/product/steps.json +1 -0
- package/blueprints/refactor/hearing.json +2 -1
- package/blueprints/review/checks/findings.mjs +5 -0
- package/blueprints/review/checks/findingsView.d.mts +15 -0
- package/blueprints/review/checks/findingsView.mjs +33 -0
- package/blueprints/review/skills/report/SKILL.md +2 -1
- package/blueprints/review/steps.json +2 -1
- package/blueprints/style/checks/sources.mjs +4 -1
- package/blueprints/style/checks/sourcesView.d.mts +2 -0
- package/blueprints/style/checks/sourcesView.mjs +15 -0
- package/blueprints/style/skills/report/SKILL.md +1 -1
- package/blueprints/style/steps.json +4 -2
- package/blueprints/verify/checks/extract.mjs +4 -0
- package/blueprints/verify/checks/factsView.d.mts +4 -0
- package/blueprints/verify/checks/factsView.mjs +39 -0
- package/blueprints/verify/skills/report/SKILL.md +2 -1
- package/blueprints/verify/steps.json +2 -1
- package/blueprints/write/checks/outlineView.d.mts +4 -0
- package/blueprints/write/checks/outlineView.mjs +14 -0
- package/blueprints/write/checks/parts.mjs +5 -0
- package/blueprints/write/manifest.json +8 -1
- package/blueprints/write/steps.json +4 -2
- package/common/blueprint/collectionSource.ts +114 -0
- package/common/blueprint/hearing.ts +62 -5
- package/common/blueprint/manifest.ts +22 -5
- package/common/blueprint/plan.ts +5 -0
- package/common/blueprint/refusal.ts +7 -2
- package/common/blueprint/run.ts +4 -1
- package/common/blueprint/sharedAppSource.ts +30 -0
- package/common/blueprint/state.ts +4 -0
- package/common/blueprint/stepPrompt.ts +69 -1
- package/common/collectionActions.ts +31 -0
- package/common/dirConfigSource.ts +3 -0
- package/common/fileGitStatus.ts +21 -0
- package/common/filesPage.ts +39 -0
- package/common/keymap.ts +12 -1
- package/common/mdPreviewMessage.ts +16 -2
- package/common/paletteConfig.ts +31 -0
- package/common/promptHistory.ts +9 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-COztuCu2.js → abnfDiagram-O67JEVCF-D8GL0pEj.js} +1 -1
- package/dist/assets/architecture-7GRP2DOG-TdhGGkKM.js +1 -0
- package/dist/assets/{architectureDiagram-NJMV4G6O-CocxKb8D.js → architectureDiagram-NJMV4G6O-kyzehsRR.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CoIOnOr8.js → blockDiagram-BEXU5L5S-9hqLban3.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DCkPDN1m.js → c4Diagram-YGBWAQC7-CUJxINHi.js} +1 -1
- package/dist/assets/channel-C2Xy1RUG.js +1 -0
- package/dist/assets/{chunk-3FUC2YCW-BpkN5ntL.js → chunk-3FUC2YCW-Dx91F6cc.js} +1 -1
- package/dist/assets/{chunk-5DYCD2WN-CkSUlK9I.js → chunk-5DYCD2WN-e-5CFr2i.js} +1 -1
- package/dist/assets/{chunk-742MDFTN-IT8yQzYr.js → chunk-742MDFTN-DLMZmirF.js} +1 -1
- package/dist/assets/{chunk-7INBJB4K-JGgoF_Kc.js → chunk-7INBJB4K-CwRDZro8.js} +1 -1
- package/dist/assets/{chunk-7M6MHVWA-DeMh7ryC.js → chunk-7M6MHVWA-Gm5DDkAY.js} +1 -1
- package/dist/assets/{chunk-7PRAP22T-Mu7uMyeb.js → chunk-7PRAP22T-CoSNjn-E.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-CZnXYcEC.js → chunk-GTNCS2PH-CLXhqupL.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-hNVTjL0n.js → chunk-GWA4HPMP-DX5sIYil.js} +1 -1
- package/dist/assets/{chunk-MBY4JIJT-BNBCCYaO.js → chunk-MBY4JIJT-Dzr4kplc.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-BFEY_LEK.js → chunk-NETBCI7D-DWhxcXYA.js} +1 -1
- package/dist/assets/{chunk-O7XYJQB3-igihGd57.js → chunk-O7XYJQB3-DaFEDFVm.js} +1 -1
- package/dist/assets/{chunk-UA2S7LBM-AvoPHYX2.js → chunk-UA2S7LBM-bYQCj6FB.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CnzHksV_.js → chunk-WEXAMYUT-DlYEsAVg.js} +1 -1
- package/dist/assets/{chunk-Z7XXMR3K-BZUbrMkZ.js → chunk-Z7XXMR3K-Bn6kE_dK.js} +1 -1
- package/dist/assets/{chunk-ZIGJFQKS-DFj3hrIY.js → chunk-ZIGJFQKS-CIZD8LBc.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-C2GePjwg.js → classDiagram-v2-NBCMYWYE-CHcNkGSD.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-E4NRCqvt.js → cynefin-OW5HDTMX-BgabrmIk.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BnHou4hN.js → cynefinDiagram-VND7K2PF-DwdVdS9K.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-kSCwZxth.js → dagre-6A5THRUB-DaqNJgIs.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DT04U1z3.js → diagram-22UHCM2B-BRDs0wCw.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-QaR0jD4H.js → diagram-3UASUU5V-BeXv9OSL.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-lmm7fQMw.js → diagram-ATOU4E4O-D2JkNL-b.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CdsN9JC6.js → diagram-CDSNMT55-DeYKsB8E.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CaVs00rU.js → diagram-MLGK6HIB-BHFLtv-n.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BudW9uT0.js → diagram-MPIPVDR6-Brtp-_IQ.js} +1 -1
- package/dist/assets/{dist-6UpZENeR.js → dist-B0sdAm9J.js} +1 -1
- package/dist/assets/dist-B2YXJLwR.js +11 -0
- package/dist/assets/{dist-unjTvGLV.js → dist-BSQTHmad.js} +1 -1
- package/dist/assets/dist-BTX-l2yD.js +1 -0
- package/dist/assets/dist-ByH4MDuI.js +9 -0
- package/dist/assets/dist-C1cqjsKD.js +1 -0
- package/dist/assets/{dist-BMBUd4uQ.js → dist-C1s7S4KF.js} +1 -1
- package/dist/assets/dist-CWSTMA5n.js +1 -0
- package/dist/assets/dist-CeWYOYWG.js +1 -0
- package/dist/assets/{dist-DJ_QvH_t.js → dist-SAdtDOGy.js} +2 -2
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DT9rFfx-.js → ebnfDiagram-ZINNZB2B-CE9roUQp.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DDeDnR5i.js → elk-276RUBZZ-DQHLfTBN.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-B9Fv1DKw.js → erDiagram-OPXOYQCR-Be7__jhY.js} +1 -1
- package/dist/assets/eventmodeling-NTZA5JFV-BXnmc8kB.js +1 -0
- package/dist/assets/flowDiagram-KWPJA3E3-C2T1HY21.js +1 -0
- package/dist/assets/{ganttDiagram-FUAMR5RP-CzlCs_Rv.js → ganttDiagram-FUAMR5RP-IKV0i-z0.js} +1 -1
- package/dist/assets/{gitGraph-4MIJSDKK-DMlpm2X5.js → gitGraph-4MIJSDKK-EofSfZtF.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-BodBMwuj.js → gitGraphDiagram-X574FWY7-DaljLxKK.js} +1 -1
- package/dist/assets/{index-DtoMXmGR.js → index-0Wsb2_IF.js} +359 -349
- package/dist/assets/index-BZZNuDnO.css +1 -0
- package/dist/assets/{info-A6RAGUB7-DcD8IyFk.js → info-A6RAGUB7-B6ck_7jX.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BaZCI3bY.js → infoDiagram-VRGFBTTK-C8jMc5nJ.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-C2YA2vvC.js → ishikawaDiagram-OU5B5YK6-BOM9dJ1W.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CqjWX8sS.js → journeyDiagram-ZHPQQLJL-DIYby1k-.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-CoM8jlm5.js → kanban-definition-PNTS6WVX-CAmlLeaS.js} +1 -1
- package/dist/assets/{line-Bt2FCpGX.js → line-DLTV2ptB.js} +1 -1
- package/dist/assets/{mermaid-parser.core-D9qR3oRl.js → mermaid-parser.core-CW3k1W-u.js} +3 -3
- package/dist/assets/{mermaid.core-BIXntNJ_.js → mermaid.core-B_zPtmsr.js} +4 -4
- package/dist/assets/{mindmap-definition-NLK3R4M7-4FYsrukx.js → mindmap-definition-NLK3R4M7-Ji-mQMX0.js} +1 -1
- package/dist/assets/{packet-AYTQ26CC-eQcU7oGW.js → packet-AYTQ26CC-MR7J7hYw.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-AwYWp-ta.js → pegDiagram-GJSIUBJH-D_wwEoYb.js} +1 -1
- package/dist/assets/{pie-WAS4IAKB-CJF4n-uR.js → pie-WAS4IAKB-DNuGDXby.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-CKnVXfdJ.js → pieDiagram-5QR66LMP-C1YCt7xK.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-BefBP8Qf.js → quadrantDiagram-O4NWA36T-DN-d9LoE.js} +1 -1
- package/dist/assets/{radar-RG4KPBEZ-BjQl0TqH.js → radar-RG4KPBEZ-dW6gQx6l.js} +1 -1
- package/dist/assets/{railroad-74A4TZTK-Z4kUresc.js → railroad-74A4TZTK-DnjwnHPx.js} +1 -1
- package/dist/assets/railroad-abnf-HS5TGJTU-COO_8DKu.js +1 -0
- package/dist/assets/railroad-ebnf-LZEXJU2U-H6w--gix.js +1 -0
- package/dist/assets/railroad-peg-WCYAUIDC-91dBFIdz.js +1 -0
- package/dist/assets/{railroadDiagram-XR7U4H2S-BUskllkB.js → railroadDiagram-XR7U4H2S-DdfYiy6T.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BCbsSEYH.js → requirementDiagram-PLB6GJNP-BzIEk2VN.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-DuFOhh5F.js → sankeyDiagram-IPEJSGJF-D_zpgGm7.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CtacIk91.js → sequenceDiagram-PO4LG4MO-WLxiEHkc.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bh0PfQR6.js → stateDiagram-v2-GCMORJYK-DYPJzs6R.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-3VWib0yw.js → swimlanes-2SLR337P-C6VX1K4R.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CGKdav0R.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DLUjmMJp.js → timeline-definition-EJHVYXUP-DqVxaMbg.js} +1 -1
- package/dist/assets/{treeView-Q6P3EWNA-urctZJZ8.js → treeView-Q6P3EWNA-CswdkPra.js} +1 -1
- package/dist/assets/{treemap-WGGIJYW6-7VaIVFkb.js → treemap-WGGIJYW6-DlB2H8xd.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-OlI3xHtd.js → usecaseDiagram-POWQR4AR-BkdJels6.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-ABZr8NaN.js → vennDiagram-UO4OBE2U-Cj8A--rK.js} +1 -1
- package/dist/assets/{wardley-WFR3VGLG-CX3iRocH.js → wardley-WFR3VGLG-B0WzVCNm.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-CmHM0PTe.js → wardleyDiagram-VNRHLVJA-BtKepEjT.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-CYUP0J_R.js → xychartDiagram-PMCCYNJV-BIVIXtl9.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +5 -4
- package/server/backends/collectionActionIndex.spec.ts +40 -0
- package/server/backends/collectionActionIndex.ts +18 -0
- package/server/backends/collectionActionIndexRoute.ts +17 -0
- package/server/backends/collections.ts +1 -1
- package/server/backends/filesPage.ts +46 -0
- package/server/backends/html.ts +1 -1
- package/server/backends/remoteHost/handlers/listSkills.ts +55 -6
- package/server/backends/remoteHost/listSkillsSession.spec.ts +84 -0
- package/server/backends/remoteHost/pluginSkills.spec.ts +110 -0
- package/server/backends/remoteHost/pluginSkills.ts +90 -0
- package/server/backends/remoteHost/skillOverrides.ts +7 -2
- package/server/backends/sharedApp/context.ts +3 -0
- package/server/backends/sharedApp/declare.ts +18 -6
- package/server/backends/sharedApp/publish.ts +11 -3
- package/server/backends/sharedApp/rosterRemovals.ts +59 -0
- package/server/blueprint/answersFile.ts +12 -0
- package/server/blueprint/collectionSnapshot.ts +207 -0
- package/server/blueprint/executor.ts +22 -6
- package/server/blueprint/newFolder.ts +13 -0
- package/server/blueprint/routes.ts +101 -9
- package/server/blueprint/samples.ts +4 -10
- package/server/blueprint/sharedAppsProvider.ts +53 -0
- package/server/blueprint/wiring.ts +14 -0
- package/server/blueprint/writeAllOrNone.ts +11 -0
- package/server/config/app-config.ts +18 -0
- package/server/config/config-body.ts +3 -1
- package/server/config/config-routes.ts +76 -33
- package/server/config/config-schema.ts +2 -0
- package/server/config/dir-config.ts +6 -1
- package/server/config/header-config.ts +31 -7
- package/server/config/header-context.ts +1 -1
- package/server/config/header-resolve.ts +7 -5
- package/server/config/worktree-dir-config.ts +1 -1
- package/server/files/files-browse.ts +7 -0
- package/server/files/files-git-status.ts +65 -0
- package/server/files/mdPreviewEmbed.ts +10 -7
- package/server/git/statusEntries.ts +40 -0
- package/server/git/worktrees.ts +19 -3
- package/server/infra/shared-app-tool.ts +24 -5
- package/server/routes/app-routes.ts +4 -0
- package/server/session/activity-transition.ts +12 -3
- package/server/session/lifecycle.ts +3 -2
- package/server/skills/mulmoterminal-header/SKILL.md +25 -1
- package/server/skills/mulmoterminal-keys/SKILL.md +48 -1
- package/server/skills/mulmoterminal-shared-app/SKILL.md +10 -2
- package/dist/assets/architecture-7GRP2DOG-DSPxsQOu.js +0 -1
- package/dist/assets/channel-j_IknEot.js +0 -1
- package/dist/assets/dist-4u5CgGvM.js +0 -9
- package/dist/assets/dist-B2UfqsPh.js +0 -1
- package/dist/assets/dist-BkNFiPKG.js +0 -11
- package/dist/assets/dist-CBfB3Kbo.js +0 -1
- package/dist/assets/dist-CHPrzLIK.js +0 -1
- package/dist/assets/dist-D8e4q8MY.js +0 -1
- package/dist/assets/eventmodeling-NTZA5JFV-BnIWMpmq.js +0 -1
- package/dist/assets/flowDiagram-KWPJA3E3-CgKOYFOK.js +0 -1
- package/dist/assets/index-XRjuwB8c.css +0 -1
- package/dist/assets/railroad-abnf-HS5TGJTU-DUcNrXOB.js +0 -1
- package/dist/assets/railroad-ebnf-LZEXJU2U-CiLdyg9w.js +0 -1
- package/dist/assets/railroad-peg-WCYAUIDC-k26xztEI.js +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DZwaV5aw.js +0 -8
package/README.md
CHANGED
|
@@ -303,7 +303,8 @@ with `tmux` installed the agent sessions survive and come back under **Settings
|
|
|
303
303
|
survived a restart**; without it they end with the server.
|
|
304
304
|
|
|
305
305
|
**First-run setup (optional).** `npx mulmoterminal@latest init` checks your environment (Node ≥ 22.12
|
|
306
|
-
and every CLI in the table above),
|
|
306
|
+
and every CLI in the table above), says how to update when Node is behind the latest LTS of its line
|
|
307
|
+
or Claude Code is behind its `stable` release, seeds the launcher's **directory
|
|
307
308
|
presets** from the projects in your Claude Code history, and writes `~/.mulmoterminal/config.json`.
|
|
308
309
|
It's **idempotent** — re-run it any time to refresh the presets; it overwrites the managed parts
|
|
309
310
|
and keeps your other settings. When `claude` is installed it can hand off to the
|
|
@@ -766,7 +767,7 @@ The Settings modal (the gear button) persists per-user UI choices to `~/.mulmote
|
|
|
766
767
|
|
|
767
768
|

|
|
768
769
|
|
|
769
|
-
*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.
|
|
770
|
+
*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. Four settings stay with their skill because a form would be the wrong tool for them (`keymap`, `themes`, `providers`, `buttons`/`chips`); Settings shows what each is doing now and launches that skill. 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.*
|
|
770
771
|
|
|
771
772
|
| Field | Meaning |
|
|
772
773
|
| ------------ | ------- |
|
|
@@ -1287,6 +1288,41 @@ server says when it arrives — including any directory you expanded while waiti
|
|
|
1287
1288
|
pane has not read before says so (`Loading…`) rather than claiming to be empty, and a read that
|
|
1288
1289
|
fails shows the error rather than a tree it can no longer vouch for.
|
|
1289
1290
|
|
|
1291
|
+
**Several files open as tabs, when you ask for them.** A plain click in the tree replaces the file
|
|
1292
|
+
in front, as it always has — so someone who never wants a tab never sees one. **Cmd+click** (Ctrl+click
|
|
1293
|
+
off macOS) or the row menu's **Open in a new tab** adds a tab, and a strip of tabs appears under the
|
|
1294
|
+
header once there are two. Opening a file that already has a tab goes to that tab, from the tree, the
|
|
1295
|
+
finder, the search or a clicked terminal path alike. Switching tabs saves the one you leave, exactly as
|
|
1296
|
+
opening another file does, so only the front tab can hold unsaved edits; each tab keeps its own place
|
|
1297
|
+
(Preview or editor, caret, scroll) and the whole strip is remembered with the rest of the pane. Close a
|
|
1298
|
+
tab with its **×**, a middle click, or **Delete** while it has focus; ←/→, Home and End move between them.
|
|
1299
|
+
The tree follows the tab in front, as VS Code's explorer does: its folders open and its row comes into
|
|
1300
|
+
view (a reload still puts the tree back where you left it).
|
|
1301
|
+
|
|
1302
|
+
**The tree shows what git sees.** In a git repository a changed file is tinted and lettered as in VS
|
|
1303
|
+
Code's explorer — `M` modified, `A` added, `U` untracked, `R` renamed — and a folder holding changes
|
|
1304
|
+
carries a dot, so a collapsed tree still says where the agent has been. It is read when the tree
|
|
1305
|
+
loads or reloads, after a save or an outside change to the open file, and every thirty seconds.
|
|
1306
|
+
|
|
1307
|
+
**The editor marks what changed since the last commit.** A bar beside each line that is new (green)
|
|
1308
|
+
or changed (amber), and a notch where lines were removed, as VS Code's gutter shows them — kept up
|
|
1309
|
+
to date as you type. **Changes** in the header also shows the removed lines in place, as a unified
|
|
1310
|
+
diff. Nothing is marked for a file outside git or one not yet committed.
|
|
1311
|
+
|
|
1312
|
+
**HTML pages and images show in the pane too.** An `.html` file has a **Preview** like a Markdown
|
|
1313
|
+
file: the page itself, sandboxed as presentHtml pages are — its scripts run on an opaque origin with
|
|
1314
|
+
no fetch/XHR; images and the curated CDN list still load, and so does an image it links beside it,
|
|
1315
|
+
while a relative stylesheet or script does not. An `.svg` previews as the picture it draws, and a PNG, JPEG, GIF or WebP opens as the image
|
|
1316
|
+
rather than as "not text". A path to any of these clicked in terminal output opens in the pane when
|
|
1317
|
+
it is up, so a chart an agent just wrote stays in the grid. They are served only under the workspace
|
|
1318
|
+
or a terminal's directory, like every other file the app hands to the browser.
|
|
1319
|
+
|
|
1320
|
+
**A link in the Preview opens in the pane.** A link to another file (`./b.md`, `../README.md`) is
|
|
1321
|
+
resolved against the document it is in and opens in a new tab — in Preview when it is Markdown — or
|
|
1322
|
+
goes to the tab that file already has. A link that climbs above the pane's folder is not opened and
|
|
1323
|
+
the pane says so; an external link opens in a new browser tab, and a `#heading` link scrolls the
|
|
1324
|
+
document.
|
|
1325
|
+
|
|
1290
1326
|
The toggle is not the only way in: while a cell is enlarged, **clicking a file path the agent
|
|
1291
1327
|
printed** opens it here too, rather than in a new tab or full-screen — see
|
|
1292
1328
|
[Clicking a file path](#clicking-a-file-path).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const NODE_RELEASES_URL: string;
|
|
2
|
+
export declare const CLAUDE_DIST_TAGS_URL: string;
|
|
3
|
+
export declare function fetchNodeReleases(): Promise<unknown>;
|
|
4
|
+
export declare function fetchClaudeStable(): Promise<string | null>;
|
|
5
|
+
export declare function resolveCommandPath(cmd: string, env?: Record<string, string | undefined>, platform?: string): string | null;
|
|
6
|
+
export declare function readClaudeVersion(binaryPath: string, timeout_ms?: number): Promise<string | null>;
|
|
7
|
+
export declare function checkVersions(options: { claudeBin: string | null }): Promise<{ node: string[]; claude: string[] }>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The network and process half of `init`'s version check (#2477); the decisions are in
|
|
2
|
+
// bin/version-drift.js. Every probe resolves null on failure — `init` must finish offline.
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { realpathSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { extensionsFor, namesAPath, realProbe, searchDirectories } from "./has-command.js";
|
|
7
|
+
import { nodeUpgradeGuide } from "./node-install.js";
|
|
8
|
+
import { claudeDrift, claudeDriftLines, claudeUpdateCommands, nodeDrift, nodeDriftLines, parseClaudeVersion } from "./version-drift.js";
|
|
9
|
+
|
|
10
|
+
export const NODE_RELEASES_URL = "https://nodejs.org/dist/index.json";
|
|
11
|
+
const REGISTRY = (process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
12
|
+
export const CLAUDE_DIST_TAGS_URL = `${REGISTRY}/-/package/@anthropic-ai/claude-code/dist-tags`;
|
|
13
|
+
|
|
14
|
+
const FETCH_TIMEOUT_MS = 4000;
|
|
15
|
+
// `claude --version` starts the whole CLI, which is slower than a fetch on a cold disk.
|
|
16
|
+
const CLAUDE_VERSION_TIMEOUT_MS = 8000;
|
|
17
|
+
|
|
18
|
+
async function fetchJson(url) {
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS), headers: { accept: "application/json" } });
|
|
21
|
+
return response.ok ? await response.json() : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const fetchNodeReleases = () => fetchJson(NODE_RELEASES_URL);
|
|
28
|
+
|
|
29
|
+
export async function fetchClaudeStable() {
|
|
30
|
+
const distTags = await fetchJson(CLAUDE_DIST_TAGS_URL);
|
|
31
|
+
return typeof distTags?.stable === "string" ? distTags.stable : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where `cmd` lives, symlinks followed, or null — searched with the start-up gate's own PATH rules
|
|
36
|
+
* (bin/has-command.js). Only picks the update command, never gates.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveCommandPath(cmd, env = process.env, platform = process.platform) {
|
|
39
|
+
const join = platform === "win32" ? path.win32.join : path.posix.join;
|
|
40
|
+
const candidates = namesAPath(cmd) ? [cmd] : searchDirectories(platform, env).flatMap((dir) => extensionsFor(platform).map((ext) => join(dir, cmd + ext)));
|
|
41
|
+
const found = candidates.find((candidate) => realProbe.isFile(candidate) && (platform === "win32" || realProbe.isExecutable(candidate)));
|
|
42
|
+
if (found === undefined) return null;
|
|
43
|
+
try {
|
|
44
|
+
return realpathSync(found);
|
|
45
|
+
} catch {
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Own timer rather than execFile's `timeout`: that one only signals the child and then waits for it
|
|
51
|
+
// to exit, so a `claude` that ignores SIGTERM (or a grandchild holding stdout) would hold `init`.
|
|
52
|
+
export function readClaudeVersion(binaryPath, timeout_ms = CLAUDE_VERSION_TIMEOUT_MS) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const child = execFile(binaryPath, ["--version"], (error, stdout) => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
resolve(error ? null : parseClaudeVersion(stdout));
|
|
57
|
+
});
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
child.kill("SIGKILL");
|
|
60
|
+
resolve(null);
|
|
61
|
+
}, timeout_ms);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function claudeLines(claudeBin) {
|
|
66
|
+
const binaryPath = resolveCommandPath(claudeBin);
|
|
67
|
+
const [localVersion, stable] = await Promise.all([binaryPath ? readClaudeVersion(binaryPath) : null, fetchClaudeStable()]);
|
|
68
|
+
return claudeDriftLines(claudeDrift(localVersion, stable), localVersion, claudeUpdateCommands(binaryPath));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function nodeLines() {
|
|
72
|
+
const releases = await fetchNodeReleases();
|
|
73
|
+
return nodeDriftLines(nodeDrift(process.versions.node, releases), nodeUpgradeGuide(process.execPath, process.platform, process.env));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The hint lines `init` prints under its Node and Claude Code checks. */
|
|
77
|
+
export async function checkVersions({ claudeBin }) {
|
|
78
|
+
const [node, claude] = await Promise.all([nodeLines(), claudeBin ? claudeLines(claudeBin) : []]);
|
|
79
|
+
return { node, claude };
|
|
80
|
+
}
|
package/bin/has-command.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export interface CommandProbe {
|
|
|
3
3
|
isExecutable: (candidate: string) => boolean;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export declare const realProbe: CommandProbe;
|
|
7
|
+
export declare const namesAPath: (cmd: string) => boolean;
|
|
8
|
+
export declare const extensionsFor: (platform: string) => string[];
|
|
9
|
+
export declare const searchDirectories: (platform: string, env: Record<string, string | undefined>) => string[];
|
|
6
10
|
export declare function hasCommand(
|
|
7
11
|
cmd: string,
|
|
8
12
|
options?: { platform?: NodeJS.Platform; env?: Record<string, string | undefined>; probe?: CommandProbe },
|
package/bin/has-command.js
CHANGED
|
@@ -37,9 +37,9 @@ import path from "node:path";
|
|
|
37
37
|
const WINDOWS_LAUNCHABLE_EXTENSIONS = ["", ".exe", ".com", ".cmd", ".bat"];
|
|
38
38
|
|
|
39
39
|
/** A name CreateProcess/execvp resolves itself rather than by searching PATH. */
|
|
40
|
-
const namesAPath = (cmd) => cmd.includes("/") || cmd.includes("\\");
|
|
40
|
+
export const namesAPath = (cmd) => cmd.includes("/") || cmd.includes("\\");
|
|
41
41
|
|
|
42
|
-
const realProbe = {
|
|
42
|
+
export const realProbe = {
|
|
43
43
|
isFile: (candidate) => {
|
|
44
44
|
try {
|
|
45
45
|
return statSync(candidate).isFile();
|
|
@@ -60,7 +60,7 @@ const realProbe = {
|
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
// "" first so an explicit `foo.exe` is found as itself rather than as `foo.exe.EXE`.
|
|
63
|
-
const extensionsFor = (platform) => (platform === "win32" ? WINDOWS_LAUNCHABLE_EXTENSIONS : [""]);
|
|
63
|
+
export const extensionsFor = (platform) => (platform === "win32" ? WINDOWS_LAUNCHABLE_EXTENSIONS : [""]);
|
|
64
64
|
|
|
65
65
|
// A Windows PATH entry may be QUOTED — `"C:\Program Files\tools"` — which the shells strip and a
|
|
66
66
|
// plain join would not, leaving a path that matches nothing. Same rule as the server's
|
|
@@ -69,7 +69,7 @@ const extensionsFor = (platform) => (platform === "win32" ? WINDOWS_LAUNCHABLE_E
|
|
|
69
69
|
// The current directory is on neither platform's list. cmd.exe searches it and POSIX shells do
|
|
70
70
|
// not, but what launches an agent is node-pty on both — and the directory THIS process sits in is
|
|
71
71
|
// the launch directory, never the one the PTY will run in.
|
|
72
|
-
const searchDirectories = (platform, env) => {
|
|
72
|
+
export const searchDirectories = (platform, env) => {
|
|
73
73
|
const raw = env.PATH || env.Path || "";
|
|
74
74
|
if (platform === "win32") {
|
|
75
75
|
return raw
|
package/bin/mulmoterminal.js
CHANGED
|
@@ -223,12 +223,18 @@ function passStartupGate(args) {
|
|
|
223
223
|
async function runInit(initArgs) {
|
|
224
224
|
log("Setting up MulmoTerminal…\n");
|
|
225
225
|
|
|
226
|
+
const hasClaude = agentInstalled("claude");
|
|
227
|
+
// Imported here so a normal launch never loads the version check or reaches its network.
|
|
228
|
+
const { checkVersions } = await import("./check-versions.js");
|
|
229
|
+
const versionHints = await checkVersions({ claudeBin: hasClaude ? agentBin("claude") : null });
|
|
230
|
+
|
|
226
231
|
const nodeOk = nodeMeetsMinimum(process.versions.node);
|
|
227
232
|
console.log(nodeOk ? ` ✓ Node ${process.versions.node}` : ` ✗ Node ${process.versions.node} — MulmoTerminal needs ≥ ${MIN_NODE_LABEL}`);
|
|
233
|
+
versionHints.node.forEach((line) => console.log(line));
|
|
228
234
|
|
|
229
|
-
const hasClaude = agentInstalled("claude");
|
|
230
235
|
if (hasClaude) {
|
|
231
236
|
console.log(" ✓ Claude Code CLI");
|
|
237
|
+
versionHints.claude.forEach((line) => console.log(line));
|
|
232
238
|
} else {
|
|
233
239
|
console.log(" ✗ Claude Code CLI — not found");
|
|
234
240
|
console.log(" → npm install -g @anthropic-ai/claude-code (then run `claude` and log in)");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Drift = { kind: "current" } | { kind: "unknown" } | { kind: "behind"; latest: string; sameMajor?: boolean };
|
|
2
|
+
export declare const CURRENT: Drift;
|
|
3
|
+
export declare const UNKNOWN: Drift;
|
|
4
|
+
export declare const CLAUDE_UPDATE_NATIVE: string;
|
|
5
|
+
export declare const CLAUDE_UPDATE_NPM: string;
|
|
6
|
+
export declare function nodeDrift(localVersion: string, releases: unknown): Drift;
|
|
7
|
+
export declare function claudeDrift(localVersion: string | null, stableVersion: string | null): Drift;
|
|
8
|
+
export declare function parseClaudeVersion(stdout: string | null | undefined): string | null;
|
|
9
|
+
export declare function claudeUpdateCommands(realPath: string | null): string[];
|
|
10
|
+
export declare function nodeDriftLines(drift: Drift, upgrade: import("./node-install.js").NodeUpgradeGuide): string[];
|
|
11
|
+
export declare function claudeDriftLines(drift: Drift, localVersion: string | null, updateCommands: string[]): string[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Whether this machine's Node and Claude Code are behind the published releases, and what `init`
|
|
2
|
+
// says about it (#2477). Pure: the releases, the local versions and the binary path are handed in
|
|
3
|
+
// by bin/check-versions.js, so every decision here is testable without the network.
|
|
4
|
+
import { NODE_DOWNLOAD_URL } from "./node-install.js";
|
|
5
|
+
import { isNewerVersion } from "./update-check.js";
|
|
6
|
+
|
|
7
|
+
export const CURRENT = { kind: "current" };
|
|
8
|
+
export const UNKNOWN = { kind: "unknown" };
|
|
9
|
+
|
|
10
|
+
const stripV = (version) => String(version).replace(/^v/, "");
|
|
11
|
+
const majorOf = (version) => Number.parseInt(stripV(version).split(".")[0], 10);
|
|
12
|
+
const newestOf = (versions) => versions.reduce((newest, version) => (isNewerVersion(version, newest) ? version : newest));
|
|
13
|
+
// Remote strings are printed as-is, so only a plain x.y.z is accepted — anything else would compare
|
|
14
|
+
// as zeros (and read as current) or put the registry's text on the user's terminal.
|
|
15
|
+
const isPlainVersion = (version) => typeof version === "string" && /^v?\d+\.\d+\.\d+$/.test(version);
|
|
16
|
+
const isRelease = (entry) => typeof entry === "object" && entry !== null && isPlainVersion(entry.version);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Node against the LTS releases in nodejs.org's index.json.
|
|
20
|
+
*
|
|
21
|
+
* The target is the newest LTS of the same major: moving someone off a working 22.x because 24.x
|
|
22
|
+
* exists is not this check's call. A major with no LTS release is compared with the newest LTS,
|
|
23
|
+
* unless it is newer than that one — a Current release is not behind anything.
|
|
24
|
+
*/
|
|
25
|
+
export function nodeDrift(localVersion, releases) {
|
|
26
|
+
if (!Array.isArray(releases) || Number.isNaN(majorOf(localVersion))) return UNKNOWN;
|
|
27
|
+
const ltsVersions = releases.filter((entry) => isRelease(entry) && entry.lts).map((entry) => stripV(entry.version));
|
|
28
|
+
if (ltsVersions.length === 0) return UNKNOWN;
|
|
29
|
+
const sameMajor = ltsVersions.filter((version) => majorOf(version) === majorOf(localVersion));
|
|
30
|
+
const newestLts = newestOf(ltsVersions);
|
|
31
|
+
if (sameMajor.length === 0 && majorOf(localVersion) > majorOf(newestLts)) return CURRENT;
|
|
32
|
+
const target = sameMajor.length > 0 ? newestOf(sameMajor) : newestLts;
|
|
33
|
+
return isNewerVersion(target, stripV(localVersion)) ? { kind: "behind", latest: target, sameMajor: sameMajor.length > 0 } : CURRENT;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Claude Code against the npm `stable` dist-tag. Ahead of `stable` (on `latest`) is current. */
|
|
37
|
+
export function claudeDrift(localVersion, stableVersion) {
|
|
38
|
+
if (typeof localVersion !== "string" || !isPlainVersion(stableVersion)) return UNKNOWN;
|
|
39
|
+
return isNewerVersion(stableVersion, localVersion) ? { kind: "behind", latest: stableVersion } : CURRENT;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `claude --version` prints `2.1.284 (Claude Code)`; anything without a leading version is null. */
|
|
43
|
+
export function parseClaudeVersion(stdout) {
|
|
44
|
+
return /^\s*(\d+\.\d+\.\d+)/.exec(String(stdout ?? ""))?.[1] ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const CLAUDE_UPDATE_NATIVE = "claude update";
|
|
48
|
+
export const CLAUDE_UPDATE_NPM = "npm install -g @anthropic-ai/claude-code@latest";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* How to update the Claude Code at `realPath`. Only the two layouts checked on a real machine get a
|
|
52
|
+
* single command; anything else gets both, as the server's own "too old" message does.
|
|
53
|
+
*/
|
|
54
|
+
export function claudeUpdateCommands(realPath) {
|
|
55
|
+
const path = String(realPath ?? "").replaceAll("\\", "/");
|
|
56
|
+
if (path.includes("/node_modules/@anthropic-ai/claude-code/")) return [CLAUDE_UPDATE_NPM];
|
|
57
|
+
if (path.includes("/.local/share/claude/")) return [CLAUDE_UPDATE_NATIVE];
|
|
58
|
+
return [CLAUDE_UPDATE_NATIVE, `${CLAUDE_UPDATE_NPM} (if you installed it with npm)`];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const HINT = " → ";
|
|
62
|
+
const COMMAND = " ";
|
|
63
|
+
|
|
64
|
+
function updateLines(headline, commands, fallback, via = null) {
|
|
65
|
+
if (commands.length === 0) return [`${HINT}${headline} Update: ${fallback}`];
|
|
66
|
+
const withTool = via ? " with " + via : "";
|
|
67
|
+
return [`${HINT}${headline} To update${withTool}:`, ...commands.map((command) => `${COMMAND}${command}`)];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The lines printed under `✓ Node …`; empty when there is nothing to say. The commands are the
|
|
72
|
+
* install tool's own upgrade (`nodeUpgradeGuide`), which may land on a newer major than the one
|
|
73
|
+
* named — so the tool is named too.
|
|
74
|
+
*/
|
|
75
|
+
export function nodeDriftLines(drift, upgrade) {
|
|
76
|
+
if (drift.kind === "unknown") return [`${HINT}○ could not check for a newer Node (offline?)`];
|
|
77
|
+
if (drift.kind !== "behind") return [];
|
|
78
|
+
const headline = drift.sameMajor ? `Node ${drift.latest} is out (latest ${majorOf(drift.latest)}.x LTS).` : `Node ${drift.latest} is the latest LTS.`;
|
|
79
|
+
return updateLines(headline, upgrade.commands, NODE_DOWNLOAD_URL, upgrade.via);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The lines printed under `✓ Claude Code CLI`; empty when there is nothing to say. */
|
|
83
|
+
export function claudeDriftLines(drift, localVersion, updateCommands) {
|
|
84
|
+
if (localVersion === null) return [`${HINT}○ could not read the Claude Code version`];
|
|
85
|
+
if (drift.kind === "unknown") return [`${HINT}○ could not check for a newer Claude Code (offline?)`];
|
|
86
|
+
if (drift.kind !== "behind") return [];
|
|
87
|
+
return updateLines(`Claude Code ${localVersion} is older than the stable release ${drift.latest}.`, updateCommands, CLAUDE_UPDATE_NATIVE);
|
|
88
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Reads .blueprint/replies.json — an answer to each question the person asked — and answers one question
|
|
2
2
|
// per mode. Exit 0 is yes.
|
|
3
3
|
// answer every question has exactly one reply; a reply found in the documents quotes them (chaff cite)
|
|
4
|
-
// and names every
|
|
4
|
+
// and names every place it quotes (the address, or the heading of the section there); a reply not found says what was searched. Records the
|
|
5
5
|
// documents' and FAQ.md's fingerprints.
|
|
6
6
|
// keep the documents are unchanged; FAQ.md holds every question after what it held before when the
|
|
7
7
|
// person asked to keep the answers, and is untouched otherwise
|
|
@@ -9,6 +9,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { fromBase } from "./base.mjs";
|
|
10
10
|
const { fail, quotationProblems, readJson } = await import(fromBase("chaff.mjs"));
|
|
11
11
|
const { digest, documentSource, documentsNamed, fingerprint } = await import(fromBase("documents.mjs"));
|
|
12
|
+
const { headingsOf, namesPlace } = await import(fromBase("places.mjs"));
|
|
12
13
|
|
|
13
14
|
const REPLIES = ".blueprint/replies.json";
|
|
14
15
|
const REPLIES_PAGE = ".blueprint/replies.md";
|
|
@@ -29,6 +30,15 @@ if (answers?.keep === KEEP && documents.includes(FAQ)) fail(`${FAQ} is one of th
|
|
|
29
30
|
const indented = (line) => " " + line;
|
|
30
31
|
const nonEmpty = (value) => typeof value === "string" && value.trim() !== "";
|
|
31
32
|
|
|
33
|
+
// Read once per document, and only for one of the documents: a source that is not is refused by the quotation check.
|
|
34
|
+
const headingCache = new Map();
|
|
35
|
+
const headingAt = (source, address) => {
|
|
36
|
+
const resolved = documentSource(documents)(source);
|
|
37
|
+
if (!resolved.path) return undefined;
|
|
38
|
+
if (!headingCache.has(resolved.path)) headingCache.set(resolved.path, headingsOf(resolved.path));
|
|
39
|
+
return headingCache.get(resolved.path).get(address.trim());
|
|
40
|
+
};
|
|
41
|
+
|
|
32
42
|
const replyProblem = (reply) => {
|
|
33
43
|
if (typeof reply !== "object" || reply === null) return "a reply is not an object";
|
|
34
44
|
if (!nonEmpty(reply.question)) return "a reply names no question";
|
|
@@ -39,7 +49,10 @@ const replyProblem = (reply) => {
|
|
|
39
49
|
if (!Array.isArray(citations)) return `${which}: citations must be an array`;
|
|
40
50
|
if (reply.found && citations.length === 0) return `${which}: an answer found in the documents quotes them`;
|
|
41
51
|
if (!citations.every((citation) => nonEmpty(citation?.address))) return `${which}: every quotation needs an "address"`;
|
|
42
|
-
const unnamed = citations.filter(
|
|
52
|
+
const unnamed = citations.filter(
|
|
53
|
+
(citation) =>
|
|
54
|
+
!reply.answer.includes(citation.address.trim()) && !namesPlace(reply.answer, citation.address, headingAt(String(citation.source), citation.address)),
|
|
55
|
+
);
|
|
43
56
|
if (unnamed.length > 0) return `${which}: the answer does not name ${unnamed.map((citation) => citation.address).join(", ")}, which it quotes`;
|
|
44
57
|
if (!reply.found && (!Array.isArray(reply.searched) || !reply.searched.some(nonEmpty))) return `${which}: say what was searched ("searched")`;
|
|
45
58
|
return null;
|
|
@@ -48,11 +48,15 @@ the answers before anything is kept.
|
|
|
48
48
|
- `question`: the question exactly as it is in `questions`. One reply per question, none extra.
|
|
49
49
|
- `found`: `true` when the documents answer it. Then at least one citation.
|
|
50
50
|
- `citations`: `source` is the document's path as `documents` names it; `address` is where; `quote` is
|
|
51
|
-
copied character for character — the check runs `chaff cite` on every one. **
|
|
52
|
-
the `answer` text** so the person can open the document there
|
|
51
|
+
copied character for character — the check runs `chaff cite` on every one. **Name every cited place in
|
|
52
|
+
the `answer` text** so the person can open the document there: an article or item as the document numbers it
|
|
53
|
+
(第4条第2項, Section 3.1), and a section of a Markdown document by its heading, word for word, in 「」 (「作業用フォルダを信頼しておく」;
|
|
54
|
+
in an English answer, "Getting started") — the check looks for it in those marks.
|
|
55
|
+
chaff's index for a section (`h1.3`) goes in `address` only — it means nothing to the person.
|
|
53
56
|
- `searched`: when `found` is `false`, the words and places you looked for.
|
|
54
57
|
|
|
55
|
-
Also write `.blueprint/replies.md` for the person: each question as a heading, the answer, and the quotations
|
|
58
|
+
Also write `.blueprint/replies.md` for the person: each question as a heading, the answer, and the quotations,
|
|
59
|
+
each with its place named as in the answer (never `h1.3`).
|
|
56
60
|
|
|
57
61
|
## Done when
|
|
58
62
|
|
|
@@ -17,7 +17,8 @@ run the check and stop.
|
|
|
17
17
|
Otherwise add to `FAQ.md` in this folder (create it if there is none). **Add after what it already holds;
|
|
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
|
-
written (document and
|
|
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
22
|
too.
|
|
22
23
|
|
|
23
24
|
## Done when
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Types for places.mjs, which the packs run with plain node and the specs import.
|
|
2
|
+
export function headingsIn(tree: unknown): Map<string, string>;
|
|
3
|
+
export function headingsOf(file: string): Map<string, string>;
|
|
4
|
+
export function namesPlace(text: string, address: string, heading: string | undefined): boolean;
|
|
5
|
+
export function placeNamesIn(tree: unknown): Map<string, string>;
|
|
6
|
+
export function placeNamesOf(file: string): Map<string, string>;
|
|
7
|
+
export function placeNamer(sourcePath: (source: string) => { path?: string; problem?: string }): (source: string, address: string) => string;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Where a quotation sits, named for the person who reads it: a section of a Markdown document by its heading,
|
|
2
|
+
// since chaff's address for it (h1.3) tells nobody anything. Other addresses (第4条第2項, 3.1) already read as the
|
|
3
|
+
// document writes them.
|
|
4
|
+
import { runChaff } from "./chaff.mjs";
|
|
5
|
+
|
|
6
|
+
const sectionsIn = (node) => [
|
|
7
|
+
...(node?.kind === "section" && typeof node.address === "string" && typeof node.attrs?.heading === "string" ? [[node.address, node.attrs.heading]] : []),
|
|
8
|
+
...(Array.isArray(node?.children) ? node.children.flatMap(sectionsIn) : []),
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const PARTS = new Set(["section", "article"]);
|
|
12
|
+
|
|
13
|
+
const text = (value) => (typeof value === "string" && value !== "" ? value : null);
|
|
14
|
+
|
|
15
|
+
// A Japanese article (第4条, 第二十一条の二): what is under it is named as Japanese law cites it.
|
|
16
|
+
const JA_ARTICLE = /^第[^条\s]+条/u;
|
|
17
|
+
const halfWidth = (digits) => digits.replace(/[0-9]/gu, (digit) => String.fromCharCode(digit.charCodeAt(0) - 0xfee0));
|
|
18
|
+
|
|
19
|
+
// Under a Japanese article, one level down is a paragraph (第2項, numbered from the address when chaff gives it no
|
|
20
|
+
// label) and anything deeper an item (第三号, by its label). chaff calls both "item", so the depth tells them apart.
|
|
21
|
+
function japaneseName(label, own, above, article) {
|
|
22
|
+
const levels = own.slice(article.address.length + 1).split(".");
|
|
23
|
+
if (levels.length === 1) return `${above}第${halfWidth(label ?? levels[0])}項`;
|
|
24
|
+
return `${above}第${label ?? levels.at(-1)}号`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A part (an article, a section) is named by its label, or by its quoted heading when it has none (a Markdown
|
|
28
|
+
// section); anything below it (an item, a paragraph) adds its own label to the name of what holds it.
|
|
29
|
+
function nameOf(node, own, above, article) {
|
|
30
|
+
const label = text(node?.attrs?.label);
|
|
31
|
+
if (PARTS.has(node?.kind)) {
|
|
32
|
+
const heading = text(node?.attrs?.heading);
|
|
33
|
+
return label ?? (heading === null ? own : `「${heading}」`);
|
|
34
|
+
}
|
|
35
|
+
if (above === null) return label ?? own;
|
|
36
|
+
return article?.japanese && own.startsWith(`${article.address}.`) ? japaneseName(label, own, above, article) : `${above} ${label ?? own}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const partOf = (node, own, name, article) => (PARTS.has(node?.kind) && own !== null ? { address: own, japanese: JA_ARTICLE.test(name ?? "") } : article);
|
|
40
|
+
|
|
41
|
+
const namesIn = (node, above, article) => {
|
|
42
|
+
const own = text(node?.address);
|
|
43
|
+
const name = own === null ? above : nameOf(node, own, above, article);
|
|
44
|
+
const here = own === null ? [] : [[own, name]];
|
|
45
|
+
const within = partOf(node, own, name, article);
|
|
46
|
+
return [...here, ...(Array.isArray(node?.children) ? node.children.flatMap((child) => namesIn(child, name, within)) : [])];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Every address in a chaff tree, with the name a person reads for it: 第4条第2項, Section 3.2 (a) (i), 「用意するもの」. */
|
|
50
|
+
export const placeNamesIn = (tree) => new Map(namesIn(tree, null, null));
|
|
51
|
+
|
|
52
|
+
/** The place names of `file`, by address; none when chaff cannot read its tree. */
|
|
53
|
+
export const placeNamesOf = (file) => {
|
|
54
|
+
const run = runChaff(["tree", file, "--format", "json"]);
|
|
55
|
+
if (run.code !== 0) return new Map();
|
|
56
|
+
try {
|
|
57
|
+
return placeNamesIn(JSON.parse(run.stdout));
|
|
58
|
+
} catch {
|
|
59
|
+
return new Map();
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Where a citation sits, for a view a person reads: its place name from the document's tree, read once per document
|
|
65
|
+
* and only for one of `documents` (through `sourcePath`, which resolves or refuses a source); the address itself
|
|
66
|
+
* when there is no name.
|
|
67
|
+
*/
|
|
68
|
+
export const placeNamer = (sourcePath) => {
|
|
69
|
+
const cache = new Map();
|
|
70
|
+
return (source, address) => {
|
|
71
|
+
const resolved = sourcePath(source);
|
|
72
|
+
if (!resolved.path) return address;
|
|
73
|
+
if (!cache.has(resolved.path)) cache.set(resolved.path, placeNamesOf(resolved.path));
|
|
74
|
+
return cache.get(resolved.path).get(String(address).trim()) ?? address;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Every section address in a chaff tree (`tree --format json`), with its heading. */
|
|
79
|
+
export const headingsIn = (tree) => new Map(sectionsIn(tree));
|
|
80
|
+
|
|
81
|
+
/** The section headings of `file`, by address; none when chaff cannot read its tree. */
|
|
82
|
+
export const headingsOf = (file) => {
|
|
83
|
+
const run = runChaff(["tree", file, "--format", "json"]);
|
|
84
|
+
if (run.code !== 0) return new Map();
|
|
85
|
+
try {
|
|
86
|
+
return headingsIn(JSON.parse(run.stdout));
|
|
87
|
+
} catch {
|
|
88
|
+
return new Map();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// A heading counts only in quotation marks: a short one (期限) is also an ordinary word, and bare it would be found by accident.
|
|
93
|
+
const QUOTES = [
|
|
94
|
+
["「", "」"],
|
|
95
|
+
['"', '"'],
|
|
96
|
+
["“", "”"],
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/** Whether `text` names a place: by its address as written, or by the quoted heading of the section at that address. */
|
|
100
|
+
export const namesPlace = (text, address, heading) =>
|
|
101
|
+
text.includes(address.trim()) ||
|
|
102
|
+
(typeof heading === "string" && heading !== "" && QUOTES.some(([open, close]) => text.includes(`${open}${heading}${close}`)));
|
|
@@ -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.13`) 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.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The security review's report exists, walks every OWASP Top 10:2025 category, and leaves no HIGH or
|
|
3
|
+
# MEDIUM finding open or merely accepted. A finding is one line: `- <HIGH|MEDIUM|LOW> <fixed|open|accepted>: …`.
|
|
4
|
+
set -eu
|
|
5
|
+
report=.blueprint/security-review.md
|
|
6
|
+
[ -s "$report" ] || { echo "missing $report" >&2; exit 1; }
|
|
7
|
+
for category in A01 A02 A03 A04 A05 A06 A07 A08 A09 A10; do
|
|
8
|
+
grep -q "^#.*$category" "$report" || { echo "$report has no section for $category (OWASP Top 10:2025)" >&2; exit 1; }
|
|
9
|
+
done
|
|
10
|
+
open=$(grep -nE '^- (HIGH|MEDIUM) (open|accepted)' "$report" || true)
|
|
11
|
+
[ -z "$open" ] || { printf 'HIGH / MEDIUM findings must be fixed, but are open or accepted in %s:\n%s\n' "$report" "$open" >&2; exit 1; }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The security review is proven by what ships, not by the review's word: the rules refuse what the
|
|
3
|
+
# review's emulator tests say they refuse, the rules end in deny-all, Hosting sends the headers a browser
|
|
4
|
+
# needs, no service account key is in the project, the dependencies have no known high-severity hole,
|
|
5
|
+
# the review's own report is in place, and dev runs what the review left — so production, which ships
|
|
6
|
+
# what dev ran, gets the fixes, and a header that breaks the page shows up here, on dev.
|
|
7
|
+
set -eu
|
|
8
|
+
here="$(dirname "$0")"
|
|
9
|
+
sh "$here/security-report.sh"
|
|
10
|
+
grep -qE 'allow +read, *write: *if +false' firestore.rules || { echo "firestore.rules does not end in a deny-all (allow read, write: if false)" >&2; exit 1; }
|
|
11
|
+
node -e 'const fs=require("node:fs");
|
|
12
|
+
const hosting=[JSON.parse(fs.readFileSync("firebase.json","utf8")).hosting].flat().filter(Boolean);
|
|
13
|
+
if (hosting.length===0) { console.error("firebase.json has no hosting section"); process.exit(1); }
|
|
14
|
+
for (const site of hosting) {
|
|
15
|
+
const all=(site.headers??[]).filter((h)=>h.source==="**").flatMap((h)=>h.headers??[]);
|
|
16
|
+
const value=(key)=>all.find((h)=>h.key.toLowerCase()===key)?.value??"";
|
|
17
|
+
const missing=[];
|
|
18
|
+
if (!/nosniff/i.test(value("x-content-type-options"))) missing.push("X-Content-Type-Options: nosniff");
|
|
19
|
+
if (!value("content-security-policy")) missing.push("Content-Security-Policy");
|
|
20
|
+
if (!/frame-ancestors/i.test(value("content-security-policy")) && !/deny|sameorigin/i.test(value("x-frame-options"))) missing.push("frame-ancestors or X-Frame-Options");
|
|
21
|
+
if (missing.length) { console.error("firebase.json hosting headers for ** lack: "+missing.join(", ")); process.exit(1); }
|
|
22
|
+
}'
|
|
23
|
+
keys=$(grep -rlE '"private_key"|-----BEGIN [A-Z ]*PRIVATE KEY-----' . --exclude-dir=node_modules --exclude-dir=.firebase --exclude-dir=dist 2>/dev/null || true)
|
|
24
|
+
[ -z "$keys" ] || { printf 'a private key is in the project; move it out and revoke it:\n%s\n' "$keys" >&2; exit 1; }
|
|
25
|
+
audit() {
|
|
26
|
+
# Read from the summary, not the exit code: an audit that could not reach the registry must not pass.
|
|
27
|
+
(cd "$1" && yarn audit --groups dependencies --json 2>/dev/null) | node -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{
|
|
28
|
+
const summary = s.split("\n").filter(Boolean).map((line) => JSON.parse(line)).find((entry) => entry.type === "auditSummary");
|
|
29
|
+
if (!summary) { console.error(process.argv[1] + ": yarn audit did not report a summary (is the registry reachable?)"); process.exit(1); }
|
|
30
|
+
const { high = 0, critical = 0 } = summary.data.vulnerabilities;
|
|
31
|
+
if (high + critical > 0) { console.error(`${process.argv[1]}: yarn audit finds ${high} high and ${critical} critical vulnerabilities in dependencies`); process.exit(1); }
|
|
32
|
+
})' "$1"
|
|
33
|
+
}
|
|
34
|
+
audit .
|
|
35
|
+
if [ -f functions/package.json ]; then
|
|
36
|
+
[ -f functions/yarn.lock ] || { echo "functions/ has no yarn.lock; install its dependencies with yarn so they can be audited" >&2; exit 1; }
|
|
37
|
+
audit functions
|
|
38
|
+
fi
|
|
39
|
+
sh "$here/emulator-test.sh" security
|
|
40
|
+
[ .blueprint/build-id -nt .blueprint/security-review.md ] || { echo "dev was not deployed after the review; deploy it to dev again so production ships what dev runs" >&2; exit 1; }
|
|
41
|
+
sh "$here/deploy-check.sh" dev
|
|
@@ -6,3 +6,7 @@ if grep -n '{{' .blueprint/spec.md >&2; then
|
|
|
6
6
|
echo "unfilled placeholders are left in .blueprint/spec.md" >&2
|
|
7
7
|
exit 1
|
|
8
8
|
fi
|
|
9
|
+
# A usecase may hold its spec to more than the template: when it ships its own spec check, that runs too.
|
|
10
|
+
if [ -n "${BLUEPRINT_USECASE:-}" ] && [ -f "$BLUEPRINT_USECASE/checks/spec.sh" ]; then
|
|
11
|
+
sh "$BLUEPRINT_USECASE/checks/spec.sh"
|
|
12
|
+
fi
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# The first page the finished build shows says where the published app is and what to try to see it works.
|
|
3
|
+
set -eu
|
|
4
|
+
start=.blueprint/start-here.md
|
|
5
|
+
[ -s "$start" ] || { echo "missing $start" >&2; exit 1; }
|
|
6
|
+
prod=$(sh "$(dirname "$0")/project-id.sh" prod)
|
|
7
|
+
grep -q "https://$prod.web.app" "$start" || { echo "$start does not name the production URL (https://$prod.web.app)" >&2; exit 1; }
|
|
8
|
+
grep -q '^- \[ \] ' "$start" || { echo "$start has no checklist of what to try (- [ ] …)" >&2; exit 1; }
|