strom-research 1.24.0 → 1.25.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.cs.md +9 -0
- package/CHANGELOG.de.md +10 -0
- package/CHANGELOG.md +9 -0
- package/assets/lang/cs.json +1 -0
- package/assets/lang/de.json +1 -0
- package/dist/cli/main.js +3 -0
- package/dist/cli/ui.js +1 -0
- package/dist/commands/setup.js +4 -0
- package/dist/commands/start.js +1 -1
- package/dist/core/json.js +10 -4
- package/dist/core/live.js +2 -2
- package/dist/core/tree.js +1 -1
- package/dist/core/uninstall.js +33 -3
- package/dist/core/widget.js +106 -32
- package/package.json +4 -1
package/CHANGELOG.cs.md
CHANGED
|
@@ -5,6 +5,15 @@
|
|
|
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.25.0
|
|
9
|
+
|
|
10
|
+
*Připravuje se: bety do 1.25.0-beta.1. Oddíl bude úplný s vydáním.*
|
|
11
|
+
|
|
12
|
+
- **Data pro widget Strom i na Windows.** Výzkum teď zapisuje to, co z něj ukazuje widget Strom (na čem se pracuje,
|
|
13
|
+
nálezy za posledních 24 hodin, co čeká), i na Windows, do `%USERPROFILE%\.config\strom\widget`, se značkou verze
|
|
14
|
+
strom, která je zapsala. Widget, který zrovna čte svůj soubor, most nikdy nezdrží. `strom uninstall` odebere i soubory
|
|
15
|
+
widgetu, také na Macu; výzkum a nastavení zůstávají.
|
|
16
|
+
|
|
8
17
|
## 1.24.0
|
|
9
18
|
|
|
10
19
|
*31. 10. 2026 · patří k aplikaci Strom 3.19.0.*
|
package/CHANGELOG.de.md
CHANGED
|
@@ -5,6 +5,16 @@
|
|
|
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.25.0
|
|
9
|
+
|
|
10
|
+
*In Vorbereitung: die Betas bis 1.25.0-beta.1. Der Abschnitt ist mit dem Release vollständig.*
|
|
11
|
+
|
|
12
|
+
- **Daten für das Strom-Widget auch unter Windows.** Eine Forschung schreibt jetzt, was das Strom-Widget von ihr zeigt
|
|
13
|
+
(woran gearbeitet wird, die Funde der letzten 24 Stunden, was wartet), auch unter Windows, nach
|
|
14
|
+
`%USERPROFILE%\.config\strom\widget`, mit einer Marke des strom, der es geschrieben hat. Ein Widget, das gerade seine
|
|
15
|
+
Datei liest, hält die Brücke nie auf. `strom uninstall` entfernt auch die Dateien des Widgets, auch auf dem Mac; die
|
|
16
|
+
Forschung und die Einstellungen bleiben.
|
|
17
|
+
|
|
8
18
|
## 1.24.0
|
|
9
19
|
|
|
10
20
|
*31.10.2026 · gehört zur Strom-App 3.19.0.*
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.25.0
|
|
9
|
+
|
|
10
|
+
*In preparation: the betas up to 1.25.0-beta.1. The section is complete with the release.*
|
|
11
|
+
|
|
12
|
+
- **Data for the Strom widget on Windows too.** A research now writes what the Strom widget shows of it (what it works
|
|
13
|
+
on, the finds of the last 24 hours, what waits) also on Windows, into `%USERPROFILE%\.config\strom\widget`, with a
|
|
14
|
+
mark of the strom that wrote it. A widget reading its file never holds up the bridge. `strom uninstall` takes the
|
|
15
|
+
widget's files too, on a Mac as well; the research and the settings stay.
|
|
16
|
+
|
|
8
17
|
## 1.24.0
|
|
9
18
|
|
|
10
19
|
*2026-10-31 · goes with the Strom app 3.19.0.*
|
package/assets/lang/cs.json
CHANGED
|
@@ -881,6 +881,7 @@
|
|
|
881
881
|
"ui.uninstall.links": "odkazy z aplikace Strom",
|
|
882
882
|
"ui.uninstall.git": "vlastní git stromu",
|
|
883
883
|
"ui.uninstall.path": "záznam instalátoru v PATH",
|
|
884
|
+
"ui.uninstall.widget": "soubory widgetu Strom",
|
|
884
885
|
"ui.uninstall.program": "program",
|
|
885
886
|
"ui.uninstall.list": "strom z počítače odebere:",
|
|
886
887
|
"ui.uninstall.keeps": "Výzkumy zůstávají ({home}), stejně jako nastavení s klíči, které je pečetí ({config}) – smazat je ručně, jen pokud už nikdy nebudou potřeba.",
|
package/assets/lang/de.json
CHANGED
|
@@ -881,6 +881,7 @@
|
|
|
881
881
|
"ui.uninstall.links": "die Links aus der Strom-App",
|
|
882
882
|
"ui.uninstall.git": "strom's eigenes git",
|
|
883
883
|
"ui.uninstall.path": "der PATH-Eintrag des Installationsprogramms",
|
|
884
|
+
"ui.uninstall.widget": "die Dateien des Strom-Widgets",
|
|
884
885
|
"ui.uninstall.program": "das Programm",
|
|
885
886
|
"ui.uninstall.list": "strom entfernt von diesem Computer:",
|
|
886
887
|
"ui.uninstall.keeps": "Die Forschungen bleiben ({home}), ebenso die Einstellungen mit den Schlüsseln, die sie versiegeln ({config}) – sie nur dann selbst löschen, wenn sie nie wieder gebraucht werden.",
|
package/dist/cli/main.js
CHANGED
|
@@ -38,6 +38,7 @@ import { expandFromLine, refreshLinks } from "../core/links.js";
|
|
|
38
38
|
import { clockLine, FINISH_LINE, finishLimitLine, finishOf, timeLine } from "../core/clock.js";
|
|
39
39
|
import { currentSession } from "../core/session.js";
|
|
40
40
|
import { backgroundOf } from "../core/fetching.js";
|
|
41
|
+
import { markWidget } from "../core/widget.js";
|
|
41
42
|
import { callMistake, checkArgs, GroupOnly, parseOptions, resolveCommand, firstWord, severalRecords, splitPassthrough, usageOf } from "./execute.js";
|
|
42
43
|
import { placeholders, UI, ui } from "./ui.js";
|
|
43
44
|
import { catalog, localized, placeholdersOf, sayCommandAs } from "../core/phrases.js";
|
|
@@ -410,6 +411,8 @@ async function mainOf(argv, io, env, cwd) {
|
|
|
410
411
|
ctx.settings.config.lastChannel = channel;
|
|
411
412
|
ctx.settings.save();
|
|
412
413
|
refreshGlobal(env);
|
|
414
|
+
// …and the widget on the desktop (macOS, Windows) learns this strom writes its files: index.json, even with no tree
|
|
415
|
+
markWidget(env);
|
|
413
416
|
// …and the links from the Strom app lead to this strom again (where strom made them on this person's yes —
|
|
414
417
|
// never another installation's: one with its own settings asks)
|
|
415
418
|
try {
|
package/dist/cli/ui.js
CHANGED
|
@@ -718,6 +718,7 @@ export const UI = {
|
|
|
718
718
|
"ui.uninstall.links": "the links from the Strom app",
|
|
719
719
|
"ui.uninstall.git": "strom's own git",
|
|
720
720
|
"ui.uninstall.path": "the installer's PATH entry",
|
|
721
|
+
"ui.uninstall.widget": "the Strom widget's files",
|
|
721
722
|
"ui.uninstall.program": "the program",
|
|
722
723
|
"ui.uninstall.list": "strom takes this off the computer:",
|
|
723
724
|
"ui.uninstall.keeps": "The research stays ({home}), and so do the settings with the keys that seal it ({config}) — delete them by hand only if they are never wanted again.",
|
package/dist/commands/setup.js
CHANGED
|
@@ -68,6 +68,7 @@ import { ensureHooksDir } from "../core/hooks.js";
|
|
|
68
68
|
import { claudeInChrome, CLAUDE_IN_CHROME_URL, downloadsDir } from "../core/browser.js";
|
|
69
69
|
import { browserConnectors, fenceKeepsNetOff, listConnectors } from "../core/connector.js";
|
|
70
70
|
import { appReadLimits } from "../core/live-read.js";
|
|
71
|
+
import { markWidget } from "../core/widget.js";
|
|
71
72
|
export const SHARED_DIRS = ["media", "catalog", "tools", "cache", "inbox"];
|
|
72
73
|
export function ensureShared(dir) {
|
|
73
74
|
for (const d of SHARED_DIRS)
|
|
@@ -94,6 +95,8 @@ register({
|
|
|
94
95
|
const s = ctx.settings;
|
|
95
96
|
if (ctx.interactive && !isAgent(ctx.env)) {
|
|
96
97
|
const r = await setupWizard(ctx, { research: Boolean(opts.research) });
|
|
98
|
+
// the widget on the desktop (macOS, Windows) learns this strom writes its files
|
|
99
|
+
markWidget(ctx.env);
|
|
97
100
|
// run in an archive (the Strom app's link to the research's settings too): research offered beside it
|
|
98
101
|
let root;
|
|
99
102
|
try {
|
|
@@ -184,6 +187,7 @@ register({
|
|
|
184
187
|
cfg.trees = trees;
|
|
185
188
|
s.save();
|
|
186
189
|
fs.mkdirSync(home, { recursive: true });
|
|
190
|
+
markWidget(ctx.env);
|
|
187
191
|
const sharedDir = s.shared().value;
|
|
188
192
|
const treesDir = s.trees().value;
|
|
189
193
|
fs.mkdirSync(treesDir, { recursive: true });
|
package/dist/commands/start.js
CHANGED
|
@@ -497,7 +497,7 @@ register({
|
|
|
497
497
|
},
|
|
498
498
|
}, {
|
|
499
499
|
path: ["uninstall"],
|
|
500
|
-
summary: "Take strom off this computer: what the agents were taught, the shortcut, its own git, its PATH lines and the program — the research and the settings stay",
|
|
500
|
+
summary: "Take strom off this computer: what the agents were taught, the shortcut, its own git, its PATH lines, the widget's files and the program — the research and the settings stay",
|
|
501
501
|
group: "setup",
|
|
502
502
|
description: "The user's decision: a person at their terminal is asked there (--yes: no question); asked by an agent, strom asks\n" +
|
|
503
503
|
"in a window of the system. Installed through npm, the program itself goes with npm uninstall -g strom-research.",
|
package/dist/core/json.js
CHANGED
|
@@ -42,7 +42,7 @@ export function stringifyCanonical(value) {
|
|
|
42
42
|
export const RENAME_PATIENCE = 1500;
|
|
43
43
|
const RENAME_FIRST_WAIT = 25;
|
|
44
44
|
/** What another program holding the target (an antivirus, the search indexer, a backup) makes a rename fail with. */
|
|
45
|
-
const HELD = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
45
|
+
export const HELD = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
46
46
|
/**
|
|
47
47
|
* fn, again for a while on Windows when a file it uses is held: an antivirus, the indexer or a sync tool that has the
|
|
48
48
|
* file open for a moment makes a rename, a read or a removal fail with EPERM, EBUSY or EACCES. The waits grow (25, 50,
|
|
@@ -88,14 +88,20 @@ export function readFileRetrying(file, platform = process.platform) {
|
|
|
88
88
|
export function removeRetrying(file, platform = process.platform) {
|
|
89
89
|
retryingHeld(file, platform, () => fs.rmSync(file, { force: true }));
|
|
90
90
|
}
|
|
91
|
-
/**
|
|
92
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Write via a temp file in the same directory and rename: never a half file. `patient: false`: the rename tried once,
|
|
93
|
+
* never waited for a file another program holds (the widget's files: the bridge's tick is never held up by a reader).
|
|
94
|
+
*/
|
|
95
|
+
export function writeFileAtomic(file, content, opts = {}) {
|
|
93
96
|
const dir = path.dirname(file);
|
|
94
97
|
fs.mkdirSync(dir, { recursive: true });
|
|
95
98
|
const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`);
|
|
96
99
|
fs.writeFileSync(tmp, content, "utf8");
|
|
97
100
|
try {
|
|
98
|
-
|
|
101
|
+
if (opts.patient === false)
|
|
102
|
+
fs.renameSync(tmp, file);
|
|
103
|
+
else
|
|
104
|
+
renameRetrying(tmp, file);
|
|
99
105
|
}
|
|
100
106
|
catch (err) {
|
|
101
107
|
fs.rmSync(tmp, { force: true });
|
package/dist/core/live.js
CHANGED
|
@@ -3727,8 +3727,8 @@ export function serveLive(root, env) {
|
|
|
3727
3727
|
return atWorkNow;
|
|
3728
3728
|
};
|
|
3729
3729
|
let workers = safe("the start", () => JSON.stringify(atWork(seen))) ?? "";
|
|
3730
|
-
// the research on the desktop of a Mac
|
|
3731
|
-
// (core/widget.ts; none
|
|
3730
|
+
// the research on the desktop (the Strom Widget of a Mac, the widget of Windows 11): the tree's overview written
|
|
3731
|
+
// while the bridge runs (core/widget.ts; none but on macOS and Windows) — the first a moment after the start, never in its way
|
|
3732
3732
|
const widget = widgetWriter(root, env);
|
|
3733
3733
|
if (widget)
|
|
3734
3734
|
setTimeout(() => safe("the widget", () => widget.tick(true)), 1000).unref?.();
|
package/dist/core/tree.js
CHANGED
|
@@ -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.
|
|
32
|
+
export const VERSION = "1.25.0-beta.1";
|
|
33
33
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
34
34
|
.strom/
|
|
35
35
|
/media/
|
package/dist/core/uninstall.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// What strom put on this computer outside the research, and taking it away
|
|
2
2
|
// again (strom uninstall): what the agents were taught, the shortcut, strom's
|
|
3
|
-
// own git, the PATH lines of the installer
|
|
4
|
-
//
|
|
5
|
-
// are the user's; strom
|
|
3
|
+
// own git, the PATH lines of the installer, the widget's files (what it
|
|
4
|
+
// showed of the research) and the program itself. The research stays, and so
|
|
5
|
+
// do the settings with the keys that seal it — they are the user's; strom
|
|
6
|
+
// says where they are.
|
|
6
7
|
import fs from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -13,6 +14,7 @@ import { globalTargets, isInstalled, uninstallGlobal } from "../agents/global.js
|
|
|
13
14
|
import { PROFILES } from "../agents/profiles.js";
|
|
14
15
|
import { holdsPath, installation, pathForms, PROGRAM_ENTRIES } from "./self.js";
|
|
15
16
|
import { linkFiles, linkOwner, unregisterLinks } from "./links.js";
|
|
17
|
+
import { widgetDir } from "./widget.js";
|
|
16
18
|
/** The mark the installer (install/install.sh) puts on the PATH line it adds. */
|
|
17
19
|
const MARK = "# strom research";
|
|
18
20
|
const sys = (cmd, args) => {
|
|
@@ -328,6 +330,11 @@ export function uninstallPlan(env, names, opts = {}) {
|
|
|
328
330
|
// none
|
|
329
331
|
}
|
|
330
332
|
}
|
|
333
|
+
// the widget's files (macOS, Windows): only what strom wrote there, the folder when nothing else is left in it — the
|
|
334
|
+
// widget would show the research as not at work for ever
|
|
335
|
+
const widget = widgetDir(env, platform);
|
|
336
|
+
if (widget && widgetFiles(widget).length && !holdsKept(widget, keep, platform))
|
|
337
|
+
remove.push({ kind: "widget", path: widget, remove: () => removeWidgetFiles(widget) });
|
|
331
338
|
const program = inst.kind === "installed" ? inst.root : undefined;
|
|
332
339
|
// install.json's launchers: those of a launcher's name outside the research only
|
|
333
340
|
const chosen = launchersToTake(inst.launchers ?? [], keep, platform);
|
|
@@ -346,3 +353,26 @@ export function uninstallPlan(env, names, opts = {}) {
|
|
|
346
353
|
const skipped = chosen.skipped.filter((l) => !pathForms(l, platform).some((f) => taken.has(f)));
|
|
347
354
|
return { remove, npm: inst.kind === "npm", ...(program ? { program, launchers: chosen.take, skipped } : {}) };
|
|
348
355
|
}
|
|
356
|
+
/** What strom writes into the widget's folder: index.json, each tree's <ID>.json and a file being written beside one. */
|
|
357
|
+
const WIDGET_FILE = /^(?:index|[0-9A-Za-z-]{1,64})\.json$|^\..+\.json\.\d+\.[0-9a-f]+\.tmp$/u;
|
|
358
|
+
export function widgetFiles(dir) {
|
|
359
|
+
try {
|
|
360
|
+
return fs.readdirSync(dir).filter((n) => WIDGET_FILE.test(n) && fs.statSync(path.join(dir, n)).isFile());
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/** The widget's files taken; the folder too when nothing else is in it (never what strom did not write there). */
|
|
367
|
+
export function removeWidgetFiles(dir) {
|
|
368
|
+
for (const n of widgetFiles(dir))
|
|
369
|
+
fs.rmSync(path.join(dir, n), { force: true });
|
|
370
|
+
try {
|
|
371
|
+
if (!fs.readdirSync(dir).length)
|
|
372
|
+
fs.rmdirSync(dir);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// gone already, or another program's file in it: it stays
|
|
376
|
+
}
|
|
377
|
+
return !widgetFiles(dir).length;
|
|
378
|
+
}
|
package/dist/core/widget.js
CHANGED
|
@@ -4,20 +4,22 @@
|
|
|
4
4
|
// phone-finds.ts) and cleaned as for it (forPhone): never a path, a record's ID, a spend or a model. Beside each text
|
|
5
5
|
// that names a person, the same without the names (the widget's "hide names").
|
|
6
6
|
//
|
|
7
|
-
// Written by the tree's bridge (strom live) while it runs, on macOS
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// folder — a research of a test
|
|
7
|
+
// Written by the tree's bridge (strom live) while it runs, on macOS (~/Library/Application Support/Strom/widget/) and
|
|
8
|
+
// Windows (%USERPROFILE%\.config\strom\widget\, the Windows 11 widget of strom-win): <tree id>.json and index.json
|
|
9
|
+
// (the trees there), each whole (a file beside it renamed over it). Looked at every WIDGET_CHECK_MS, written when what
|
|
10
|
+
// it says changed, else every WIDGET_HEARTBEAT_MS; the bridge ending as it should writes `running: false`, its research
|
|
11
|
+
// gone takes the file away. An archive has none. The widget only reads: nothing of it comes back. An isolated
|
|
12
|
+
// installation (STROM_ISOLATED: a test's, a second strom) writes into its own settings folder — a research of a test
|
|
13
|
+
// never shows in the person's widget. index.json is there from this strom's first run and its setup on (`strom`: the
|
|
14
|
+
// version that wrote it), so the widget tells a strom too old to write any from one whose bridge has not run yet.
|
|
13
15
|
import fs from "node:fs";
|
|
14
16
|
import path from "node:path";
|
|
15
17
|
import { configDir, isolated, userHome } from "./paths.js";
|
|
16
18
|
import { Settings } from "./config.js";
|
|
17
|
-
import { Tree } from "./tree.js";
|
|
19
|
+
import { Tree, VERSION } from "./tree.js";
|
|
18
20
|
import { actsAsArchive } from "./state.js";
|
|
19
21
|
import { foldText } from "./text.js";
|
|
20
|
-
import { writeFileAtomic } from "./json.js";
|
|
22
|
+
import { writeFileAtomic, HELD } from "./json.js";
|
|
21
23
|
import { appUrlSetting } from "./stromapp.js";
|
|
22
24
|
import { forPhone, treeOverview } from "./phone-watch.js";
|
|
23
25
|
import { treeFinds } from "./phone-finds.js";
|
|
@@ -37,17 +39,23 @@ export const WIDGET_PENDING = 3;
|
|
|
37
39
|
/** What stands for a name taken out of a text. */
|
|
38
40
|
const HIDDEN = "…";
|
|
39
41
|
/**
|
|
40
|
-
* Where the widget's files are written: none but on macOS; STROM_WIDGET_DIR when set; an isolated
|
|
41
|
-
* own settings folder; else ~/Library/Application Support/Strom/widget of the HOME strom
|
|
42
|
+
* Where the widget's files are written: none but on macOS and Windows; STROM_WIDGET_DIR when set; an isolated
|
|
43
|
+
* installation into its own settings folder; else on macOS ~/Library/Application Support/Strom/widget of the HOME strom
|
|
44
|
+
* runs with, on Windows %USERPROFILE%\.config\strom\widget — the profile's own folder (HOME may be Git Bash's), also
|
|
45
|
+
* with settings elsewhere (STROM_CONFIG_DIR), as the Mac's ~/Library. Never under AppData: an app packaged as MSIX
|
|
46
|
+
* (Claude's desktop app, whose agent starts the bridge) gets every new file there as a private copy of its own, which
|
|
47
|
+
* the widget would never see (found 2026-10-30 with the seal keys; core/paths.ts configDir).
|
|
42
48
|
*/
|
|
43
49
|
export function widgetDir(env, platform = process.platform) {
|
|
44
|
-
if (platform !== "darwin")
|
|
50
|
+
if (platform !== "darwin" && platform !== "win32")
|
|
45
51
|
return undefined;
|
|
46
52
|
const said = env.STROM_WIDGET_DIR?.trim();
|
|
47
53
|
if (said)
|
|
48
54
|
return path.resolve(said);
|
|
49
55
|
if (isolated(env))
|
|
50
56
|
return path.join(configDir(env, platform), "widget");
|
|
57
|
+
if (platform === "win32")
|
|
58
|
+
return path.join(env.USERPROFILE || userHome(env), ".config", "strom", "widget");
|
|
51
59
|
return path.join(userHome(env), "Library", "Application Support", "Strom", "widget");
|
|
52
60
|
}
|
|
53
61
|
/** A moment as the widget reads it: ISO 8601 to the second, in UTC (Swift's .iso8601 reads no fractions). */
|
|
@@ -318,15 +326,37 @@ export function widgetTree(root, env, opts) {
|
|
|
318
326
|
const treeFile = (dir, key) => path.join(dir, `${key}.json`);
|
|
319
327
|
/** A tree's key as a file's name: the tree's ID (a UUID) — never a path. */
|
|
320
328
|
const KEY = /^[0-9A-Za-z-]{1,64}$/u;
|
|
321
|
-
/**
|
|
329
|
+
/**
|
|
330
|
+
* A file of the widget written whole (a file beside it renamed over it) — at once or not now: the widget reading it this
|
|
331
|
+
* moment holds it on Windows (a rename over an open file fails with EPERM or EBUSY there), and the bridge never waits
|
|
332
|
+
* for it (writeFileAtomic's patience would hold the bridge's tick for 1.5 s). False: not written, nothing left beside
|
|
333
|
+
* it; the next look writes it.
|
|
334
|
+
*/
|
|
335
|
+
function writeNow(file, content) {
|
|
336
|
+
try {
|
|
337
|
+
writeFileAtomic(file, content, { patient: false });
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
if (HELD.has(err.code ?? ""))
|
|
342
|
+
return false;
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* index.json as the files of the trees there say it, written whole when it says something else (or another strom wrote
|
|
348
|
+
* it); the folder made when there is none (the mark of this strom: strom's first run, its setup). False: held by the
|
|
349
|
+
* widget this moment, not written.
|
|
350
|
+
*/
|
|
322
351
|
export function writeWidgetIndex(dir, now = Date.now()) {
|
|
323
352
|
const trees = [];
|
|
324
353
|
let names = [];
|
|
325
354
|
try {
|
|
326
355
|
names = fs.readdirSync(dir).filter((n) => n.endsWith(".json") && n !== "index.json" && !n.startsWith("."));
|
|
327
356
|
}
|
|
328
|
-
catch {
|
|
329
|
-
|
|
357
|
+
catch (err) {
|
|
358
|
+
if (err.code !== "ENOENT")
|
|
359
|
+
return true;
|
|
330
360
|
}
|
|
331
361
|
for (const n of names.sort()) {
|
|
332
362
|
try {
|
|
@@ -342,34 +372,64 @@ export function writeWidgetIndex(dir, now = Date.now()) {
|
|
|
342
372
|
const file = path.join(dir, "index.json");
|
|
343
373
|
try {
|
|
344
374
|
const was = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
345
|
-
if (JSON.stringify(was.trees) === JSON.stringify(trees) && was.schemaVersion === WIDGET_SCHEMA)
|
|
346
|
-
return;
|
|
375
|
+
if (JSON.stringify(was.trees) === JSON.stringify(trees) && was.schemaVersion === WIDGET_SCHEMA && was.strom === VERSION)
|
|
376
|
+
return true;
|
|
347
377
|
}
|
|
348
378
|
catch {
|
|
349
379
|
// none yet
|
|
350
380
|
}
|
|
351
|
-
|
|
381
|
+
return writeNow(file, `${JSON.stringify({ schemaVersion: WIDGET_SCHEMA, generatedAt: isoSecond(now), strom: VERSION, trees }, null, 2)}\n`);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* The mark that this strom writes the widget's files (on macOS and Windows): index.json, with no tree in it when no
|
|
385
|
+
* bridge ran yet — written at strom's first run of a version and at its setup. A widget that finds none tells a strom too
|
|
386
|
+
* old to write any (strom update) from one whose bridge has not run yet. Never in the way: any failure said nowhere.
|
|
387
|
+
*/
|
|
388
|
+
export function markWidget(env, platform = process.platform) {
|
|
389
|
+
const dir = widgetDir(env, platform);
|
|
390
|
+
if (!dir)
|
|
391
|
+
return;
|
|
392
|
+
try {
|
|
393
|
+
writeWidgetIndex(dir);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// written by the next bridge
|
|
397
|
+
}
|
|
352
398
|
}
|
|
353
|
-
/** A tree's file written whole (a file beside it renamed over it), then index.json. */
|
|
399
|
+
/** A tree's file written whole (a file beside it renamed over it), then index.json. False: held by the widget, not now. */
|
|
354
400
|
export function writeWidgetTree(dir, data) {
|
|
355
401
|
if (!KEY.test(data.treeKey))
|
|
356
|
-
return;
|
|
357
|
-
|
|
358
|
-
|
|
402
|
+
return true;
|
|
403
|
+
if (!writeNow(treeFile(dir, data.treeKey), `${JSON.stringify(data, null, 2)}\n`))
|
|
404
|
+
return false;
|
|
405
|
+
return writeWidgetIndex(dir, Date.parse(data.generatedAt) || Date.now());
|
|
359
406
|
}
|
|
360
|
-
/**
|
|
361
|
-
|
|
407
|
+
/**
|
|
408
|
+
* A tree's file taken away (its research gone, an archive now), and index.json without it. One the widget holds without
|
|
409
|
+
* letting it go (Windows) stays: it says at least `running: false` (as `last` was, or as it says), not a research at work
|
|
410
|
+
* for the quarter of an hour the widget takes to call it old.
|
|
411
|
+
*/
|
|
412
|
+
export function dropWidgetTree(dir, key, last) {
|
|
362
413
|
if (!KEY.test(key))
|
|
363
414
|
return;
|
|
415
|
+
const file = treeFile(dir, key);
|
|
364
416
|
try {
|
|
365
|
-
fs.rmSync(
|
|
417
|
+
fs.rmSync(file, { force: true });
|
|
366
418
|
}
|
|
367
419
|
catch {
|
|
420
|
+
try {
|
|
421
|
+
const was = last ?? JSON.parse(fs.readFileSync(file, "utf8"));
|
|
422
|
+
const at = isoSecond(Date.now());
|
|
423
|
+
writeWidgetTree(dir, { ...was, running: false, generatedAt: at, lastActiveAt: at });
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
// nothing more to do: the widget calls it old by itself
|
|
427
|
+
}
|
|
368
428
|
return;
|
|
369
429
|
}
|
|
370
430
|
writeWidgetIndex(dir);
|
|
371
431
|
}
|
|
372
|
-
/** The writer of a tree's bridge; none
|
|
432
|
+
/** The writer of a tree's bridge; none but on macOS and Windows. */
|
|
373
433
|
export function widgetWriter(root, env, platform = process.platform, clock = Date.now) {
|
|
374
434
|
const dir = widgetDir(env, platform);
|
|
375
435
|
if (!dir)
|
|
@@ -381,40 +441,54 @@ export function widgetWriter(root, env, platform = process.platform, clock = Dat
|
|
|
381
441
|
let said = "";
|
|
382
442
|
let last;
|
|
383
443
|
let key;
|
|
444
|
+
// what the widget held at the last look (Windows: renamed over an open file): written at the next tick, not the next look
|
|
445
|
+
let held;
|
|
384
446
|
// what moves with the clock alone says nothing new
|
|
385
447
|
const content = (t) => JSON.stringify({ ...t, generatedAt: "", lastActiveAt: "", findings: { ...t.findings, windowStart: "" } });
|
|
448
|
+
const write = (t, c, now) => {
|
|
449
|
+
if (!writeWidgetTree(dir, t)) {
|
|
450
|
+
held = { t, c };
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
held = undefined;
|
|
454
|
+
said = c;
|
|
455
|
+
written = now;
|
|
456
|
+
};
|
|
386
457
|
return {
|
|
387
458
|
tick(force = false) {
|
|
388
459
|
const now = clock();
|
|
389
|
-
if (!force && now - checked < checkMs)
|
|
460
|
+
if (!force && now - checked < checkMs) {
|
|
461
|
+
if (held)
|
|
462
|
+
write({ ...held.t, generatedAt: isoSecond(now), lastActiveAt: isoSecond(now) }, held.c, now);
|
|
390
463
|
return;
|
|
464
|
+
}
|
|
391
465
|
checked = now;
|
|
392
466
|
const t = widgetTree(root, env, { running: true, now });
|
|
393
467
|
if (!t) {
|
|
394
468
|
// an archive now: nothing of it on the desktop
|
|
395
469
|
if (key)
|
|
396
|
-
dropWidgetTree(dir, key);
|
|
470
|
+
dropWidgetTree(dir, key, last);
|
|
397
471
|
key = undefined;
|
|
398
472
|
last = undefined;
|
|
473
|
+
held = undefined;
|
|
399
474
|
said = "";
|
|
400
475
|
return;
|
|
401
476
|
}
|
|
402
477
|
key = t.treeKey;
|
|
403
478
|
last = t;
|
|
404
479
|
const c = content(t);
|
|
405
|
-
if (c === said && now - written < beatMs)
|
|
480
|
+
if (c === said && now - written < beatMs && !held)
|
|
406
481
|
return;
|
|
407
|
-
|
|
408
|
-
said = c;
|
|
409
|
-
written = now;
|
|
482
|
+
write(t, c, now);
|
|
410
483
|
},
|
|
411
484
|
end(gone) {
|
|
412
485
|
if (!key)
|
|
413
486
|
return;
|
|
414
487
|
// what the file says now is no longer what was written: the next look writes it again
|
|
415
488
|
said = "";
|
|
489
|
+
held = undefined;
|
|
416
490
|
if (gone)
|
|
417
|
-
return dropWidgetTree(dir, key);
|
|
491
|
+
return dropWidgetTree(dir, key, last && { ...last, running: false });
|
|
418
492
|
if (!last)
|
|
419
493
|
return;
|
|
420
494
|
const at = isoSecond(clock());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strom-research",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0-beta.1",
|
|
4
4
|
"description": "Genealogical research toolkit for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -57,5 +57,8 @@
|
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^22.20.4",
|
|
59
59
|
"typescript": "^7.0.2"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"tag": "beta"
|
|
60
63
|
}
|
|
61
64
|
}
|