strom-research 1.22.1 → 1.22.2
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/CHANGELOG.cs.md +15 -0
- package/CHANGELOG.de.md +17 -0
- package/CHANGELOG.md +15 -0
- package/assets/lang/cs.json +14 -0
- package/assets/lang/de.json +14 -0
- package/dist/agents/files.js +12 -2
- package/dist/cli/main.js +9 -0
- package/dist/cli/menu.js +7 -1
- package/dist/cli/seal.js +45 -2
- package/dist/cli/ui.js +14 -0
- package/dist/commands/checks.js +19 -6
- package/dist/commands/setup.js +38 -3
- package/dist/core/changelog.js +1 -1
- package/dist/core/git.js +2 -2
- package/dist/core/integrity.js +104 -14
- package/dist/core/live.js +4 -0
- package/dist/core/oldsettings.js +101 -0
- package/dist/core/packaged.js +69 -0
- package/dist/core/paths.js +9 -5
- package/dist/core/seal.js +51 -3
- package/dist/core/tree.js +11 -5
- package/package.json +1 -1
package/CHANGELOG.cs.md
CHANGED
|
@@ -5,6 +5,21 @@
|
|
|
5
5
|
Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou na
|
|
6
6
|
[GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases) (anglicky).
|
|
7
7
|
|
|
8
|
+
## 1.22.2
|
|
9
|
+
|
|
10
|
+
*30. 10. 2026 · patří k aplikaci Strom 3.17.1.*
|
|
11
|
+
|
|
12
|
+
- **Strom z desktopové aplikace agenta a z terminálu zase zapisují do jednoho výzkumu (Windows).** Desktopová aplikace
|
|
13
|
+
agenta může být pro Windows zabalená (MSIX) a každý soubor, který strom spuštěný z ní vytvořil v nastavení stromu, skončil
|
|
14
|
+
v soukromé kopii aplikace: strom z aplikace a strom z terminálu měly každý vlastní pečetní klíč a každý bral zápisy
|
|
15
|
+
druhého jako změnu mimo strom. Nastavení je teď mimo AppData (`%USERPROFILE%\.config\strom`) a samo se převezme ze
|
|
16
|
+
staré složky i z kopie aplikace (obě zůstanou, jak byly). Rozdvojený výzkum se opraví sám, kde to jde; jinak
|
|
17
|
+
klíče sjednotí `strom seal adopt` v terminálu (nabídka ho jednou nabídne) – každý záznam zkontrolovaný, nic se
|
|
18
|
+
neztratí, nikdy `strom repair`. `strom update` spuštěný z takové aplikace nic nenainstaluje (viděla by to jen ona)
|
|
19
|
+
a řekne, ať se spustí v terminálu; `strom doctor` najde kopii programu, kterou si aplikace drží pro sebe.
|
|
20
|
+
- **`strom repair` po commitu přes git, který nezměnil žádný záznam** (prázdný, jen poznámky), výzkum znovu zapečetí;
|
|
21
|
+
dřív commit zůstal nezapečetěný, každý zápis byl odmítnut a nebylo co opravit.
|
|
22
|
+
|
|
8
23
|
## 1.22.1
|
|
9
24
|
|
|
10
25
|
*29. 10. 2026 · patří k aplikaci Strom 3.17.0.*
|
package/CHANGELOG.de.md
CHANGED
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Installationszeilen stehen bei
|
|
6
6
|
[GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases) (auf Englisch).
|
|
7
7
|
|
|
8
|
+
## 1.22.2
|
|
9
|
+
|
|
10
|
+
*30.10.2026 · gehört zur Strom-App 3.17.1.*
|
|
11
|
+
|
|
12
|
+
- **strom aus der Desktop-App eines Agenten und aus einem Terminal schreiben wieder in eine Forschung (Windows).** Die
|
|
13
|
+
Desktop-App eines Agenten kann für Windows verpackt sein (MSIX), und jede Datei, die ein von ihr gestartetes strom in den Einstellungen
|
|
14
|
+
von strom anlegte, landete in der privaten Kopie der App: das strom der App und das des Terminals hatten je einen
|
|
15
|
+
eigenen Siegelschlüssel, und jedes hielt die Schreibvorgänge des anderen für Änderungen außerhalb von strom. Die
|
|
16
|
+
Einstellungen liegen jetzt außerhalb von AppData (`%USERPROFILE%\.config\strom`) und werden aus dem alten Ordner
|
|
17
|
+
und aus der Kopie der App von selbst übernommen (beide bleiben, wie sie waren). Eine so geteilte Forschung heilt sich
|
|
18
|
+
selbst, wo es geht; sonst vereint `strom seal adopt` in einem Terminal die Schlüssel (das Menü bietet es einmal an) –
|
|
19
|
+
jeder Datensatz geprüft, nichts geht verloren, nie `strom repair`. Ein aus einer solchen App gestartetes
|
|
20
|
+
`strom update` installiert nichts (nur die App sähe es) und sagt, es im Terminal auszuführen; `strom doctor` findet
|
|
21
|
+
eine Kopie des Programms, die eine App für sich behält.
|
|
22
|
+
- **`strom repair` nach einem mit git gemachten Commit, der keinen Datensatz änderte** (leer, nur Notizen), versiegelt
|
|
23
|
+
die Forschung wieder; vorher blieb der Commit unversiegelt, jeder Schreibvorgang wurde abgelehnt, und es gab nichts zu reparieren.
|
|
24
|
+
|
|
8
25
|
## 1.22.1
|
|
9
26
|
|
|
10
27
|
*29.10.2026 · gehört zur Strom-App 3.17.0.*
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ English · [Čeština](CHANGELOG.cs.md) · [Deutsch](CHANGELOG.de.md)
|
|
|
5
5
|
The changes of each version in short. The full notes and the install lines are on
|
|
6
6
|
[GitHub Releases](https://github.com/ACiDekCZ/strom-research/releases).
|
|
7
7
|
|
|
8
|
+
## 1.22.2
|
|
9
|
+
|
|
10
|
+
*2026-10-30 · goes with the Strom app 3.17.1.*
|
|
11
|
+
|
|
12
|
+
- **Strom from an agent's desktop app and from a terminal write to one research again (Windows).** An agent's
|
|
13
|
+
desktop app can be packaged for Windows (MSIX), and every file a strom it started made in strom's settings went to
|
|
14
|
+
the app's private copy: the app's strom and the terminal's each had a seal key of their own, and each refused the other's writes as
|
|
15
|
+
made outside strom. The settings are now outside AppData (`%USERPROFILE%\.config\strom`), taken over from the old
|
|
16
|
+
folder and from the app's copy by themselves (both left as they were). A research split so heals itself where it can;
|
|
17
|
+
else `strom seal adopt` in a terminal unites the keys (the menu offers it once) — every record checked, nothing lost,
|
|
18
|
+
never `strom repair`. `strom update` started from such an app installs nothing (only the app would see it) and says
|
|
19
|
+
to run it in a terminal; `strom doctor` finds a copy of the program an app keeps for itself.
|
|
20
|
+
- **`strom repair` after a commit made with git that changed no record** (an empty one, notes only) seals the research
|
|
21
|
+
again; before, the commit stayed unsealed, every write was refused and there was nothing to repair.
|
|
22
|
+
|
|
8
23
|
## 1.22.1
|
|
9
24
|
|
|
10
25
|
*2026-10-29 · goes with the Strom app 3.17.0.*
|
package/assets/lang/cs.json
CHANGED
|
@@ -569,6 +569,8 @@
|
|
|
569
569
|
"ui.tree.created": "✓ Rodokmen „{name}“ je založený: {dir}",
|
|
570
570
|
"ui.chat.trust": "Claude Code se poprvé zeptá, jestli této složce důvěřovat. Je to složka rodokmenu, kterou vytvořil strom – stačí potvrdit „Yes, proceed“.",
|
|
571
571
|
"ui.doc.program": "program",
|
|
572
|
+
"ui.doc.packaged": "kopie programu",
|
|
573
|
+
"ui.doc.packaged.copy": "aplikace zabalená pro Windows má vlastní kopii programu: {folders} – ta aplikace spouští jiný strom než terminál",
|
|
572
574
|
"ui.doc.git": "git (historie)",
|
|
573
575
|
"ui.doc.home": "složka výzkumu",
|
|
574
576
|
"ui.doc.gitdir": "git ve složce výzkumů",
|
|
@@ -636,6 +638,7 @@
|
|
|
636
638
|
"ui.doc.appurl.env.fix": "proměnnou STROM_APP_URL nastavit na adresu aplikace Strom ({web}, nebo její kopie na tomto počítači http://127.0.0.1:<port>/), nebo odebrat",
|
|
637
639
|
"ui.doc.tree.ok": "{name}: data v pořádku a zapečetěná",
|
|
638
640
|
"ui.doc.tree.bad": "{name}: {n, plural, one {# problém} few {# problémy} other {# problémů}}",
|
|
641
|
+
"ui.doc.tree.twokeys": "{name}: zapečetěno dvěma klíči stromu na tomto počítači (strom z aplikace a z terminálu) – zápisy obou jsou stromu",
|
|
639
642
|
"ui.doc.write": "zápis do rodokmenu",
|
|
640
643
|
"ui.doc.write.ok": "do jeho .strom i .git lze zapisovat",
|
|
641
644
|
"ui.doc.write.sandbox": "{where}: zápis odepřen – strom běží pod agentem, nejspíš to zakazuje jeho sandbox",
|
|
@@ -672,6 +675,8 @@
|
|
|
672
675
|
"ui.fix.git.inwindow": "Dokončit instalaci v okně systému a pak znovu spustit strom doctor.",
|
|
673
676
|
"ui.fix.failed": "Nepovedlo se: {detail}",
|
|
674
677
|
"ui.fix.app": "Otevřít aplikaci Strom v prohlížeči a nainstalovat ji?",
|
|
678
|
+
"ui.fix.packaged": "Přesunout kopii programu, kterou si aplikace drží pro sebe, do koše (aplikace pak spouští strom z terminálu)?",
|
|
679
|
+
"ui.fix.packaged.done": "V koši: {folder}",
|
|
675
680
|
"ui.fix.nothing": "Nic, co by strom opravil sám – viz šipky.",
|
|
676
681
|
"ui.why.git.human": "chybí git – strom v něm uchovává historii výzkumu",
|
|
677
682
|
"ui.why.git.agent": "chybí git (strom v něm uchovává historii výzkumu): řekni to uživateli a spusť tohle – uživatel to potvrdí v okně systému",
|
|
@@ -2565,6 +2570,8 @@
|
|
|
2565
2570
|
"ui.error.folder.in-program.default.hint": "Zadat složku mimo ni: strom setup --home \"{suggested}\" --yes",
|
|
2566
2571
|
"ui.error.update.in-program": "{folder} leží uvnitř {entry}, kterou aktualizace nahrazuje – výzkum by šel s ní. Nic se nenainstalovalo.",
|
|
2567
2572
|
"ui.error.update.in-program.hint": "Nejdřív přesunout výzkum ze složky programu strom: strom setup ve vlastním terminálu (otázka na složku ho přesune i s výzkumem), pak znovu strom update",
|
|
2573
|
+
"ui.error.update.packaged": "Tento strom spustila aplikace zabalená pro Windows (MSIX): co by aktualizace nainstalovala do {folder}, uvidí jen ta aplikace, terminál by zůstal na {current}. Nic se nenainstalovalo.",
|
|
2574
|
+
"ui.error.update.packaged.hint": "strom update ve vlastním terminálu (PowerShell); aplikace novou verzi převezme odtud",
|
|
2568
2575
|
"ui.error.tree.newer": "Tento rodokmen zapsal strom {by}, novější než tento strom {version}. Tento strom ho nechává beze změny.",
|
|
2569
2576
|
"ui.error.tree.newer.hint": "Aktualizovat strom: strom update",
|
|
2570
2577
|
"ui.error.tree.newer.backup": "Tento rodokmen zapsal strom {by}, novější než tento strom {version}. Tento strom ho nechává beze změny.",
|
|
@@ -3087,9 +3094,16 @@
|
|
|
3087
3094
|
"ui.seal.ask": "Převzít „{name}“ na tomto počítači? Záznamy jsou zkontrolované a beze změn.",
|
|
3088
3095
|
"ui.seal.adopted": "„{name}“ je teď zapečetěný na tomto počítači – zapisovat jde tady",
|
|
3089
3096
|
"ui.seal.already": "„{name}“ je na tomto počítači zapečetěný už",
|
|
3097
|
+
"ui.seal.unite.ask": "„{name}“ je na tomto počítači zapečetěný dvěma klíči stromu (strom z aplikace a z terminálu, každý s vlastním nastavením). Sjednotit je? Záznamy jsou zkontrolované: nic se nezměnilo mimo strom, nic se neztratí.",
|
|
3098
|
+
"ui.seal.united": "„{name}“ je teď na tomto počítači zapečetěný jedním klíčem – zapisovat jde z aplikace i z terminálu",
|
|
3099
|
+
"ui.error.seal.other-key": "výzkum „{name}“ je na tomto počítači zapečetěný dvěma klíči stromu (strom z aplikace a z terminálu, každý s vlastním nastavením): nic se nezapsalo",
|
|
3100
|
+
"ui.error.seal.other-key.hint": "sjednotit ve vlastním terminálu: {set} – nikdy strom repair, ten by tu práci vrátil zpět",
|
|
3101
|
+
"ui.check.otherkey": "zapečetěno dvěma klíči stromu na tomto počítači (strom z aplikace a z terminálu): zápisy obou jsou stromu",
|
|
3102
|
+
"ui.check.otherkey.hint": "sjednotit ve vlastním terminálu: strom seal adopt – nikdy strom repair",
|
|
3090
3103
|
"ui.seal.problems": "nepřevzato: záznamy nesedí se svou pečetí – co, řekne strom check",
|
|
3091
3104
|
"ui.menu.seal": "Zapečetěno na jiném počítači: zapisovat tu jde až po převzetí (strom seal adopt).",
|
|
3092
3105
|
"ui.seal.bridge": "výzkum „{name}“ je zapečetěný na jiném počítači: nic se nezapsalo – po převzetí na tomto počítači (terminál: strom seal adopt, nebo nabídka strom) poslat znovu",
|
|
3106
|
+
"ui.seal.bridge.twokeys": "výzkum „{name}“ je na tomto počítači zapečetěný dvěma klíči stromu (strom z aplikace a z terminálu): nic se nezapsalo – po sjednocení (terminál: strom seal adopt, nebo nabídka strom) poslat znovu",
|
|
3093
3107
|
"ui.check.foreignseal": "zapečetěno na jiném počítači (nebo tu chybí nastavení stromu): číst jde, zapisovat až po převzetí tady",
|
|
3094
3108
|
"ui.check.foreignseal.hint": "převzít ve vlastním terminálu: strom seal adopt – nebo v nabídce (strom)",
|
|
3095
3109
|
"ui.why.seal.human": "výzkum je zapečetěný na jiném počítači: zapisovat tu jde až po převzetí (potvrzuje se v tomto terminálu)",
|
package/assets/lang/de.json
CHANGED
|
@@ -569,6 +569,8 @@
|
|
|
569
569
|
"ui.tree.created": "✓ Der Stammbaum „{name}“ ist angelegt: {dir}",
|
|
570
570
|
"ui.chat.trust": "Beim ersten Mal fragt Claude Code, ob diesem Ordner vertraut wird. Es ist der Ordner des Stammbaums, von strom angelegt – mit „Yes, proceed“ bestätigen.",
|
|
571
571
|
"ui.doc.program": "Programm",
|
|
572
|
+
"ui.doc.packaged": "Kopie des Programms",
|
|
573
|
+
"ui.doc.packaged.copy": "eine für Windows verpackte App hat eine eigene Kopie des Programms: {folders} – diese App startet ein anderes strom als das Terminal",
|
|
572
574
|
"ui.doc.git": "Git (Verlauf)",
|
|
573
575
|
"ui.doc.home": "Forschungsordner",
|
|
574
576
|
"ui.doc.gitdir": "git im Forschungsordner",
|
|
@@ -636,6 +638,7 @@
|
|
|
636
638
|
"ui.doc.appurl.env.fix": "die Variable STROM_APP_URL auf eine Adresse der Strom-App setzen ({web}, oder eine Kopie auf diesem Computer http://127.0.0.1:<port>/) oder entfernen",
|
|
637
639
|
"ui.doc.tree.ok": "{name}: Daten in Ordnung und versiegelt",
|
|
638
640
|
"ui.doc.tree.bad": "{name}: {n, plural, one {# Problem} other {# Probleme}}",
|
|
641
|
+
"ui.doc.tree.twokeys": "{name}: mit zwei Schlüsseln von strom auf diesem Computer versiegelt (strom einer App und eines Terminals) – die Schreibvorgänge beider sind von strom",
|
|
639
642
|
"ui.doc.write": "Schreiben in diesen Stammbaum",
|
|
640
643
|
"ui.doc.write.ok": "in sein .strom und .git kann geschrieben werden",
|
|
641
644
|
"ui.doc.write.sandbox": "{where}: Schreiben verweigert – strom läuft unter einem Agenten, wahrscheinlich verbietet es dessen Sandbox",
|
|
@@ -672,6 +675,8 @@
|
|
|
672
675
|
"ui.fix.git.inwindow": "Die Installation im Fenster des Systems abschließen, dann strom doctor erneut starten.",
|
|
673
676
|
"ui.fix.failed": "Das hat nicht geklappt: {detail}",
|
|
674
677
|
"ui.fix.app": "Die Strom-App im Browser öffnen, um sie zu installieren?",
|
|
678
|
+
"ui.fix.packaged": "Die Kopie des Programms, die eine App für sich behält, in den Papierkorb legen (die App startet dann das strom des Terminals)?",
|
|
679
|
+
"ui.fix.packaged.done": "Im Papierkorb: {folder}",
|
|
675
680
|
"ui.fix.nothing": "Nichts, was strom selbst beheben kann – siehe die Pfeile.",
|
|
676
681
|
"ui.why.git.human": "Git fehlt – strom bewahrt darin den Verlauf der Forschung auf",
|
|
677
682
|
"ui.why.git.agent": "Git fehlt (strom bewahrt darin den Verlauf auf): sag es dem Benutzer und führe dies aus – der Benutzer bestätigt in einem Fenster des Systems",
|
|
@@ -2565,6 +2570,8 @@
|
|
|
2565
2570
|
"ui.error.folder.in-program.default.hint": "Einen Ordner außerhalb angeben: strom setup --home \"{suggested}\" --yes",
|
|
2566
2571
|
"ui.error.update.in-program": "{folder} liegt in {entry}, das das Update ersetzt – die Forschung ginge mit. Nichts wurde installiert.",
|
|
2567
2572
|
"ui.error.update.in-program.hint": "Zuerst die Forschung aus dem Programmordner von strom verschieben: strom setup in einem eigenen Terminal (die Frage nach dem Ordner verschiebt sie mit), dann erneut strom update",
|
|
2573
|
+
"ui.error.update.packaged": "Dieses strom wurde von einer für Windows verpackten App (MSIX) gestartet: was das Update in {folder} installierte, sähe nur diese App, das Terminal bliebe bei {current}. Nichts wurde installiert.",
|
|
2574
|
+
"ui.error.update.packaged.hint": "strom update in einem eigenen Terminal (PowerShell); die App übernimmt die neue Version von dort",
|
|
2568
2575
|
"ui.error.tree.newer": "Diesen Stammbaum hat strom {by} geschrieben, neuer als dieses strom {version}. Dieses strom lässt ihn unverändert.",
|
|
2569
2576
|
"ui.error.tree.newer.hint": "strom aktualisieren: strom update",
|
|
2570
2577
|
"ui.error.tree.newer.backup": "Diesen Stammbaum hat strom {by} geschrieben, neuer als dieses strom {version}. Dieses strom lässt ihn unverändert.",
|
|
@@ -3087,9 +3094,16 @@
|
|
|
3087
3094
|
"ui.seal.ask": "„{name}“ auf diesem Computer übernehmen? Die Einträge sind geprüft und unverändert.",
|
|
3088
3095
|
"ui.seal.adopted": "„{name}“ ist jetzt auf diesem Computer versiegelt – schreiben geht hier",
|
|
3089
3096
|
"ui.seal.already": "„{name}“ ist auf diesem Computer schon versiegelt",
|
|
3097
|
+
"ui.seal.unite.ask": "„{name}“ ist auf diesem Computer mit zwei Schlüsseln von strom versiegelt (strom aus einer App und aus einem Terminal, jedes mit eigenen Einstellungen). Vereinen? Alle Einträge sind geprüft: nichts außerhalb von strom geändert, nichts geht verloren.",
|
|
3098
|
+
"ui.seal.united": "„{name}“ ist jetzt auf diesem Computer mit einem Schlüssel versiegelt – schreiben geht aus der App wie aus dem Terminal",
|
|
3099
|
+
"ui.error.seal.other-key": "die Forschung „{name}“ ist auf diesem Computer mit zwei Schlüsseln von strom versiegelt (strom aus einer App und aus einem Terminal, jedes mit eigenen Einstellungen): nichts wurde geschrieben",
|
|
3100
|
+
"ui.error.seal.other-key.hint": "vereinen in einem eigenen Terminal: {set} – nie strom repair, das würde diese Arbeit rückgängig machen",
|
|
3101
|
+
"ui.check.otherkey": "mit zwei Schlüsseln von strom auf diesem Computer versiegelt (strom aus einer App und aus einem Terminal): was beide schrieben, ist von strom",
|
|
3102
|
+
"ui.check.otherkey.hint": "vereinen in einem eigenen Terminal: strom seal adopt – nie strom repair",
|
|
3090
3103
|
"ui.seal.problems": "nicht übernommen: was im Stammbaum steht, ist nicht so versiegelt, wie strom es schrieb – was, sagt strom check",
|
|
3091
3104
|
"ui.menu.seal": "Auf einem anderen Computer versiegelt: schreiben hier erst nach der Übernahme (strom seal adopt).",
|
|
3092
3105
|
"ui.seal.bridge": "die Forschung „{name}“ ist auf einem anderen Computer versiegelt: nichts wurde geschrieben – nach der Übernahme auf diesem Computer (Terminal: strom seal adopt, oder das Menü von strom) erneut senden",
|
|
3106
|
+
"ui.seal.bridge.twokeys": "die Forschung „{name}“ ist auf diesem Computer mit zwei Schlüsseln von strom versiegelt (strom einer App und eines Terminals): nichts wurde geschrieben – nach dem Vereinen (Terminal: strom seal adopt, oder das Menü von strom) erneut senden",
|
|
3093
3107
|
"ui.check.foreignseal": "auf einem anderen Computer versiegelt (oder die Einstellungen von strom fehlen hier): lesen geht, schreiben erst nach der Übernahme hier",
|
|
3094
3108
|
"ui.check.foreignseal.hint": "übernehmen in einem eigenen Terminal: strom seal adopt – oder im Menü (strom)",
|
|
3095
3109
|
"ui.why.seal.human": "die Forschung ist auf einem anderen Computer versiegelt: schreiben hier erst nach der Übernahme (bestätigt in diesem Terminal)",
|
package/dist/agents/files.js
CHANGED
|
@@ -14,6 +14,7 @@ import { isArchive } from "../core/mode.js";
|
|
|
14
14
|
import { Settings } from "../core/config.js";
|
|
15
15
|
import { agentAppUrl, appSite } from "../core/stromapp.js";
|
|
16
16
|
import { configDir, ownCommand } from "../core/paths.js";
|
|
17
|
+
import { oldSettingsDirs, packagedSettingsPattern } from "../core/oldsettings.js";
|
|
17
18
|
import { within } from "../core/relocate.js";
|
|
18
19
|
import { agentBrowser } from "../core/connector.js";
|
|
19
20
|
import { CHROME_ALLOW, CHROME_DENY, chromeDomain } from "../core/browser.js";
|
|
@@ -212,6 +213,15 @@ export function settingsKeptFrom(dir, research, platform = process.platform) {
|
|
|
212
213
|
return { path: p, folder };
|
|
213
214
|
});
|
|
214
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* What of strom's settings is kept from the agents: of today's folder, of each it was kept in before (oldsettings.ts),
|
|
218
|
+
* and every packaged app's copy whole, one made after these rules too.
|
|
219
|
+
*/
|
|
220
|
+
function keptSettings(tree, settings) {
|
|
221
|
+
const research = researchFolders(tree, settings);
|
|
222
|
+
const packaged = packagedSettingsPattern(tree.env);
|
|
223
|
+
return [...[configDir(tree.env), ...oldSettingsDirs(tree.env)].flatMap((d) => settingsKeptFrom(d, research)), ...(packaged ? [{ path: packaged, folder: true }] : [])];
|
|
224
|
+
}
|
|
215
225
|
/** The folders of the research the agent works in: its home, the shared folder, the tree. */
|
|
216
226
|
function researchFolders(tree, settings) {
|
|
217
227
|
return [settings.home()?.value, settings.shared()?.value, tree.root].filter((p) => !!p);
|
|
@@ -227,7 +237,7 @@ function treeRules(tree, agent) {
|
|
|
227
237
|
const shared = settings.shared()?.value;
|
|
228
238
|
const claude = agent === "claude";
|
|
229
239
|
const at = (abs) => (claude ? permissionPath(abs) : abs.replace(/\\/g, "/"));
|
|
230
|
-
const kept =
|
|
240
|
+
const kept = keptSettings(tree, settings).map((k) => (k.folder ? `${at(k.path)}/**` : at(k.path)));
|
|
231
241
|
// Browser tools (Claude in Chrome): for the sites of the connectors fetching through the browser — or, agent.browser
|
|
232
242
|
// always, of every archive of the research (anything else is asked in a conversation, not used in a run).
|
|
233
243
|
const web = claude ? agentBrowser(tree, shared) : { on: false, hosts: [] };
|
|
@@ -402,7 +412,7 @@ export function opencodeConfig(tree) {
|
|
|
402
412
|
}
|
|
403
413
|
: {}),
|
|
404
414
|
// the seal keys and the person's settings, and what the browser downloads (strom's to take over)
|
|
405
|
-
...Object.fromEntries(
|
|
415
|
+
...Object.fromEntries(keptSettings(tree, settings).map((k) => [k.folder ? `${abs(k.path)}/*` : abs(k.path), "deny"])),
|
|
406
416
|
[`${abs(settings.downloads())}/*`]: "deny",
|
|
407
417
|
},
|
|
408
418
|
webfetch: "allow",
|
package/dist/cli/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import { commands, match } from "./registry.js";
|
|
|
13
13
|
import { asCommand, asCommandJson, shellArg } from "./format.js";
|
|
14
14
|
import { groupHelpAs, helpAs } from "./help.js";
|
|
15
15
|
import { autoCommit } from "./commit.js";
|
|
16
|
+
import { takeOverOldSettings } from "../core/oldsettings.js";
|
|
16
17
|
import { assertIntact } from "../core/integrity.js";
|
|
17
18
|
import { head as gitHead, resetCache } from "../core/git.js";
|
|
18
19
|
import { newerTree, Tree, VERSION } from "../core/tree.js";
|
|
@@ -226,6 +227,14 @@ async function mainOf(argv, io, env, cwd) {
|
|
|
226
227
|
resetCache(undefined, "command");
|
|
227
228
|
// the app's install line in its one variable (STROM_FROM): read as the five of an older line
|
|
228
229
|
expandFromLine(env);
|
|
230
|
+
// strom's settings on Windows before (%APPDATA%\strom and a packaged app's copy of it) taken over, before anything
|
|
231
|
+
// reads them (core/oldsettings.ts)
|
|
232
|
+
try {
|
|
233
|
+
takeOverOldSettings(env);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// taken over next time; the old ones are read meanwhile
|
|
237
|
+
}
|
|
229
238
|
// what ran and how long, for the reminder of the session's time
|
|
230
239
|
const startedAt = Date.now();
|
|
231
240
|
let ran;
|
package/dist/cli/menu.js
CHANGED
|
@@ -10,7 +10,7 @@ import fs from "node:fs";
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { ui } from "./ui.js";
|
|
12
12
|
import { isAgent } from "../core/which.js";
|
|
13
|
-
import { offerAdopt } from "./seal.js";
|
|
13
|
+
import { offerAdopt, offerUnite } from "./seal.js";
|
|
14
14
|
import { mb, setTidyAsked, tidyAskAgain, tidyAsked, tidyOn, tidyPlan, TIDY_SAID } from "../core/tidy.js";
|
|
15
15
|
import { calibrationLabel, calibrationOffer } from "../core/viewsizes.js";
|
|
16
16
|
/** A line as a sentence: its first letter capital (a catalog's text that also stands inside a line). */
|
|
@@ -231,6 +231,12 @@ export async function runMenu(ctx, run) {
|
|
|
231
231
|
out();
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
else if (offerUnite(ctx, tree, !isAgent(ctx.env))) {
|
|
235
|
+
// sealed here with two keys of this computer (strom of an app packaged for Windows and of a terminal): united on
|
|
236
|
+
// the person's yes, offered once (as strom seal adopt: every operation and record checked)
|
|
237
|
+
await run(["seal", "adopt", "--tree", root]);
|
|
238
|
+
out();
|
|
239
|
+
}
|
|
234
240
|
// what of it the Strom app does not show (only here in the terminal) said, so its number and the app's agree
|
|
235
241
|
if (waiting)
|
|
236
242
|
out(waitingLine(lang, waits));
|
package/dist/cli/seal.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// every record checked against its seal, the person's yes, then this computer's key. Never by itself, never by an agent.
|
|
3
3
|
import { ui } from "./ui.js";
|
|
4
4
|
import { verifyFull } from "../core/integrity.js";
|
|
5
|
-
import { createKey } from "../core/seal.js";
|
|
5
|
+
import { createKey, fingerprint, otherKeys } from "../core/seal.js";
|
|
6
6
|
import { sealedHere } from "../core/tree.js";
|
|
7
7
|
/**
|
|
8
8
|
* The tree taken over here: refused while a record does not match its seal (what, strom check says), else asked — the
|
|
@@ -10,7 +10,7 @@ import { sealedHere } from "../core/tree.js";
|
|
|
10
10
|
*/
|
|
11
11
|
export async function adoptSeal(ctx, tree, lang) {
|
|
12
12
|
if (tree.key)
|
|
13
|
-
return
|
|
13
|
+
return uniteSeal(ctx, tree, lang);
|
|
14
14
|
const problems = verifyFull(tree).findings.filter((f) => f.level === "error");
|
|
15
15
|
if (problems.length)
|
|
16
16
|
return { adopted: false, why: "problems", problems };
|
|
@@ -33,6 +33,16 @@ export function offerAdopt(ctx, root, person) {
|
|
|
33
33
|
asked.add(root);
|
|
34
34
|
return true;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether to offer uniting this tree's keys now (strom update brought a strom that sees both): sealed here, another key
|
|
38
|
+
* of this computer that the check needs, a person at a terminal, not asked yet in this process.
|
|
39
|
+
*/
|
|
40
|
+
export function offerUnite(ctx, tree, person) {
|
|
41
|
+
if (!person || !ctx.interactive || asked.has(tree.root) || !tree.key || !otherKeys(tree.env, tree.config.id, tree.key).length)
|
|
42
|
+
return false;
|
|
43
|
+
asked.add(tree.root);
|
|
44
|
+
return keysToUnite(tree).length > 0;
|
|
45
|
+
}
|
|
36
46
|
/**
|
|
37
47
|
* The family trees the setup found in the research's folder (strom brought back, a folder from another computer): each
|
|
38
48
|
* said, and of one sealed on another computer how it is taken over — the setup then never ends with "strom init".
|
|
@@ -48,3 +58,36 @@ export function foundTrees(ctx, lang) {
|
|
|
48
58
|
];
|
|
49
59
|
return { lines, foreign: foreign.map(({ name, root }) => ({ name, root })) };
|
|
50
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The other keys of this computer a tree sealed here needs united (strom of an app packaged for Windows and of a
|
|
63
|
+
* terminal each had their own: core/oldsettings.ts): none when the check finds nothing wrong, or none of them.
|
|
64
|
+
*/
|
|
65
|
+
export function keysToUnite(tree) {
|
|
66
|
+
if (!tree.key)
|
|
67
|
+
return [];
|
|
68
|
+
if (!verifyFull(tree).findings.some((f) => f.level === "error"))
|
|
69
|
+
return [];
|
|
70
|
+
return otherKeys(tree.env, tree.config.id, tree.key).map((o) => o.key);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A tree sealed here with another key of this computer too: every operation of the other keys checked with them and
|
|
74
|
+
* every record against its seal — refused while anything does not match (a record changed outside strom stays refused),
|
|
75
|
+
* else the person's yes, then the operation seal.unite of this key (the other keys' operations until now taken) and its
|
|
76
|
+
* commit sealed with it.
|
|
77
|
+
*/
|
|
78
|
+
async function uniteSeal(ctx, tree, lang) {
|
|
79
|
+
const others = keysToUnite(tree);
|
|
80
|
+
if (!others.length)
|
|
81
|
+
return { adopted: false, why: "already" };
|
|
82
|
+
const problems = verifyFull(tree, { trust: others }).findings.filter((f) => f.level === "error");
|
|
83
|
+
if (problems.length)
|
|
84
|
+
return { adopted: false, why: "problems", problems };
|
|
85
|
+
if (!(await ctx.confirm(ui(lang, "ui.seal.unite.ask", { name: tree.config.name }), true)))
|
|
86
|
+
return { adopted: false, why: "cancelled" };
|
|
87
|
+
const united = others.map(fingerprint);
|
|
88
|
+
tree.withTreeLock(() => {
|
|
89
|
+
tree.appendOp({ op: "seal.unite", targets: [], files: [], summary: "Seal keys of this computer united", keys: united });
|
|
90
|
+
tree.commit("Seal: keys of this computer united");
|
|
91
|
+
});
|
|
92
|
+
return { adopted: true, united };
|
|
93
|
+
}
|
package/dist/cli/ui.js
CHANGED
|
@@ -540,6 +540,8 @@ export const UI = {
|
|
|
540
540
|
"ui.tree.created": "✓ The family tree \"{name}\" is ready: {dir}",
|
|
541
541
|
"ui.chat.trust": "The first time, Claude Code asks whether this folder is trusted. It is the family tree's folder, made by strom — confirm with \"Yes, proceed\".",
|
|
542
542
|
"ui.doc.program": "program",
|
|
543
|
+
"ui.doc.packaged": "copy of the program",
|
|
544
|
+
"ui.doc.packaged.copy": "an app packaged for Windows keeps a copy of the program of its own: {folders} — that app runs another strom than the terminal",
|
|
543
545
|
"ui.doc.git": "git (history)",
|
|
544
546
|
"ui.doc.home": "research folder",
|
|
545
547
|
"ui.doc.gitdir": "git in the research folder",
|
|
@@ -607,6 +609,7 @@ export const UI = {
|
|
|
607
609
|
"ui.doc.appurl.env.fix": "the variable STROM_APP_URL set to an address of the Strom app ({web}, or a copy on this computer http://127.0.0.1:<port>/), or removed",
|
|
608
610
|
"ui.doc.tree.ok": "{name}: data consistent and sealed",
|
|
609
611
|
"ui.doc.tree.bad": "{name}: {n, plural, one {# problem} other {# problems}}",
|
|
612
|
+
"ui.doc.tree.twokeys": "{name}: sealed with two keys of strom on this computer (strom of an app and of a terminal) — writes of both are strom's",
|
|
610
613
|
"ui.doc.write": "writing into this family tree",
|
|
611
614
|
"ui.doc.write.ok": "its .strom and .git can be written",
|
|
612
615
|
"ui.doc.write.sandbox": "{where}: writing refused — strom runs under an agent, most likely its sandbox forbids it",
|
|
@@ -643,6 +646,8 @@ export const UI = {
|
|
|
643
646
|
"ui.fix.git.inwindow": "Finish the installation in the window of the system, then run strom doctor again.",
|
|
644
647
|
"ui.fix.failed": "It did not work: {detail}",
|
|
645
648
|
"ui.fix.app": "Open the Strom app in the browser, to install it?",
|
|
649
|
+
"ui.fix.packaged": "Put the copy of the program an app keeps for itself into the trash (the app then runs the strom of the terminal)?",
|
|
650
|
+
"ui.fix.packaged.done": "Into the trash: {folder}",
|
|
646
651
|
"ui.fix.nothing": "Nothing strom can fix by itself — see the arrows.",
|
|
647
652
|
"ui.why.git.human": "git is missing — strom keeps the history of the research in it",
|
|
648
653
|
"ui.why.git.agent": "git is missing (strom keeps the research history in it): tell the user, then run this — the user says yes in a window of the system",
|
|
@@ -2420,6 +2425,8 @@ export const UI = {
|
|
|
2420
2425
|
"ui.error.folder.in-program.default.hint": "Name a folder outside it: strom setup --home \"{suggested}\" --yes",
|
|
2421
2426
|
"ui.error.update.in-program": "{folder} lies inside {entry}, which the update replaces — the research would go with it. Nothing was installed.",
|
|
2422
2427
|
"ui.error.update.in-program.hint": "First move the research out of strom's program folder: strom setup in a terminal of its own (the question about the folder moves it along), then strom update again",
|
|
2428
|
+
"ui.error.update.packaged": "This strom was started by an app packaged for Windows (MSIX): what the update installed into {folder} only that app would see, the terminal would stay at {current}. Nothing was installed.",
|
|
2429
|
+
"ui.error.update.packaged.hint": "strom update in a terminal of its own (PowerShell); the app takes the new version from there",
|
|
2423
2430
|
"ui.error.tree.newer": "This family tree was written by strom {by}, newer than this strom {version}. This strom keeps it untouched.",
|
|
2424
2431
|
"ui.error.tree.newer.hint": "Update strom: strom update",
|
|
2425
2432
|
"ui.error.tree.newer.backup": "This family tree was written by strom {by}, newer than this strom {version}. This strom keeps it untouched.",
|
|
@@ -2781,9 +2788,16 @@ export const UI = {
|
|
|
2781
2788
|
"ui.seal.ask": "Take over \"{name}\" on this computer? Its records were checked: unchanged.",
|
|
2782
2789
|
"ui.seal.adopted": "\"{name}\" is sealed on this computer now — writing works here",
|
|
2783
2790
|
"ui.seal.already": "\"{name}\" is sealed on this computer already",
|
|
2791
|
+
"ui.seal.unite.ask": "\"{name}\" was sealed with two keys of strom on this computer (strom of an app and of a terminal, each with settings of its own). Unite them? Every record was checked: nothing changed outside strom, nothing is lost.",
|
|
2792
|
+
"ui.seal.united": "\"{name}\" is sealed with one key on this computer now — writing works from the app and the terminal alike",
|
|
2793
|
+
"ui.error.seal.other-key": "\"{name}\" was sealed with two keys of strom on this computer (strom of an app and of a terminal, each with settings of its own): nothing written",
|
|
2794
|
+
"ui.error.seal.other-key.hint": "united in a terminal of its own: {set} — never strom repair, which would undo that work",
|
|
2795
|
+
"ui.check.otherkey": "sealed with two keys of strom on this computer (strom of an app and of a terminal): the writes of both are strom's own",
|
|
2796
|
+
"ui.check.otherkey.hint": "united in a terminal of its own: strom seal adopt — never strom repair",
|
|
2784
2797
|
"ui.seal.problems": "not taken over: records do not match their seals — what, strom check says",
|
|
2785
2798
|
"ui.menu.seal": "Sealed on another computer: writing here only once taken over (strom seal adopt).",
|
|
2786
2799
|
"ui.seal.bridge": "\"{name}\" was sealed on another computer: nothing was written — once it is taken over on this one (a terminal: strom seal adopt, or strom's menu), send again",
|
|
2800
|
+
"ui.seal.bridge.twokeys": "\"{name}\" was sealed with two keys of strom on this computer (strom of an app and of a terminal): nothing was written — once they are united (a terminal: strom seal adopt, or strom's menu), send again",
|
|
2787
2801
|
"ui.check.foreignseal": "sealed on another computer (or strom's settings are not on this one): it can be read, written only once taken over here",
|
|
2788
2802
|
"ui.check.foreignseal.hint": "taken over in a terminal of its own: strom seal adopt — or in strom's menu (strom)",
|
|
2789
2803
|
"ui.why.seal.human": "the research was sealed on another computer: writing here only once taken over (confirmed in this terminal)",
|
package/dist/commands/checks.js
CHANGED
|
@@ -8,9 +8,9 @@ import { counted, lines, table } from "../cli/format.js";
|
|
|
8
8
|
import { check, SHARED_MISSING, sharedMissing } from "../core/check.js";
|
|
9
9
|
import { guard } from "../core/guard.js";
|
|
10
10
|
import { unprovenPeople } from "../core/review.js";
|
|
11
|
-
import { NOT_DOWNLOADED, SEALED_PATHS, notDownloaded, notDownloadedError, verifyFast, verifyFull } from "../core/integrity.js";
|
|
11
|
+
import { NOT_DOWNLOADED, OTHER_KEY, SEALED_PATHS, notDownloaded, notDownloadedError, otherKeyError, verifyFast, verifyFull } from "../core/integrity.js";
|
|
12
12
|
import { verifyCommitSeal } from "../core/seal.js";
|
|
13
|
-
import { adoptSeal } from "../cli/seal.js";
|
|
13
|
+
import { adoptSeal, keysToUnite } from "../cli/seal.js";
|
|
14
14
|
import { NeedsConsentError, StromError } from "../core/errors.js";
|
|
15
15
|
import * as git from "../core/git.js";
|
|
16
16
|
import { ui } from "../cli/ui.js";
|
|
@@ -29,6 +29,8 @@ function said(f, lang) {
|
|
|
29
29
|
return ui(lang, "ui.check.unwritable");
|
|
30
30
|
if (lang && f.code === "foreign-seal")
|
|
31
31
|
return ui(lang, "ui.check.foreignseal");
|
|
32
|
+
if (lang && f.code === OTHER_KEY)
|
|
33
|
+
return ui(lang, "ui.check.otherkey");
|
|
32
34
|
if (lang && f.code === NOT_DOWNLOADED)
|
|
33
35
|
return ui(lang, "ui.check.notdownloaded", { n: f.count ?? 1, first: f.file ?? "" });
|
|
34
36
|
if (lang && f.code === SHARED_MISSING)
|
|
@@ -45,6 +47,8 @@ function said(f, lang) {
|
|
|
45
47
|
function hintSaid(f, lang) {
|
|
46
48
|
if (lang && f.code === "foreign-seal")
|
|
47
49
|
return ui(lang, "ui.check.foreignseal.hint");
|
|
50
|
+
if (lang && f.code === OTHER_KEY)
|
|
51
|
+
return ui(lang, "ui.check.otherkey.hint");
|
|
48
52
|
if (lang && f.code === NOT_DOWNLOADED)
|
|
49
53
|
return ui(lang, "ui.check.notdownloaded.hint");
|
|
50
54
|
if (lang && f.code === SHARED_MISSING)
|
|
@@ -185,8 +189,10 @@ function repair(tree) {
|
|
|
185
189
|
restored.push(rel);
|
|
186
190
|
}
|
|
187
191
|
}
|
|
192
|
+
// sealed again even when the commits outside strom changed nothing of it (an empty one, notes only): else the last
|
|
193
|
+
// commit stays unsealed and every write refused, with nothing left to repair
|
|
188
194
|
if (!headSealed)
|
|
189
|
-
tree.commit(`Repair: restore the sealed state of ${sealed.slice(0, 8)}\n\nCommits made outside strom were undone for data/, strom.json, history/ and quality/.`, SEALED_PATHS);
|
|
195
|
+
tree.commit(`Repair: restore the sealed state of ${sealed.slice(0, 8)}\n\nCommits made outside strom were undone for data/, strom.json, history/ and quality/.`, SEALED_PATHS, true);
|
|
190
196
|
return { restored, quarantined, from: sealed };
|
|
191
197
|
}
|
|
192
198
|
register({
|
|
@@ -210,22 +216,29 @@ register({
|
|
|
210
216
|
summary: "Take over a tree sealed on another computer (the user runs this in a terminal)",
|
|
211
217
|
group: "history",
|
|
212
218
|
tree: true,
|
|
213
|
-
description: "Checks the chain and every record hash, then creates this computer's seal key for the tree.\
|
|
219
|
+
description: "Checks the chain and every record hash, then creates this computer's seal key for the tree.\n" +
|
|
220
|
+
"A tree sealed here with a second key of this computer too (a copy of strom's settings an app packaged for Windows\n" +
|
|
221
|
+
"made): checks every operation of that key with it, then unites the keys — never strom repair, which would undo that work.\n" +
|
|
222
|
+
"Agents must not run this — it needs the user's confirmation.",
|
|
214
223
|
person: "Taking over this tree on this computer (the seal) is the user's own, in their terminal",
|
|
215
224
|
run: async (ctx) => {
|
|
216
225
|
const tree = ctx.tree();
|
|
217
226
|
// a person reads it in the research's language (the menu, a link's terminal, the setup go the same way)
|
|
218
227
|
const lang = tree.lang;
|
|
219
228
|
const name = tree.config.name;
|
|
220
|
-
|
|
229
|
+
// sealed here: another key of this computer to unite (strom of a packaged app and of a terminal), else nothing to do
|
|
230
|
+
const unite = tree.key ? keysToUnite(tree).length > 0 : false;
|
|
231
|
+
if (tree.key && !unite)
|
|
221
232
|
return { text: ui(lang, "ui.seal.already", { name }), data: { adopted: false } };
|
|
233
|
+
if (!ctx.interactive && unite)
|
|
234
|
+
throw otherKeyError(name);
|
|
222
235
|
if (!ctx.interactive)
|
|
223
236
|
throw new NeedsConsentError([
|
|
224
237
|
{ key: "seal", kind: "consent", question: "Take over this tree on this computer?", set: "strom seal adopt" },
|
|
225
238
|
]);
|
|
226
239
|
const r = await adoptSeal(ctx, tree, lang);
|
|
227
240
|
if (r.adopted)
|
|
228
|
-
return { text: ui(lang, "ui.seal.adopted", { name }), data: { adopted: true } };
|
|
241
|
+
return r.united ? { text: ui(lang, "ui.seal.united", { name }), data: { adopted: true, united: r.united } } : { text: ui(lang, "ui.seal.adopted", { name }), data: { adopted: true } };
|
|
229
242
|
if (r.why === "problems") {
|
|
230
243
|
const found = report(r.problems, "", lang, true);
|
|
231
244
|
return { ...found, text: lines(found.text, ui(lang, "ui.seal.problems")), data: { ...found.data, adopted: false } };
|
package/dist/commands/setup.js
CHANGED
|
@@ -29,6 +29,8 @@ const MEDIA_UNNAMED_SAID = 500 * 1024 * 1024;
|
|
|
29
29
|
import { Tree, VERSION } from "../core/tree.js";
|
|
30
30
|
import { timed } from "../core/timing.js";
|
|
31
31
|
import { checked, installUpdate, isNewer, knownNewerVersion, lastChannelOf, latestRelease, newerNode, newerVersion, setInstallChannel, updateChannel, whatsNewUrl } from "../core/update.js";
|
|
32
|
+
import { privateCopies, writesPrivately } from "../core/packaged.js";
|
|
33
|
+
import { moveToTrash } from "../core/trash.js";
|
|
32
34
|
import { configDir, defaultHome, desktopDir, isolated, noLinks, ownCommand } from "../core/paths.js";
|
|
33
35
|
import { backupBefore, backupSaid, lastBackupLine } from "../cli/backups.js";
|
|
34
36
|
import { BACKUP_SAID_DAYS, lastBackup } from "../core/backup.js";
|
|
@@ -41,7 +43,7 @@ import { offerLinks, shortcutName } from "../cli/wizard.js";
|
|
|
41
43
|
import { linkHandlerState, linkScheme, linkStateText } from "../core/links.js";
|
|
42
44
|
import { globalTargets, installGlobal } from "../agents/global.js";
|
|
43
45
|
import { placeholders, ui, UI } from "../cli/ui.js";
|
|
44
|
-
import { foundTrees } from "../cli/seal.js";
|
|
46
|
+
import { adoptSeal, foundTrees } from "../cli/seal.js";
|
|
45
47
|
import { PERMISSION_LEVELS } from "../core/config.js";
|
|
46
48
|
import { PROFILES } from "../agents/profiles.js";
|
|
47
49
|
import { agentSettingsSaid, EFFORTS, hasEffort, ownEffort, raisesEffort, RECOMMENDED_EFFORT } from "../agents/effort.js";
|
|
@@ -58,7 +60,7 @@ import { isArchive } from "../core/mode.js";
|
|
|
58
60
|
import { fixEnv, fixesDoctorLine } from "./fixchecks.js";
|
|
59
61
|
import { placeOf, sampleFiles } from "../core/clouddrive.js";
|
|
60
62
|
import { check } from "../core/check.js";
|
|
61
|
-
import { assertIntact, verifyFull } from "../core/integrity.js";
|
|
63
|
+
import { assertIntact, OTHER_KEY, verifyFull } from "../core/integrity.js";
|
|
62
64
|
import { ensurePluginsDir } from "../core/connector.js";
|
|
63
65
|
import { checkGate, ensureGatesDir } from "../core/gate.js";
|
|
64
66
|
import { ensureHooksDir } from "../core/hooks.js";
|
|
@@ -277,6 +279,10 @@ function diagnoseInto(ctx, checks, t, add, progress) {
|
|
|
277
279
|
add("program", "ok", `${t("ui.doc.innode", { version: VERSION, node: process.version })}${beta}`);
|
|
278
280
|
else
|
|
279
281
|
add("program", "fail", t("ui.doc.oldnode", { node: process.version }), "https://nodejs.org");
|
|
282
|
+
// an update an app packaged for Windows installed for itself alone (core/packaged.ts): that app runs another strom
|
|
283
|
+
const copies = installation().kind === "installed" ? privateCopies(ctx.env, installation().root) : [];
|
|
284
|
+
if (copies.length)
|
|
285
|
+
add("packaged", "warn", t("ui.doc.packaged.copy", { folders: copies.map((c) => ctx.display(c)).join(", ") }), "strom doctor --fix", "packaged");
|
|
280
286
|
// The backup before another channel or an older version: one that could not be made (nothing switches until it is),
|
|
281
287
|
// else where the last one is, for a month
|
|
282
288
|
const backup = lastBackup(ctx.env, BACKUP_SAID_DAYS);
|
|
@@ -518,9 +524,13 @@ function diagnoseInto(ctx, checks, t, add, progress) {
|
|
|
518
524
|
else if (ctx.hasTree()) {
|
|
519
525
|
const tr = ctx.tree();
|
|
520
526
|
progress("tree", tr.config.name);
|
|
521
|
-
const
|
|
527
|
+
const sealErrs = timed("doctor.verify", () => verifyFull(tr).findings.filter((f) => f.level === "error"));
|
|
528
|
+
const errs = timed("doctor.check", () => check(tr).filter((f) => f.level === "error").length) + sealErrs.length;
|
|
522
529
|
if (errs === 0)
|
|
523
530
|
add("tree", "ok", t("ui.doc.tree.ok", { name: tr.config.name }));
|
|
531
|
+
// two seal keys of this computer (strom of an app packaged for Windows and of a terminal): united, never repaired
|
|
532
|
+
else if (sealErrs.length === errs && sealErrs.every((f) => f.code === OTHER_KEY))
|
|
533
|
+
add("tree", "fail", t("ui.doc.tree.twokeys", { name: tr.config.name }), "strom seal adopt", "unite");
|
|
524
534
|
else
|
|
525
535
|
add("tree", "fail", t("ui.doc.tree.bad", { name: tr.config.name, n: errs }), "strom verify ; strom check");
|
|
526
536
|
// can it be written — its locks (.strom) and history (.git) tried: found on Windows, an agent's sandbox refused
|
|
@@ -637,6 +647,20 @@ async function repair(ctx, checks, out) {
|
|
|
637
647
|
}
|
|
638
648
|
if (todo.has("links") && person)
|
|
639
649
|
await offerLinks(ctx, lang);
|
|
650
|
+
// two seal keys of this computer united: as strom seal adopt, the person's yes at a terminal (never an agent)
|
|
651
|
+
if (todo.has("unite") && person && ctx.hasTree()) {
|
|
652
|
+
const r = await adoptSeal(ctx, ctx.tree(), lang);
|
|
653
|
+
if (r.adopted)
|
|
654
|
+
out(ui(lang, "ui.seal.united", { name: ctx.tree().config.name }));
|
|
655
|
+
}
|
|
656
|
+
// the private copies of the program into the trash, on the person's yes in a terminal of their own (from the packaged
|
|
657
|
+
// app itself the copy is what it reads: nothing done there)
|
|
658
|
+
if (todo.has("packaged") && person && !writesPrivately(ctx.env, installation().root) && (await ctx.confirm(ui(lang, "ui.fix.packaged"), true))) {
|
|
659
|
+
for (const c of privateCopies(ctx.env, installation().root)) {
|
|
660
|
+
moveToTrash(c, ctx.env, "Strom (copy of an app)");
|
|
661
|
+
out(ui(lang, "ui.fix.packaged.done", { folder: ctx.display(c) }));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
640
664
|
// a change of strom's that made the reading less sure: returned on the person's yes (a window when an agent asks)
|
|
641
665
|
if (todo.has("tune"))
|
|
642
666
|
await fixTuneWorse(ctx, out);
|
|
@@ -737,6 +761,8 @@ register({
|
|
|
737
761
|
return { text: inst.kind === "npm" ? t("ui.update.npm", { version: newest, tag: channel === "beta" ? "beta" : "latest" }) : t("ui.update.source", { version: newest }), data: { current: VERSION, latest: newest, newer: true, npm: inst.kind === "npm" } };
|
|
738
762
|
// the research where the update replaces the program: nothing asked, backed up or installed
|
|
739
763
|
refuseResearchInProgram(ctx, inst.root);
|
|
764
|
+
// started by a packaged app (Claude's desktop app): the update would be that app's alone (core/packaged.ts)
|
|
765
|
+
refusePackaged(ctx, inst.root);
|
|
740
766
|
const older = newer && isNewer(VERSION, newest);
|
|
741
767
|
const question = switching
|
|
742
768
|
? `${t(`ui.update.channel.${to}.sure`, { version: newest, current: VERSION })} ${older ? t("ui.update.older", { version: newest, current: VERSION }) : t("ui.update.stays")}`
|
|
@@ -807,6 +833,15 @@ function refuseResearchInProgram(ctx, root) {
|
|
|
807
833
|
details: { folder: hit, entry },
|
|
808
834
|
});
|
|
809
835
|
}
|
|
836
|
+
function refusePackaged(ctx, root) {
|
|
837
|
+
if (!writesPrivately(ctx.env, root))
|
|
838
|
+
return;
|
|
839
|
+
throw new StromError(`this strom was started by an app packaged for Windows (MSIX): what it installed into ${ctx.display(root)} only that app would see, the terminal would stay at ${VERSION}; nothing was installed`, {
|
|
840
|
+
hint: "the user runs strom update in their own terminal (PowerShell); the app takes the new version from there",
|
|
841
|
+
code: "update.packaged",
|
|
842
|
+
params: { folder: ctx.display(root), current: VERSION },
|
|
843
|
+
});
|
|
844
|
+
}
|
|
810
845
|
/**
|
|
811
846
|
* Before strom update installs another channel or an older version: the backup the next run of that version would
|
|
812
847
|
* otherwise make (the same change: from what ran here last, its channel, to the version and channel installed). Made
|
package/dist/core/changelog.js
CHANGED
|
@@ -15,7 +15,7 @@ import { eventName, humanDate, humanPlace, humanTask } from "../cli/human.js";
|
|
|
15
15
|
import { UI, ui } from "../cli/ui.js";
|
|
16
16
|
import { actsAsArchive } from "./state.js";
|
|
17
17
|
/** Operations nobody needs to read: what another line says already, or bookkeeping. */
|
|
18
|
-
const QUIET = new Set(["session.metrics", "session.readers", "task.start", "task.continue", "session.note", "tree.settle", "seal.adopt"]);
|
|
18
|
+
const QUIET = new Set(["session.metrics", "session.readers", "task.start", "task.continue", "session.note", "tree.settle", "seal.adopt", "seal.unite"]);
|
|
19
19
|
/** The one-line form of an operation, by its kind: {who} a person or family, {name} the record. */
|
|
20
20
|
/** What strom mode logs (core/mode.ts), as the history says it. */
|
|
21
21
|
const MODE_SAID = { "the research is an archive": "log.mode.archive", "the research works with an agent again": "log.mode.research" };
|
package/dist/core/git.js
CHANGED
|
@@ -276,7 +276,7 @@ function commitTree(dir, tree, parent, message) {
|
|
|
276
276
|
* fewer processes, no hooks, no editor. Returns the hash, or undefined if
|
|
277
277
|
* nothing changed.
|
|
278
278
|
*/
|
|
279
|
-
export function commitAll(dir, message, pathspec = ["."], seal) {
|
|
279
|
+
export function commitAll(dir, message, pathspec = ["."], seal, always = false) {
|
|
280
280
|
// Paths that no longer exist are fine for `add -A` only if git knows them;
|
|
281
281
|
// filter missing, untracked-never-seen paths out to keep `add` from failing.
|
|
282
282
|
const missing = pathspec.filter((p) => p !== "." && !fs.existsSync(path.join(dir, p)));
|
|
@@ -286,7 +286,7 @@ export function commitAll(dir, message, pathspec = ["."], seal) {
|
|
|
286
286
|
git(dir, ["add", "-A", "--", ...specs]);
|
|
287
287
|
const tree = git(dir, ["write-tree"]).trim();
|
|
288
288
|
const parent = headInfo(dir);
|
|
289
|
-
if (parent && parent.tree === tree)
|
|
289
|
+
if (parent && parent.tree === tree && !always)
|
|
290
290
|
return undefined;
|
|
291
291
|
const full = seal ? `${message}\n\n${seal(tree)}\n` : `${message}\n`;
|
|
292
292
|
const r = commitTree(dir, tree, parent?.hash, full);
|
package/dist/core/integrity.js
CHANGED
|
@@ -16,8 +16,9 @@ import fs from "node:fs";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import * as git from "./git.js";
|
|
18
18
|
import { RECORD_TYPES } from "./model.js";
|
|
19
|
-
import { fileSha, fingerprint, verifyCommitSeal, verifyOp } from "./seal.js";
|
|
20
|
-
import {
|
|
19
|
+
import { fileSha, fingerprint, otherKeys, readCommitSeal, verifyCommitSeal, verifyOp } from "./seal.js";
|
|
20
|
+
import { settingsHidden } from "./oldsettings.js";
|
|
21
|
+
import { NeedsConsentError, StromError } from "./errors.js";
|
|
21
22
|
import { counted } from "../cli/format.js";
|
|
22
23
|
/**
|
|
23
24
|
* What strom alone writes in a tree, each change through a sealed operation: the records and their history (data/), the
|
|
@@ -56,8 +57,29 @@ function parseOps(text, file, findings, startLine = 0) {
|
|
|
56
57
|
});
|
|
57
58
|
return out;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A research sealed with two keys of this computer: the settings' own and another in an old settings folder (strom of
|
|
62
|
+
* an app packaged for Windows and of a terminal each had their own: core/oldsettings.ts). Its writes are the person's,
|
|
63
|
+
* never "outside strom" — united by the person in their terminal (strom seal adopt), never undone by strom repair.
|
|
64
|
+
*/
|
|
65
|
+
export const OTHER_KEY = "other-key";
|
|
66
|
+
const OTHER_KEY_HINT = "the user runs in their terminal: strom seal adopt (unites the keys — never strom repair, which would undo that work)";
|
|
67
|
+
function keysOf(tree, trust = [], united = new Map()) {
|
|
68
|
+
let others;
|
|
69
|
+
let hidden;
|
|
70
|
+
return {
|
|
71
|
+
hidden: () => (hidden ??= settingsHidden(tree.env)),
|
|
72
|
+
trusted: new Map(trust.map((k) => [fingerprint(k), k])),
|
|
73
|
+
united,
|
|
74
|
+
others: () => (others ??= new Map(tree.key ? otherKeys(tree.env, tree.config.id, tree.key).map((o) => [fingerprint(o.key), o.key]) : [])),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Is an operation shaped as strom signs one (a key's fingerprint and a signature), whatever key it was? */
|
|
78
|
+
function strommade(op) {
|
|
79
|
+
return typeof op.k === "string" && /^[0-9a-f]{12}$/u.test(op.k) && typeof op.sig === "string" && /^[0-9a-f]{32}$/u.test(op.sig);
|
|
80
|
+
}
|
|
59
81
|
/** Check chain and signatures of a run of operations. */
|
|
60
|
-
function checkOps(tree, ops, prevSig, file, findings) {
|
|
82
|
+
function checkOps(tree, ops, prevSig, file, findings, keys = keysOf(tree)) {
|
|
61
83
|
const key = tree.key;
|
|
62
84
|
const fp = key ? fingerprint(key) : undefined;
|
|
63
85
|
let prev = prevSig;
|
|
@@ -70,16 +92,27 @@ function checkOps(tree, ops, prevSig, file, findings) {
|
|
|
70
92
|
adopted = true;
|
|
71
93
|
if (!key)
|
|
72
94
|
continue;
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
const trusted = keys.trusted.get(op.k);
|
|
96
|
+
if (op.k === fp || trusted) {
|
|
97
|
+
if (!verifyOp(op, trusted ?? key))
|
|
75
98
|
findings.push({ level: "error", code: "forged-op", file, message: `operation ${op.op} at ${op.at} has an invalid signature (written outside strom)` });
|
|
76
99
|
}
|
|
100
|
+
else if ((keys.united.get(op.k) ?? "") >= op.at) {
|
|
101
|
+
// another key of this computer, its operations until then checked and taken when the person united the keys
|
|
102
|
+
}
|
|
77
103
|
else if (adopted) {
|
|
78
|
-
|
|
104
|
+
const other = keys.others().get(op.k);
|
|
105
|
+
if (other && verifyOp(op, other))
|
|
106
|
+
findings.push({ level: "error", code: OTHER_KEY, file, message: `operation ${op.op} at ${op.at} is signed with another seal key of strom on this computer (a second copy of strom's settings)`, hint: OTHER_KEY_HINT });
|
|
107
|
+
// a key out of sight only for what strom signs (a fingerprint and a signature): anything else was written outside
|
|
108
|
+
else if (keys.hidden() && strommade(op))
|
|
109
|
+
findings.push({ level: "error", code: OTHER_KEY, file, message: `operation ${op.op} at ${op.at} is signed with a key this strom does not see (an app packaged for Windows started it)`, hint: OTHER_KEY_HINT });
|
|
110
|
+
else
|
|
111
|
+
findings.push({ level: "error", code: "forged-op", file, message: `operation ${op.op} at ${op.at} is signed with an unknown key after the tree was adopted` });
|
|
79
112
|
}
|
|
80
113
|
}
|
|
81
114
|
}
|
|
82
|
-
function headSeal(tree, findings) {
|
|
115
|
+
function headSeal(tree, findings, keys = keysOf(tree)) {
|
|
83
116
|
const head = git.headInfo(tree.root);
|
|
84
117
|
if (!head)
|
|
85
118
|
return;
|
|
@@ -93,7 +126,27 @@ function headSeal(tree, findings) {
|
|
|
93
126
|
});
|
|
94
127
|
return;
|
|
95
128
|
}
|
|
96
|
-
|
|
129
|
+
const sealedWith = (k) => verifyCommitSeal(k, head.tree, head.message);
|
|
130
|
+
if (sealedWith(key) || [...keys.trusted.values()].some(sealedWith))
|
|
131
|
+
return;
|
|
132
|
+
if ([...keys.others().values()].some(sealedWith))
|
|
133
|
+
findings.push({
|
|
134
|
+
level: "error",
|
|
135
|
+
code: OTHER_KEY,
|
|
136
|
+
message: "the last commit was sealed with another seal key of strom on this computer (a second copy of strom's settings)",
|
|
137
|
+
hint: OTHER_KEY_HINT,
|
|
138
|
+
});
|
|
139
|
+
// started by an app packaged for Windows: the terminal's key is out of its sight — the terminal unites them (where
|
|
140
|
+
// seal adopt checks every seal again: a commit of nobody's stays refused). Only a commit with strom's seal: one
|
|
141
|
+
// without (git used directly) is said as it always was, never sent to uniting it cannot do
|
|
142
|
+
else if (keys.hidden() && readCommitSeal(head.message) !== undefined)
|
|
143
|
+
findings.push({
|
|
144
|
+
level: "error",
|
|
145
|
+
code: OTHER_KEY,
|
|
146
|
+
message: "the last commit was not sealed with a key this strom sees — an app packaged for Windows started it, the key of the terminal's strom is out of its sight",
|
|
147
|
+
hint: OTHER_KEY_HINT,
|
|
148
|
+
});
|
|
149
|
+
else
|
|
97
150
|
findings.push({
|
|
98
151
|
level: "error",
|
|
99
152
|
code: "foreign-commit",
|
|
@@ -295,17 +348,44 @@ export function verifyFast(tree, snap = snapshot(tree)) {
|
|
|
295
348
|
findings.unshift(notDownloadedFinding([...cloud].sort()));
|
|
296
349
|
return { findings, tampered: [...tampered] };
|
|
297
350
|
}
|
|
298
|
-
/**
|
|
299
|
-
|
|
351
|
+
/**
|
|
352
|
+
* The other keys of this computer a seal.unite of the tree's own key took, each with when (its operations until then
|
|
353
|
+
* are the person's).
|
|
354
|
+
*/
|
|
355
|
+
function unitedKeys(tree, logs) {
|
|
356
|
+
const united = new Map();
|
|
357
|
+
const key = tree.key;
|
|
358
|
+
if (!key)
|
|
359
|
+
return united;
|
|
360
|
+
const fp = fingerprint(key);
|
|
361
|
+
for (const { ops } of logs)
|
|
362
|
+
for (const op of ops)
|
|
363
|
+
if (op.op === "seal.unite" && op.k === fp && verifyOp(op, key))
|
|
364
|
+
for (const k of op.keys ?? [])
|
|
365
|
+
if ((united.get(k) ?? "") < op.at)
|
|
366
|
+
united.set(k, op.at);
|
|
367
|
+
return united;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Full check: every operation and every record. `trust`: other keys of this computer whose operations and commits are
|
|
371
|
+
* checked with them as the tree's own (strom seal adopt, before it unites them).
|
|
372
|
+
*/
|
|
373
|
+
export function verifyFull(tree, o = {}) {
|
|
300
374
|
const findings = [];
|
|
301
375
|
const tampered = new Set();
|
|
302
|
-
headSeal(tree, findings);
|
|
303
376
|
const latest = new Map();
|
|
304
377
|
const all = [];
|
|
305
|
-
|
|
378
|
+
const logs = opsLogs(tree.dataDir).map((file) => {
|
|
306
379
|
const rel = path.relative(tree.root, file).split(path.sep).join("/");
|
|
307
|
-
|
|
308
|
-
|
|
380
|
+
return { rel, ops: parseOps(fs.readFileSync(file, "utf8"), rel, findings) };
|
|
381
|
+
});
|
|
382
|
+
const keys = keysOf(tree, o.trust, unitedKeys(tree, logs));
|
|
383
|
+
// the last commit said first, as ever
|
|
384
|
+
const headFindings = [];
|
|
385
|
+
headSeal(tree, headFindings, keys);
|
|
386
|
+
findings.unshift(...headFindings);
|
|
387
|
+
for (const { rel, ops } of logs) {
|
|
388
|
+
checkOps(tree, ops, "", rel, findings, keys);
|
|
309
389
|
all.push(...ops);
|
|
310
390
|
}
|
|
311
391
|
// Logs are per session: the last word on a file is the newest operation across all of them.
|
|
@@ -387,6 +467,9 @@ export function assertIntact(tree) {
|
|
|
387
467
|
tree.requireKey();
|
|
388
468
|
return;
|
|
389
469
|
}
|
|
470
|
+
// the last commit sealed with another key of this computer: the person unites them (never strom repair)
|
|
471
|
+
if (errors.every((f) => f.code === OTHER_KEY))
|
|
472
|
+
throw otherKeyError(tree.config.name);
|
|
390
473
|
// files only in iCloud: downloaded first — what else is wrong is judged then
|
|
391
474
|
if (errors.some((f) => f.code === NOT_DOWNLOADED))
|
|
392
475
|
throw notDownloadedError(notDownloaded(tree.root));
|
|
@@ -410,3 +493,10 @@ export function notDownloadedError(files) {
|
|
|
410
493
|
details: files.slice(0, 50),
|
|
411
494
|
});
|
|
412
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* Writing refused while the last commit carries the seal of another key of this computer: a consent of the person in
|
|
498
|
+
* their terminal (strom seal adopt unites the keys), said in their language (ui.error.seal.other-key).
|
|
499
|
+
*/
|
|
500
|
+
export function otherKeyError(name) {
|
|
501
|
+
return new NeedsConsentError([{ key: "seal", kind: "consent", question: "This tree was sealed with two keys of strom on this computer. Unite them?", set: "strom seal adopt" }], undefined, undefined, { code: "seal.other-key", params: { name, set: "strom seal adopt" } });
|
|
502
|
+
}
|
package/dist/core/live.js
CHANGED
|
@@ -246,6 +246,7 @@ const CODES = {
|
|
|
246
246
|
"ui.media.large": "media.large",
|
|
247
247
|
"ui.media.full": "media.full",
|
|
248
248
|
"ui.seal.bridge": SEAL_FOREIGN,
|
|
249
|
+
"ui.seal.bridge.twokeys": "seal.other-key",
|
|
249
250
|
"ui.error.tree.not-downloaded": "tree.not-downloaded",
|
|
250
251
|
"ui.experiments.link": "experiments.link",
|
|
251
252
|
"ui.history.place.link": "history.link",
|
|
@@ -2240,6 +2241,9 @@ export function serveLive(root, env) {
|
|
|
2240
2241
|
// sealed on another computer since it was asked (its key gone meanwhile): said as the bridge says it before
|
|
2241
2242
|
if (data.code === SEAL_FOREIGN)
|
|
2242
2243
|
reply(409, { ...sealRefusal(), intake: sent.intake });
|
|
2244
|
+
// sealed here with two keys of this computer (an app packaged for Windows and a terminal): united in a terminal
|
|
2245
|
+
else if (data.code === "seal.other-key")
|
|
2246
|
+
reply(409, { ...said(lang, "ui.seal.bridge.twokeys", { name: String(data.params?.name ?? ""), set: "strom seal adopt" }), intake: sent.intake });
|
|
2243
2247
|
// no room on the disk: said as everywhere (507 disk.full), never the system's words with strom's inner path
|
|
2244
2248
|
else if (data.code === DISK_FULL || isDiskFull(why))
|
|
2245
2249
|
reply(507, { ...diskFullSaid(lang, fullWhere(data, root)), reason: why, intake: sent.intake });
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// strom's settings on Windows before 1.23: %APPDATA%\strom. An app packaged as MSIX (Claude's desktop app) gets every
|
|
2
|
+
// file it creates in AppData as a private copy of its own (%LOCALAPPDATA%\Packages\<app>\LocalCache\Roaming\strom),
|
|
3
|
+
// read before the real one and seen by no other program — a strom it started and a strom of a terminal each had their
|
|
4
|
+
// own seal key for one research, each side's writes "outside strom" to the other (found 2026-10-30). Microsoft: "all
|
|
5
|
+
// newly created files and folders in the user's AppData folder are written to a private per-user, per-app location";
|
|
6
|
+
// "apart from AppData, the app can write to any location where the user has write access, including other parts of
|
|
7
|
+
// %userprofile%" (learn.microsoft.com/windows/msix/desktop/flexible-virtualization). The settings are outside AppData
|
|
8
|
+
// now (paths.ts configDir); these folders are read, taken over once, and never moved or deleted: an older strom reads
|
|
9
|
+
// them still (a switch back to an older version).
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { configDir, userHome } from "./paths.js";
|
|
13
|
+
import { writesPrivately } from "./packaged.js";
|
|
14
|
+
import { installation } from "./self.js";
|
|
15
|
+
/**
|
|
16
|
+
* The folders strom kept its settings in before, as this process sees them: %APPDATA%\strom first (in a packaged app
|
|
17
|
+
* its private copy where there is one, else the real one), then each packaged app's copy, read by its path. None with
|
|
18
|
+
* settings of their own (STROM_CONFIG_DIR) and none off Windows. STROM_TEST_OLD_SETTINGS (the sources only): the folders
|
|
19
|
+
* standing for them, separated as PATH is.
|
|
20
|
+
*/
|
|
21
|
+
export function oldSettingsDirs(env, platform = process.platform, inst = installation()) {
|
|
22
|
+
if (inst.kind === "source" && env.STROM_TEST_OLD_SETTINGS !== undefined)
|
|
23
|
+
return env.STROM_TEST_OLD_SETTINGS.split(path.delimiter).filter(Boolean);
|
|
24
|
+
if (env.STROM_CONFIG_DIR || platform !== "win32")
|
|
25
|
+
return [];
|
|
26
|
+
const home = env.USERPROFILE || userHome(env);
|
|
27
|
+
const roaming = path.join(env.APPDATA || path.join(home, "AppData", "Roaming"), "strom");
|
|
28
|
+
const packages = path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Packages");
|
|
29
|
+
let apps = [];
|
|
30
|
+
try {
|
|
31
|
+
apps = fs.readdirSync(packages);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// no packaged apps
|
|
35
|
+
}
|
|
36
|
+
const copies = apps.map((a) => path.join(packages, a, "LocalCache", "Roaming", "strom")).filter((d) => fs.existsSync(d));
|
|
37
|
+
return [roaming, ...copies];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every packaged app's copy of the old settings as one pattern, those made later too:
|
|
41
|
+
* `%LOCALAPPDATA%\Packages\*\LocalCache\Roaming\strom` — kept from the agents whole (a key an agent put into a copy
|
|
42
|
+
* made after the rules were written would make a commit of nobody's another key of this computer, offered to unite).
|
|
43
|
+
* None where oldSettingsDirs has none. STROM_TEST_PACKAGES (the sources only): the folder standing for Packages.
|
|
44
|
+
*/
|
|
45
|
+
export function packagedSettingsPattern(env, platform = process.platform, inst = installation()) {
|
|
46
|
+
if (inst.kind === "source" && env.STROM_TEST_PACKAGES !== undefined)
|
|
47
|
+
return path.join(env.STROM_TEST_PACKAGES, "*", "LocalCache", "Roaming", "strom");
|
|
48
|
+
if (env.STROM_CONFIG_DIR || platform !== "win32")
|
|
49
|
+
return undefined;
|
|
50
|
+
const home = env.USERPROFILE || userHome(env);
|
|
51
|
+
return path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Packages", "*", "LocalCache", "Roaming", "strom");
|
|
52
|
+
}
|
|
53
|
+
/** What is never copied over: a lock or a file half written of a strom running meanwhile, and the keys (seal.ts). */
|
|
54
|
+
function copied(from, file) {
|
|
55
|
+
if (/\.(lock|tmp)$/u.test(file))
|
|
56
|
+
return false;
|
|
57
|
+
return path.relative(from, file).split(path.sep)[0] !== "keys";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The settings taken over into the folder of today once, before anything reads them (no config.json there yet): every
|
|
61
|
+
* old folder with settings copied in, the first one's word kept where two say something. The seal keys are taken one by
|
|
62
|
+
* one when a research needs its own (seal.ts settleKey). The folder taken from, or undefined.
|
|
63
|
+
*/
|
|
64
|
+
export function takeOverOldSettings(env, platform = process.platform, inst = installation()) {
|
|
65
|
+
const dir = configDir(env, platform);
|
|
66
|
+
if (fs.existsSync(path.join(dir, "config.json")))
|
|
67
|
+
return undefined;
|
|
68
|
+
const old = oldSettingsDirs(env, platform, inst).filter((d) => fs.existsSync(path.join(d, "config.json")));
|
|
69
|
+
if (!old.length)
|
|
70
|
+
return undefined;
|
|
71
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
for (const from of old)
|
|
73
|
+
fs.cpSync(from, dir, { recursive: true, force: false, errorOnExist: false, filter: (file) => copied(from, file) });
|
|
74
|
+
return old[0];
|
|
75
|
+
}
|
|
76
|
+
/** A research's seal key in the old folders, each different one once with where it was (the first seen first). */
|
|
77
|
+
export function oldKeys(env, treeId, inst = installation()) {
|
|
78
|
+
const out = [];
|
|
79
|
+
for (const dir of oldSettingsDirs(env, process.platform, inst)) {
|
|
80
|
+
const file = path.join(dir, "keys", `${treeId}.key`);
|
|
81
|
+
let key;
|
|
82
|
+
try {
|
|
83
|
+
key = Buffer.from(fs.readFileSync(file, "utf8").trim(), "hex");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (key.length && !out.some((o) => o.key.equals(key)))
|
|
89
|
+
out.push({ file, key });
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether this strom was started by an app packaged for Windows that keeps its own copy of the old settings: the key a
|
|
95
|
+
* strom of the terminal keeps there it cannot see (its own copy reads first) — what is sealed with it is another key of
|
|
96
|
+
* this computer, not git used directly. Tried (core/packaged.ts), only when a seal does not match.
|
|
97
|
+
*/
|
|
98
|
+
export function settingsHidden(env, inst = installation()) {
|
|
99
|
+
const dir = oldSettingsDirs(env, process.platform, inst)[0];
|
|
100
|
+
return !!dir && fs.existsSync(dir) && writesPrivately(env, dir, process.platform, inst);
|
|
101
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// A strom started by an app packaged as MSIX (Claude's desktop app on Windows): every file it creates under AppData goes
|
|
2
|
+
// to a private copy of the app's (%LOCALAPPDATA%\Packages\<app>\LocalCache\Local\…), read before the real one and seen
|
|
3
|
+
// by no other program (learn.microsoft.com/windows/msix/desktop/desktop-to-uwp-behind-the-scenes, "AppData operations
|
|
4
|
+
// on Windows 10, version 1903 and later"). strom's program is in %LOCALAPPDATA%\Programs\Strom: an update installed
|
|
5
|
+
// from there would be that app's alone — the app on the new version, the terminal on the old one for good (found
|
|
6
|
+
// 2026-10-30). Whether this process writes so is tried, not guessed: a file made in the folder and looked for in the
|
|
7
|
+
// private copies.
|
|
8
|
+
import crypto from "node:crypto";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { userHome } from "./paths.js";
|
|
12
|
+
import { installation } from "./self.js";
|
|
13
|
+
/** The AppData folders a packaged app's new files are taken from, each with its folder in the private copy. */
|
|
14
|
+
function appData(env) {
|
|
15
|
+
const home = env.USERPROFILE || userHome(env);
|
|
16
|
+
return [
|
|
17
|
+
{ real: env.LOCALAPPDATA || path.join(home, "AppData", "Local"), copy: "Local" },
|
|
18
|
+
{ real: env.APPDATA || path.join(home, "AppData", "Roaming"), copy: "Roaming" },
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
/** Where the packaged apps keep their private copies of `dir`, whether they are there or not (none: dir outside AppData). */
|
|
22
|
+
function copiesOf(env, dir) {
|
|
23
|
+
const packages = path.join(appData(env)[0].real, "Packages");
|
|
24
|
+
for (const { real, copy } of appData(env)) {
|
|
25
|
+
const rel = path.relative(real, dir);
|
|
26
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
|
|
27
|
+
continue;
|
|
28
|
+
let apps = [];
|
|
29
|
+
try {
|
|
30
|
+
apps = fs.readdirSync(packages);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// no packaged apps
|
|
34
|
+
}
|
|
35
|
+
return apps.map((a) => path.join(packages, a, "LocalCache", copy, rel));
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
/** The private copies of `dir` some packaged app made (from the terminal: the update it installed only for itself). */
|
|
40
|
+
export function privateCopies(env, dir, platform = process.platform) {
|
|
41
|
+
if (platform !== "win32")
|
|
42
|
+
return [];
|
|
43
|
+
return copiesOf(env, dir).filter((d) => fs.existsSync(d));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a file this process makes in `dir` lands in a packaged app's private copy instead. STROM_TEST_PACKAGED=1 (the
|
|
47
|
+
* sources only) stands for it. `make` writes the file (a test makes it where Windows would).
|
|
48
|
+
*/
|
|
49
|
+
export function writesPrivately(env, dir, platform = process.platform, inst = installation(), make = (file) => fs.writeFileSync(file, "", { flag: "wx" })) {
|
|
50
|
+
if (inst.kind === "source" && env.STROM_TEST_PACKAGED !== undefined)
|
|
51
|
+
return env.STROM_TEST_PACKAGED === "1";
|
|
52
|
+
if (platform !== "win32")
|
|
53
|
+
return false;
|
|
54
|
+
const copies = copiesOf(env, dir);
|
|
55
|
+
if (!copies.length)
|
|
56
|
+
return false;
|
|
57
|
+
const name = `.strom-probe-${crypto.randomBytes(6).toString("hex")}`;
|
|
58
|
+
try {
|
|
59
|
+
make(path.join(dir, name));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// the private copy says so; the real folder could have it as well — what counts is the copy
|
|
65
|
+
const hit = copies.some((c) => fs.existsSync(path.join(c, name)));
|
|
66
|
+
for (const at of [dir, ...copies])
|
|
67
|
+
fs.rmSync(path.join(at, name), { force: true });
|
|
68
|
+
return hit;
|
|
69
|
+
}
|
package/dist/core/paths.js
CHANGED
|
@@ -44,14 +44,18 @@ export function noLinks(env) {
|
|
|
44
44
|
export function userHome(env) {
|
|
45
45
|
return env.HOME || env.USERPROFILE || os.homedir();
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Directory holding the small user config (the pointer to the Strom home) and the seal keys. On Windows outside AppData
|
|
49
|
+
* (%USERPROFILE%\.config\strom, as ~/.config/strom elsewhere): an app packaged as MSIX (Claude's desktop app) gets
|
|
50
|
+
* every new file of AppData as a private copy of its own, which no strom of a terminal sees — a research had two seal
|
|
51
|
+
* keys, each side's writes "outside strom" to the other (found 2026-10-30). The folder before: core/oldsettings.ts.
|
|
52
|
+
*/
|
|
48
53
|
export function configDir(env, platform = process.platform) {
|
|
49
54
|
if (env.STROM_CONFIG_DIR)
|
|
50
55
|
return path.resolve(env.STROM_CONFIG_DIR);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return path.join(
|
|
54
|
-
}
|
|
56
|
+
// the profile's own folder (HOME is Git Bash's, which may name another)
|
|
57
|
+
if (platform === "win32")
|
|
58
|
+
return path.join(env.USERPROFILE || userHome(env), ".config", "strom");
|
|
55
59
|
const xdg = env.XDG_CONFIG_HOME || path.join(userHome(env), ".config");
|
|
56
60
|
return path.join(xdg, "strom");
|
|
57
61
|
}
|
package/dist/core/seal.js
CHANGED
|
@@ -16,6 +16,7 @@ import fs from "node:fs";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { configDir } from "./paths.js";
|
|
18
18
|
import { canonicalize } from "./json.js";
|
|
19
|
+
import { oldKeys, oldSettingsDirs } from "./oldsettings.js";
|
|
19
20
|
export function sha256(content) {
|
|
20
21
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
21
22
|
}
|
|
@@ -30,20 +31,67 @@ export function fileSha(file) {
|
|
|
30
31
|
export function keyFile(env, treeId) {
|
|
31
32
|
return path.join(configDir(env), "keys", `${treeId}.key`);
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
function readKey(file) {
|
|
34
35
|
try {
|
|
35
|
-
|
|
36
|
+
const key = Buffer.from(fs.readFileSync(file, "utf8").trim(), "hex");
|
|
37
|
+
return key.length ? key : undefined;
|
|
36
38
|
}
|
|
37
39
|
catch {
|
|
38
40
|
return undefined;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
|
-
/**
|
|
43
|
+
/** The tree's key on this computer: the settings' own, else one of the old settings folders (core/oldsettings.ts). */
|
|
44
|
+
export function loadKey(env, treeId) {
|
|
45
|
+
return readKey(keyFile(env, treeId)) ?? oldKeys(env, treeId)[0]?.key;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The tree's key, the one in an old settings folder taken over into the settings of today where they have none: of two
|
|
49
|
+
* different ones (strom of a packaged app and of a terminal, each its own) the one `prefer` says (the one that sealed the
|
|
50
|
+
* last commit, the side that could still write), else the first.
|
|
51
|
+
*/
|
|
52
|
+
export function settleKey(env, treeId, prefer) {
|
|
53
|
+
const own = readKey(keyFile(env, treeId));
|
|
54
|
+
if (own)
|
|
55
|
+
return own;
|
|
56
|
+
const old = oldKeys(env, treeId);
|
|
57
|
+
if (!old.length)
|
|
58
|
+
return undefined;
|
|
59
|
+
const key = (old.length > 1 ? old.find((o) => prefer(o.key)) : undefined)?.key ?? old[0].key;
|
|
60
|
+
try {
|
|
61
|
+
const file = keyFile(env, treeId);
|
|
62
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
63
|
+
fs.writeFileSync(file, key.toString("hex") + "\n", { mode: 0o600, flag: "wx" });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// taken over next time
|
|
67
|
+
}
|
|
68
|
+
return key;
|
|
69
|
+
}
|
|
70
|
+
/** The tree's other keys on this computer: those of the old settings folders that are not `key` (strom seal adopt unites them). */
|
|
71
|
+
export function otherKeys(env, treeId, key) {
|
|
72
|
+
return oldKeys(env, treeId).filter((o) => !o.key.equals(key));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create the key for a tree on this computer (0600). Fails if it exists. Also into the old settings folder (Windows),
|
|
76
|
+
* made if need be: an older strom, after a switch back, reads it there.
|
|
77
|
+
*/
|
|
42
78
|
export function createKey(env, treeId) {
|
|
43
79
|
const file = keyFile(env, treeId);
|
|
44
80
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
45
81
|
const key = crypto.randomBytes(32);
|
|
46
82
|
fs.writeFileSync(file, key.toString("hex") + "\n", { mode: 0o600, flag: "wx" });
|
|
83
|
+
// also where an older strom looks (Windows: %APPDATA%\strom), made if need be — back on an older version, the research
|
|
84
|
+
// is still sealed here
|
|
85
|
+
const old = oldSettingsDirs(env)[0];
|
|
86
|
+
if (old) {
|
|
87
|
+
try {
|
|
88
|
+
fs.mkdirSync(path.join(old, "keys"), { recursive: true });
|
|
89
|
+
fs.writeFileSync(path.join(old, "keys", `${treeId}.key`), key.toString("hex") + "\n", { mode: 0o600, flag: "wx" });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// a key of its own there already: the older strom keeps it
|
|
93
|
+
}
|
|
94
|
+
}
|
|
47
95
|
return key;
|
|
48
96
|
}
|
|
49
97
|
/** Short public fingerprint of a key, stored on each operation. */
|
package/dist/core/tree.js
CHANGED
|
@@ -21,7 +21,7 @@ import { settledRecord, validateRecord } from "./validate.js";
|
|
|
21
21
|
import { NeedsConsentError, StromError, UsageError } from "./errors.js";
|
|
22
22
|
import * as git from "./git.js";
|
|
23
23
|
import { opsLogs } from "./opslog.js";
|
|
24
|
-
import { commitSeal, createKey, fingerprint, loadKey, sha256, signOp } from "./seal.js";
|
|
24
|
+
import { commitSeal, createKey, fingerprint, loadKey, settleKey, sha256, signOp, verifyCommitSeal } from "./seal.js";
|
|
25
25
|
import { stringifyCanonical } from "./json.js";
|
|
26
26
|
import { lastBackup } from "./backup.js";
|
|
27
27
|
export const TREE_FILE = "strom.json";
|
|
@@ -29,7 +29,7 @@ export const TREE_FILE = "strom.json";
|
|
|
29
29
|
export const SEAL_FOREIGN = "seal.foreign";
|
|
30
30
|
// between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
|
|
31
31
|
// at a release the release's own (package.json)
|
|
32
|
-
export const VERSION = "1.22.
|
|
32
|
+
export const VERSION = "1.22.2";
|
|
33
33
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
34
34
|
.strom/
|
|
35
35
|
/media/
|
|
@@ -569,7 +569,7 @@ export class Tree {
|
|
|
569
569
|
/** The tree's seal key on this computer, or undefined (tree moved here). */
|
|
570
570
|
get key() {
|
|
571
571
|
if (this.keyCache === undefined)
|
|
572
|
-
this.keyCache =
|
|
572
|
+
this.keyCache = settleKey(this.env, this.config.id, (key) => this.headSealedWith(key)) ?? null;
|
|
573
573
|
return this.keyCache ?? undefined;
|
|
574
574
|
}
|
|
575
575
|
lastStamp = 0;
|
|
@@ -582,6 +582,11 @@ export class Tree {
|
|
|
582
582
|
opsFilesTouched() {
|
|
583
583
|
return [...this.touchedOps].map((f) => this.relative(f));
|
|
584
584
|
}
|
|
585
|
+
/** Whether the last commit carries the seal of this key. */
|
|
586
|
+
headSealedWith(key) {
|
|
587
|
+
const head = git.headInfo(this.root);
|
|
588
|
+
return !!head && verifyCommitSeal(key, head.tree, head.message);
|
|
589
|
+
}
|
|
585
590
|
/** Forget the cached key (after one was created on this computer). */
|
|
586
591
|
reloadKey() {
|
|
587
592
|
this.keyCache = undefined;
|
|
@@ -605,7 +610,8 @@ export class Tree {
|
|
|
605
610
|
], undefined, undefined, { code: SEAL_FOREIGN, params: { name: this.config.name, set: "strom seal adopt" } });
|
|
606
611
|
}
|
|
607
612
|
/** Sealed commit of everything under pathspec. */
|
|
608
|
-
commit(
|
|
613
|
+
/** `always`: a commit even when nothing changed (strom repair sealing again what was committed outside strom). */
|
|
614
|
+
commit(message, pathspec = ["."], always = false) {
|
|
609
615
|
const key = this.requireKey();
|
|
610
616
|
// its IDs on disk before it commits (written once here, not after every record: a sync of 2 200 people wrote the
|
|
611
617
|
// counters thousands of times)
|
|
@@ -613,7 +619,7 @@ export class Tree {
|
|
|
613
619
|
// another process may have committed since this one last looked — under the lock it read HEAD afresh already
|
|
614
620
|
if (this.lockDepth === 0)
|
|
615
621
|
git.resetCache(this.root);
|
|
616
|
-
const hash = git.commitAll(this.root, message, pathspec, (treeHash) => commitSeal(key, treeHash));
|
|
622
|
+
const hash = git.commitAll(this.root, message, pathspec, (treeHash) => commitSeal(key, treeHash), always);
|
|
617
623
|
// the logs this commit holds are closed: what comes next goes into new ones
|
|
618
624
|
this.opsNow.clear();
|
|
619
625
|
if (hash) {
|