strom-research 1.4.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/lang/cs.json +160 -1
- package/assets/lang/de.json +160 -1
- package/assets/method/narrate.md +2 -1
- package/assets/method/recording.md +3 -1
- package/dist/agents/files.js +8 -1
- package/dist/cli/human.js +18 -0
- package/dist/cli/main.js +9 -0
- package/dist/cli/menu-links.js +249 -0
- package/dist/cli/menu-parts.js +6 -3
- package/dist/cli/menu-research.js +185 -7
- package/dist/cli/menu-settings.js +17 -3
- package/dist/cli/menu-waiting.js +61 -47
- package/dist/cli/menu.js +3 -2
- package/dist/cli/ui.js +159 -1
- package/dist/cli/wizard.js +34 -1
- package/dist/commands/analysis.js +6 -2
- package/dist/commands/browse.js +5 -19
- package/dist/commands/checks.js +7 -1
- package/dist/commands/guide.js +15 -1
- package/dist/commands/index.js +2 -0
- package/dist/commands/intake.js +3 -0
- package/dist/commands/links.js +367 -0
- package/dist/commands/output.js +7 -1
- package/dist/commands/people.js +58 -1
- package/dist/commands/research.js +113 -24
- package/dist/commands/setup.js +13 -2
- package/dist/commands/start.js +6 -2
- package/dist/commands/story.js +17 -1
- package/dist/commands/sync.js +243 -0
- package/dist/core/actions.js +15 -0
- package/dist/core/config.js +5 -0
- package/dist/core/dialog.js +25 -0
- package/dist/core/excerpt.js +53 -5
- package/dist/core/frontier.js +10 -1
- package/dist/core/links.js +357 -0
- package/dist/core/live.js +156 -7
- package/dist/core/phrases.js +1 -0
- package/dist/core/review.js +43 -5
- package/dist/core/schema.js +1 -0
- package/dist/core/self.js +17 -0
- package/dist/core/session.js +6 -0
- package/dist/core/stromapp.js +38 -0
- package/dist/core/sync.js +876 -0
- package/dist/core/tree.js +1 -1
- package/dist/core/uninstall.js +3 -0
- package/dist/core/validate.js +4 -0
- package/dist/gedcom/export.js +66 -1
- package/dist/gedcom/validate.js +13 -4
- package/package.json +1 -1
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// strom-research:// links from the Strom app (core/links.ts): strom link open
|
|
2
|
+
// is what the system runs for one; strom link on|off|status registers the
|
|
3
|
+
// scheme for this user, takes it off, says where it leads.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { register } from "../cli/registry.js";
|
|
8
|
+
import { lines } from "../cli/format.js";
|
|
9
|
+
import { ui } from "../cli/ui.js";
|
|
10
|
+
import { readJsonIfExists } from "../core/json.js";
|
|
11
|
+
import { Tree } from "../core/tree.js";
|
|
12
|
+
import { LinkError, linkHandlerState, linkText, parseLink, registerLinks, unregisterLinks } from "../core/links.js";
|
|
13
|
+
import { displayName, lifespan } from "../core/people.js";
|
|
14
|
+
import { PROFILES } from "../agents/profiles.js";
|
|
15
|
+
import { whereToTalk } from "../core/apps.js";
|
|
16
|
+
import { pause } from "../cli/menu-parts.js";
|
|
17
|
+
import { fullExcerpt } from "../core/excerpt.js";
|
|
18
|
+
import { openForUser } from "../core/open.js";
|
|
19
|
+
import { openInNewTerminal } from "../core/shortcut.js";
|
|
20
|
+
import { systemNotice } from "../core/dialog.js";
|
|
21
|
+
import { configDir } from "../core/paths.js";
|
|
22
|
+
/** The researches here with this id — a copy of a research folder has its id too; the current one first. */
|
|
23
|
+
function researchesWithId(ctx, id) {
|
|
24
|
+
const current = ctx.settings.config.currentTree;
|
|
25
|
+
return ctx
|
|
26
|
+
.knownTrees()
|
|
27
|
+
.filter((k) => readJsonIfExists(path.join(k.root, "strom.json"))?.id?.toLowerCase() === id)
|
|
28
|
+
.sort((a, b) => Number(b.root === current) - Number(a.root === current));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What a link could not do, said where the person is: the terminal, else a window (a link opens strom with none).
|
|
32
|
+
* The result carries it too.
|
|
33
|
+
*/
|
|
34
|
+
async function say(ctx, key, values = {}, data = {}) {
|
|
35
|
+
const lang = ctx.uiLang();
|
|
36
|
+
const text = ui(lang, key, values);
|
|
37
|
+
if (!ctx.io.tty)
|
|
38
|
+
systemNotice(ui(lang, "ui.dialog.title"), text, ui(lang, "ui.link.ok"), ctx.env);
|
|
39
|
+
return { text, data: { done: false, ...data } };
|
|
40
|
+
}
|
|
41
|
+
/** The person's answer about links, kept: the question when the app opens is not asked again. */
|
|
42
|
+
function keepAnswer(ctx, answer) {
|
|
43
|
+
ctx.settings.reload();
|
|
44
|
+
ctx.settings.config.links = answer;
|
|
45
|
+
ctx.settings.save();
|
|
46
|
+
}
|
|
47
|
+
/** macOS: the terminal program this runs in, by its mark (TERM_PROGRAM). */
|
|
48
|
+
const TERMINALS = { Apple_Terminal: "Terminal", "iTerm.app": "iTerm", WezTerm: "WezTerm", ghostty: "Ghostty" };
|
|
49
|
+
/**
|
|
50
|
+
* The terminal a link opened comes in front of the Strom app when the app has answered (the tree came, or
|
|
51
|
+
* nothing did): the person sees what came of it. macOS; elsewhere the window stays where it is.
|
|
52
|
+
*/
|
|
53
|
+
function bringForward(env) {
|
|
54
|
+
const app = TERMINALS[env.TERM_PROGRAM ?? ""];
|
|
55
|
+
if (process.platform !== "darwin" || !app || env.STROM_NO_OPEN === "1")
|
|
56
|
+
return;
|
|
57
|
+
spawnSync("osascript", ["-e", `tell application "${app}" to activate`], { stdio: "ignore", timeout: 5000 });
|
|
58
|
+
}
|
|
59
|
+
/** strom itself in a research's folder (none: where strom is), as the menu runs it — quiet: its output not shown; said: handed over. */
|
|
60
|
+
function runnerOf(ctx) {
|
|
61
|
+
return async (root) => {
|
|
62
|
+
const { main } = await import("../cli/main.js");
|
|
63
|
+
return (argv, quiet, said) => main(argv, said ? { ...ctx.io, stdout: said } : quiet ? { ...ctx.io, stdout: () => undefined } : ctx.io, ctx.env, root ?? ctx.cwd);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** A person of the research a link names: the one they were merged into; one taken out is none. */
|
|
67
|
+
function personOf(root, ctx, id) {
|
|
68
|
+
const tree = Tree.open(root, ctx.env);
|
|
69
|
+
let p = tree.get(id);
|
|
70
|
+
if (p?.mergedInto)
|
|
71
|
+
p = tree.get(p.mergedInto);
|
|
72
|
+
return p && !p.retracted ? p : undefined;
|
|
73
|
+
}
|
|
74
|
+
/** A person as the user reads them, with the ID to name them by: "Jan Novák (*1905) [P0012]". */
|
|
75
|
+
function who(p) {
|
|
76
|
+
const span = lifespan(p);
|
|
77
|
+
return `${displayName(p)}${span ? ` (${span})` : ""} [${p.id}]`;
|
|
78
|
+
}
|
|
79
|
+
/** The conversation's first message about one person: only who (from the research's own record), what to ask is the person's. */
|
|
80
|
+
function aboutPerson(lang, p) {
|
|
81
|
+
return ui(lang, "ui.link.chat.say", { person: who(p) });
|
|
82
|
+
}
|
|
83
|
+
/** The conversation's first message about a conflict: its ID and title from the research itself. */
|
|
84
|
+
function aboutConflict(root, ctx, id) {
|
|
85
|
+
const tree = Tree.open(root, ctx.env);
|
|
86
|
+
const c = tree.get(id);
|
|
87
|
+
return c && c.type === "conflict" && c.state === "open" ? ui(tree.lang, "ui.link.conflict.say", { id: c.id, title: c.title }) : undefined;
|
|
88
|
+
}
|
|
89
|
+
/** What a link asks, in this terminal: which copy of the research, then as the menu does it. */
|
|
90
|
+
async function inTerminal(ctx, link) {
|
|
91
|
+
const lang = ctx.uiLang();
|
|
92
|
+
const copies = researchesWithId(ctx, link.tree);
|
|
93
|
+
if (!copies.length)
|
|
94
|
+
return say(ctx, "ui.link.notree", {}, { tree: link.tree });
|
|
95
|
+
let root = copies[0].root;
|
|
96
|
+
if (copies.length > 1) {
|
|
97
|
+
const i = await ctx.choose(ui(lang, link.action === "send" ? "ui.link.which" : "ui.link.which.one"), copies.map((c) => ({ label: `${c.name} (${ctx.display(c.root)})` })), 0, { back: ui(lang, "ui.browse.back") });
|
|
98
|
+
if (i === undefined)
|
|
99
|
+
return { text: "", data: { done: false } };
|
|
100
|
+
root = copies[i].root;
|
|
101
|
+
}
|
|
102
|
+
const run = await runnerOf(ctx)(root);
|
|
103
|
+
const tree = Tree.open(root, ctx.env);
|
|
104
|
+
const treeLang = tree.lang;
|
|
105
|
+
const end = async (done) => {
|
|
106
|
+
await pause(ctx, treeLang, "ui.enter.close");
|
|
107
|
+
return { text: "", data: { done, action: link.action, tree: root } };
|
|
108
|
+
};
|
|
109
|
+
const said = async (key, values = {}) => {
|
|
110
|
+
ctx.io.stdout(ui(treeLang, key, values) + "\n");
|
|
111
|
+
return end(false);
|
|
112
|
+
};
|
|
113
|
+
const agent = PROFILES[ctx.settings.agent(tree.config).value]?.name ?? ctx.settings.agent(tree.config).value;
|
|
114
|
+
switch (link.action) {
|
|
115
|
+
case "send": {
|
|
116
|
+
const { syncFromApp } = await import("../cli/menu-research.js");
|
|
117
|
+
await syncFromApp(ctx, run, treeLang, root, "ui.enter.close", () => bringForward(ctx.env));
|
|
118
|
+
return { text: "", data: { done: true, action: link.action, tree: root } };
|
|
119
|
+
}
|
|
120
|
+
// the research's menu, as `strom` in its folder; the research into the app's window, as strom app; strom's own
|
|
121
|
+
// commands, which ask themselves
|
|
122
|
+
case "open":
|
|
123
|
+
case "app":
|
|
124
|
+
return { text: "", data: { done: (await run(link.action === "open" ? [] : ["app"])) === 0, action: link.action, tree: root } };
|
|
125
|
+
case "update":
|
|
126
|
+
case "setup":
|
|
127
|
+
return end((await run([link.action])) === 0);
|
|
128
|
+
case "sessions": {
|
|
129
|
+
const { sessionsView } = await import("../cli/menu-links.js");
|
|
130
|
+
sessionsView(ctx, treeLang, root);
|
|
131
|
+
return end(true);
|
|
132
|
+
}
|
|
133
|
+
case "task": {
|
|
134
|
+
if (link.do) {
|
|
135
|
+
const { taskDo } = await import("../cli/menu-links.js");
|
|
136
|
+
return end(await taskDo(ctx, run, treeLang, root, link.task, link.do));
|
|
137
|
+
}
|
|
138
|
+
const { waitingTask } = await import("../cli/menu-waiting.js");
|
|
139
|
+
if (!(await waitingTask(ctx, run, treeLang, root, link.task)))
|
|
140
|
+
return said("ui.link.notask");
|
|
141
|
+
return end(true);
|
|
142
|
+
}
|
|
143
|
+
case "conflict": {
|
|
144
|
+
if (link.do === "decide") {
|
|
145
|
+
const { decideConflict } = await import("../cli/menu-links.js");
|
|
146
|
+
return end(await decideConflict(ctx, run, treeLang, root, link.id));
|
|
147
|
+
}
|
|
148
|
+
// left to the agent: it costs — said and asked first, then the conversation about it
|
|
149
|
+
const say = aboutConflict(root, ctx, link.id);
|
|
150
|
+
if (!say)
|
|
151
|
+
return said("ui.link.conflict.none", { id: link.id });
|
|
152
|
+
const { asks } = await import("../cli/menu-links.js");
|
|
153
|
+
if (!(await asks(ctx, treeLang, root, ui(treeLang, "ui.link.what.conflict.agent", { agent, title: tree.get(link.id).title }), ui(treeLang, "ui.link.cost"))))
|
|
154
|
+
return end(false);
|
|
155
|
+
return { text: "", data: { done: (await run(["chat", `--say=${say}`])) === 0, action: link.action, tree: root } };
|
|
156
|
+
}
|
|
157
|
+
case "sync-undo": {
|
|
158
|
+
const { undoSending } = await import("../cli/menu-links.js");
|
|
159
|
+
return end(await undoSending(ctx, run, treeLang, root, link.intake));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const named = "person" in link ? link.person : undefined;
|
|
163
|
+
const person = named ? personOf(root, ctx, named) : undefined;
|
|
164
|
+
if (named && !person)
|
|
165
|
+
return said("ui.link.noperson", { person: named });
|
|
166
|
+
const { asks } = await import("../cli/menu-links.js");
|
|
167
|
+
switch (link.action) {
|
|
168
|
+
case "chat": {
|
|
169
|
+
// the agent costs: said and asked first, then the conversation as strom chat starts it
|
|
170
|
+
const what = person ? ui(treeLang, "ui.link.what.chat.person", { agent, person: who(person) }) : ui(treeLang, "ui.link.what.chat", { agent });
|
|
171
|
+
if (!(await asks(ctx, treeLang, root, what, ui(treeLang, "ui.link.cost"))))
|
|
172
|
+
return { text: "", data: { done: false, action: link.action, tree: root } };
|
|
173
|
+
return { text: "", data: { done: (await run(["chat", ...(person ? [`--say=${aboutPerson(treeLang, person)}`] : [])])) === 0, action: link.action, tree: root } };
|
|
174
|
+
}
|
|
175
|
+
case "story": {
|
|
176
|
+
const { approveStory } = await import("../cli/menu-links.js");
|
|
177
|
+
return end(await approveStory(ctx, run, treeLang, root, person, who(person)));
|
|
178
|
+
}
|
|
179
|
+
case "review": {
|
|
180
|
+
const what = ui(treeLang, `ui.link.what.review.${link.scope}`, { person: who(person) });
|
|
181
|
+
if (await asks(ctx, treeLang, root, what, ui(treeLang, "ui.link.tasks"))) {
|
|
182
|
+
const { reviewOne } = await import("../cli/menu-research.js");
|
|
183
|
+
await reviewOne(ctx, run, treeLang, root, person.id, link.scope);
|
|
184
|
+
}
|
|
185
|
+
return end(true);
|
|
186
|
+
}
|
|
187
|
+
case "research": {
|
|
188
|
+
// the same direction still going: only named, nothing asked
|
|
189
|
+
const same = tree.list("research").find((r) => r.focus === person.id && r.direction === link.direction && r.state === "active");
|
|
190
|
+
if (same)
|
|
191
|
+
return said("ui.research.exists", { name: same.name });
|
|
192
|
+
const what = ui(treeLang, `ui.link.what.research.${link.direction}`, { person: who(person) });
|
|
193
|
+
if (await asks(ctx, treeLang, root, what, ui(treeLang, "ui.link.tasks"))) {
|
|
194
|
+
const { startDirection } = await import("../cli/menu-research.js");
|
|
195
|
+
await startDirection(ctx, run, treeLang, root, link.direction, ["--person", person.id], displayName(person));
|
|
196
|
+
}
|
|
197
|
+
return end(true);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* What needs no terminal: the research as it is now into the app's window (strom app), a conversation in the
|
|
203
|
+
* agent's desktop app (its first message waits there for the person to send it). Undefined: a terminal is needed.
|
|
204
|
+
*/
|
|
205
|
+
async function withoutTerminal(ctx, link, root) {
|
|
206
|
+
const run = await runnerOf(ctx)(root);
|
|
207
|
+
if (link.action === "app")
|
|
208
|
+
return { text: "", data: { done: (await run(["app"], true)) === 0, action: "app", tree: root } };
|
|
209
|
+
if (link.action !== "chat" && !(link.action === "conflict" && link.do === "agent"))
|
|
210
|
+
return undefined;
|
|
211
|
+
const tree = Tree.open(root, ctx.env);
|
|
212
|
+
if (whereToTalk(ctx.settings.agent(tree.config).value, ctx.settings.agentWhere(), ctx.env) !== "app")
|
|
213
|
+
return undefined;
|
|
214
|
+
let first;
|
|
215
|
+
if (link.action === "conflict") {
|
|
216
|
+
first = aboutConflict(root, ctx, link.id);
|
|
217
|
+
if (!first)
|
|
218
|
+
return say(ctx, "ui.link.conflict.none", { id: link.id }, { tree: link.tree });
|
|
219
|
+
}
|
|
220
|
+
else if (link.action === "chat" && link.person) {
|
|
221
|
+
const person = personOf(root, ctx, link.person);
|
|
222
|
+
if (!person)
|
|
223
|
+
return say(ctx, "ui.link.noperson", { person: link.person }, { tree: link.tree });
|
|
224
|
+
first = aboutPerson(tree.lang, person);
|
|
225
|
+
}
|
|
226
|
+
return { text: "", data: { done: (await run(["chat", ...(first ? [`--say=${first}`] : [])], true)) === 0, action: link.action, tree: root } };
|
|
227
|
+
}
|
|
228
|
+
/** An excerpt in full: the first research here that has it. */
|
|
229
|
+
function excerptOf(ctx, link) {
|
|
230
|
+
const shared = ctx.settings.shared()?.value;
|
|
231
|
+
const copies = researchesWithId(ctx, link.tree);
|
|
232
|
+
if (!copies.length || !shared)
|
|
233
|
+
return { missing: "tree" };
|
|
234
|
+
let found = { missing: "clip" };
|
|
235
|
+
for (const c of copies) {
|
|
236
|
+
const s = Tree.open(c.root, ctx.env).get(link.source);
|
|
237
|
+
if (!s || s.retracted)
|
|
238
|
+
continue;
|
|
239
|
+
found = fullExcerpt(Tree.open(c.root, ctx.env), shared, s, link.clip);
|
|
240
|
+
if (!("missing" in found))
|
|
241
|
+
return found;
|
|
242
|
+
}
|
|
243
|
+
return found;
|
|
244
|
+
}
|
|
245
|
+
register({
|
|
246
|
+
path: ["link", "open"],
|
|
247
|
+
summary: "Do what a strom-research:// link from the Strom app asks (the system runs it): send the edited tree back, an excerpt in full, …",
|
|
248
|
+
group: "setup",
|
|
249
|
+
description: "strom-research://send?tree=<research id> — the Strom app sends the edited tree to this research (a terminal opens,\n" +
|
|
250
|
+
"what it brings is shown and written on the user's word, as the menu's \"straight from the Strom app\").\n" +
|
|
251
|
+
"strom-research://excerpt?tree=<id>&source=S0042&clip=<its mark> — the excerpt from the scan, not made smaller, in the\n" +
|
|
252
|
+
"viewer of the system (the page in the online archive when the scan is not here). Nothing is written.\n" +
|
|
253
|
+
"strom-research://app?tree=<id> — the research as it is now into the app's open window (strom app).\n" +
|
|
254
|
+
"strom-research://open?tree=<id> — the research's menu in a terminal.\n" +
|
|
255
|
+
"strom-research://chat?tree=<id>[&person=P0012] — a conversation with the agent (strom chat; of one person: its first\n" +
|
|
256
|
+
"message names only who). strom-research://task?tree=<id>&task=T0007 — the task that waits for the user, answered.\n" +
|
|
257
|
+
"strom-research://review?tree=<id>&person=P0012&scope=person|family|line — the person reviewed (strom review).\n" +
|
|
258
|
+
"strom-research://research?tree=<id>&person=P0012&direction=ancestors|descendants — a new direction (research new).\n" +
|
|
259
|
+
"strom-research://task?…&task=T0007&do=park|drop|wake — the task put aside, given up, back in the queue (the reason asked).\n" +
|
|
260
|
+
"strom-research://conflict?tree=<id>&id=X0007&do=decide|agent — decided by the user (which claim, why) or left to the agent.\n" +
|
|
261
|
+
"strom-research://story?tree=<id>&person=P0012&do=final — the story approved (strom story approve).\n" +
|
|
262
|
+
"strom-research://sync-undo?tree=<id>&intake=I0042 — a sending from the app taken back (strom sync undo).\n" +
|
|
263
|
+
"strom-research://update|sessions|setup?tree=<id> — strom update, the agent's sessions and their cost, strom setup.\n" +
|
|
264
|
+
"strom-research://new?app=<the app's mark> — a tree of the app becomes a new research: named, made, the app hands the\n" +
|
|
265
|
+
"tree to its bridge (?adopt=, GET/POST /adopt), taken in as leads, sent back to the app.\n" +
|
|
266
|
+
"What writes tasks or starts the agent is said and asked in the terminal first. Any page can open such a link:\n" +
|
|
267
|
+
"each part is checked (only IDs and fixed values, never text), an unknown research, person, task or excerpt is\n" +
|
|
268
|
+
"only said. No link: the menu.",
|
|
269
|
+
args: [{ name: "link", description: "the strom-research:// link" }],
|
|
270
|
+
examples: ["strom link open 'strom-research://send?tree=0f8c2d4e-1b2a-4c3d-9e8f-7a6b5c4d3e2f'"],
|
|
271
|
+
async run(ctx, { args }) {
|
|
272
|
+
let link;
|
|
273
|
+
try {
|
|
274
|
+
link = parseLink(args[0]);
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
if (!(e instanceof LinkError))
|
|
278
|
+
throw e;
|
|
279
|
+
return say(ctx, "ui.link.bad", { why: e.message });
|
|
280
|
+
}
|
|
281
|
+
if (link.action === "menu") {
|
|
282
|
+
const where = ctx.settings.home()?.value ?? configDir(ctx.env);
|
|
283
|
+
fs.mkdirSync(where, { recursive: true });
|
|
284
|
+
openInNewTerminal([], where, ctx.env);
|
|
285
|
+
return { text: "", data: { done: true, action: "menu" } };
|
|
286
|
+
}
|
|
287
|
+
// a tree of the app becomes a research: always in a terminal (it asks the name, waits for the app)
|
|
288
|
+
if (link.action === "new") {
|
|
289
|
+
if (ctx.io.tty) {
|
|
290
|
+
const { newFromApp } = await import("../cli/menu-links.js");
|
|
291
|
+
const { openAppAt } = await import("./sync.js");
|
|
292
|
+
const done = await newFromApp(ctx, runnerOf(ctx), link.app, (url) => openAppAt(ctx, url), () => bringForward(ctx.env));
|
|
293
|
+
if (!done)
|
|
294
|
+
await pause(ctx, ctx.uiLang(), "ui.enter.close");
|
|
295
|
+
return { text: "", data: { done, action: "new" } };
|
|
296
|
+
}
|
|
297
|
+
const where = ctx.settings.home()?.value ?? configDir(ctx.env);
|
|
298
|
+
fs.mkdirSync(where, { recursive: true });
|
|
299
|
+
if (openInNewTerminal(["link", "open", linkText(link)], where, ctx.env) === false)
|
|
300
|
+
return say(ctx, "ui.link.noterminal.new");
|
|
301
|
+
return { text: "", data: { done: true, action: "new" } };
|
|
302
|
+
}
|
|
303
|
+
if (link.action !== "excerpt") {
|
|
304
|
+
// a terminal to show it in: this one, or a new window (the system started strom with none)
|
|
305
|
+
if (ctx.io.tty)
|
|
306
|
+
return inTerminal(ctx, link);
|
|
307
|
+
const copies = researchesWithId(ctx, link.tree);
|
|
308
|
+
if (!copies.length)
|
|
309
|
+
return say(ctx, "ui.link.notree", {}, { tree: link.tree });
|
|
310
|
+
const quiet = await withoutTerminal(ctx, link, copies[0].root);
|
|
311
|
+
if (quiet)
|
|
312
|
+
return quiet;
|
|
313
|
+
const opened = openInNewTerminal(["link", "open", linkText(link)], copies[0].root, ctx.env);
|
|
314
|
+
if (opened === false)
|
|
315
|
+
return link.action === "send"
|
|
316
|
+
? say(ctx, "ui.link.noterminal", { item: `${ui(ctx.uiLang(), "ui.more.title")} → ${ui(ctx.uiLang(), "ui.more.sync")}` })
|
|
317
|
+
: say(ctx, "ui.link.noterminal.any", { name: copies[0].name });
|
|
318
|
+
return { text: "", data: { done: true, action: link.action, tree: copies[0].root } };
|
|
319
|
+
}
|
|
320
|
+
const got = excerptOf(ctx, link);
|
|
321
|
+
if ("file" in got) {
|
|
322
|
+
openForUser(got.file, ctx.env);
|
|
323
|
+
return { text: ctx.display(got.file), data: { done: true, action: "excerpt", file: got.file } };
|
|
324
|
+
}
|
|
325
|
+
if ("url" in got) {
|
|
326
|
+
openForUser(got.url, ctx.env);
|
|
327
|
+
return { text: got.url, data: { done: true, action: "excerpt", url: got.url } };
|
|
328
|
+
}
|
|
329
|
+
return say(ctx, got.missing === "tree" ? "ui.link.notree" : got.missing === "scan" ? "ui.link.noscan" : "ui.link.noclip", { source: link.source }, { tree: link.tree, source: link.source, clip: link.clip });
|
|
330
|
+
},
|
|
331
|
+
}, {
|
|
332
|
+
path: ["link", "on"],
|
|
333
|
+
summary: "Let the Strom app start the research on this computer: register strom-research:// links for this user (the user's yes)",
|
|
334
|
+
group: "setup",
|
|
335
|
+
description: "The Strom app on this computer then sends the edited tree back with one click and opens an excerpt in full quality.\n" +
|
|
336
|
+
"For this user alone, no admin rights: macOS an applet in ~/Applications, Windows a key under HKCU, Linux a .desktop\n" +
|
|
337
|
+
"entry. The user's decision: an agent asks them (a window of the system). strom link off takes it away.",
|
|
338
|
+
run(ctx) {
|
|
339
|
+
const lang = ctx.uiLang();
|
|
340
|
+
if (linkHandlerState(ctx.env) === "ours")
|
|
341
|
+
return { text: ui(lang, "ui.link.on.done"), data: { on: true } };
|
|
342
|
+
ctx.requireHuman("Let the Strom app start strom on this computer (strom-research:// links)?", "strom link on", "links", ui(lang, "ui.setup.links"));
|
|
343
|
+
const on = registerLinks(ctx.env);
|
|
344
|
+
if (on)
|
|
345
|
+
keepAnswer(ctx, "yes");
|
|
346
|
+
return { text: ui(lang, on ? "ui.link.on.done" : "ui.link.on.failed"), data: { on }, ...(on ? {} : { exitCode: 1 }) };
|
|
347
|
+
},
|
|
348
|
+
}, {
|
|
349
|
+
path: ["link", "off"],
|
|
350
|
+
summary: "Take the strom-research:// links off this computer: the Strom app no longer starts the research",
|
|
351
|
+
group: "setup",
|
|
352
|
+
run(ctx) {
|
|
353
|
+
const off = unregisterLinks(ctx.env);
|
|
354
|
+
if (off)
|
|
355
|
+
keepAnswer(ctx, "no");
|
|
356
|
+
return { text: ui(ctx.uiLang(), off ? "ui.link.off.done" : "ui.link.off.failed"), data: { on: !off }, ...(off ? {} : { exitCode: 1 }) };
|
|
357
|
+
},
|
|
358
|
+
}, {
|
|
359
|
+
path: ["link", "status"],
|
|
360
|
+
summary: "Where strom-research:// links lead on this computer: to this strom, to another program, nowhere",
|
|
361
|
+
group: "setup",
|
|
362
|
+
run(ctx) {
|
|
363
|
+
const state = linkHandlerState(ctx.env);
|
|
364
|
+
const lang = ctx.uiLang();
|
|
365
|
+
return { text: lines(`${ui(lang, "ui.doc.links")}: ${ui(lang, `ui.doc.links.${state}`)}`, state === "ours" ? undefined : "→ strom link on"), data: { state } };
|
|
366
|
+
},
|
|
367
|
+
});
|
package/dist/commands/output.js
CHANGED
|
@@ -12,6 +12,8 @@ import { resolveResearch } from "./research.js";
|
|
|
12
12
|
import { UsageError } from "../core/errors.js";
|
|
13
13
|
import { excerptSettings, planExcerpts } from "../core/excerpt.js";
|
|
14
14
|
import { runGit, tracked } from "../core/git.js";
|
|
15
|
+
import { Settings } from "../core/config.js";
|
|
16
|
+
import { appOpensLinks } from "../core/stromapp.js";
|
|
15
17
|
import { EXCERPT_QUALITIES, EXCERPT_SCOPES } from "../core/model.js";
|
|
16
18
|
function findingRows(f) {
|
|
17
19
|
return table(f.slice(0, 30).map((x) => [x.level === "error" ? "ERROR" : "warn", `line ${x.line}`, x.message]));
|
|
@@ -67,7 +69,11 @@ export function writeTreeGed(tree, opts) {
|
|
|
67
69
|
const images = set && set.for !== "none"
|
|
68
70
|
? planExcerpts(tree, set.shared, { quality: set.quality, for: set.for, maxBytes: set.mb * 1024 * 1024, persons, budgetMs: opts.budgetMs })
|
|
69
71
|
: undefined;
|
|
70
|
-
|
|
72
|
+
// each excerpt's mark, for an app that asks for it in full (a strom-research:// link)
|
|
73
|
+
// an app that opens links shows what the research knows of a person too (the same version)
|
|
74
|
+
const research = opts.for === "strom" && appOpensLinks(new Settings(tree.env, {}), opts.stromVersion);
|
|
75
|
+
const clips = research && !!images;
|
|
76
|
+
const result = exportGedcom(tree, { for: opts.for, stromVersion: opts.stromVersion, ...(persons ? { persons } : {}), ...(images ? { excerpts: images.of } : {}), ...(clips ? { clips } : {}), ...(research ? { research } : {}) });
|
|
71
77
|
const findings = validateGedcom(result.text, opts.for === "standard" ? { strict: true } : {});
|
|
72
78
|
const file = opts.out ?? gedFile(tree, opts.for, opts.research);
|
|
73
79
|
if (!hasGedErrors(findings) && !tree.dryRun) {
|
package/dist/commands/people.js
CHANGED
|
@@ -9,6 +9,7 @@ import { INFORMATION } from "../core/evidence.js";
|
|
|
9
9
|
import { EVENT_KINDS, NAME_KINDS } from "../core/model.js";
|
|
10
10
|
import { ancestorGenerations, displayName, familiesAsChild, familiesAsPartner, findPersons, formatName, label, lifespan, relationTo, resolvePerson, } from "../core/people.js";
|
|
11
11
|
import { resolveResearch } from "./research.js";
|
|
12
|
+
import { DEATH_AFTER_YEARS, unprovenPeople } from "../core/review.js";
|
|
12
13
|
import { foldText } from "../core/text.js";
|
|
13
14
|
function list(v) {
|
|
14
15
|
if (v === undefined)
|
|
@@ -158,9 +159,10 @@ register({
|
|
|
158
159
|
options: [
|
|
159
160
|
{ name: "research", type: "string", value: "<G…>", description: "only people in scope of this research, with generations" },
|
|
160
161
|
{ name: "generation", type: "string", value: "<n>", description: "only this generation (with --research)" },
|
|
162
|
+
{ name: "unproven", type: "boolean", description: "only people no record of their own proves: how each stands, the records naming them, open tasks" },
|
|
161
163
|
{ name: "full", type: "boolean", description: "--json: whole records instead of one row each" },
|
|
162
164
|
],
|
|
163
|
-
examples: ["strom person list", "strom person list visek", "strom person list --research G0001 --generation 3"],
|
|
165
|
+
examples: ["strom person list", "strom person list visek", "strom person list --research G0001 --generation 3", "strom person list --unproven"],
|
|
164
166
|
run(ctx, { args, opts }) {
|
|
165
167
|
const tree = ctx.tree();
|
|
166
168
|
let people = tree.list("person").filter((p) => !p.retracted);
|
|
@@ -175,6 +177,8 @@ register({
|
|
|
175
177
|
people = people.filter((p) => gens.get(p.id) === Number(opts.generation));
|
|
176
178
|
people.sort((a, b) => gens.get(a.id) - gens.get(b.id) || a.id.localeCompare(b.id));
|
|
177
179
|
}
|
|
180
|
+
if (opts.unproven)
|
|
181
|
+
return unprovenList(ctx, tree, people, gens);
|
|
178
182
|
const page = paginate(people, ctx.limit, ctx.page);
|
|
179
183
|
const rows = page.items.map((p) => [
|
|
180
184
|
p.id,
|
|
@@ -662,3 +666,56 @@ register({
|
|
|
662
666
|
return { text: written(tree), data: { event: e } };
|
|
663
667
|
},
|
|
664
668
|
});
|
|
669
|
+
const UNPROVEN_KIND = { named: "named in a record", leads: "leads only", none: "no record" };
|
|
670
|
+
/** The people no record of their own proves: how each stands, the records that name them, the tasks already on them. */
|
|
671
|
+
function unprovenList(ctx, tree, people, gens) {
|
|
672
|
+
const among = new Set(people.map((p) => p.id));
|
|
673
|
+
const found = unprovenPeople(tree).filter((u) => among.has(u.person.id));
|
|
674
|
+
// the living come apart: the family's to tell, no research of the registers
|
|
675
|
+
const all = found.filter((u) => !u.living);
|
|
676
|
+
const living = found.filter((u) => u.living);
|
|
677
|
+
const open = tree.list("task").filter((t) => ["open", "doing", "parked", "waiting"].includes(t.state));
|
|
678
|
+
const tasksOf = (id) => open.filter((t) => t.subject.includes(id)).map((t) => t.id);
|
|
679
|
+
const counts = { named: 0, leads: 0, none: 0 };
|
|
680
|
+
for (const u of all)
|
|
681
|
+
counts[u.kind]++;
|
|
682
|
+
const page = paginate(all, ctx.limit, ctx.page);
|
|
683
|
+
const rows = page.items.map((u) => {
|
|
684
|
+
const tasks = tasksOf(u.person.id);
|
|
685
|
+
return [
|
|
686
|
+
u.person.id,
|
|
687
|
+
displayName(u.person),
|
|
688
|
+
lifespan(u.person),
|
|
689
|
+
gens ? `G${gens.get(u.person.id)}` : "",
|
|
690
|
+
UNPROVEN_KIND[u.kind] + (u.sources.length ? `: ${u.sources.slice(0, 3).join(" ")}${u.sources.length > 3 ? " …" : ""}` : ""),
|
|
691
|
+
u.kind === "none" && !u.person.notes.length ? "no note where from" : "",
|
|
692
|
+
tasks.length ? `tasks ${tasks.slice(0, 2).join(" ")}${tasks.length > 2 ? " …" : ""}` : "",
|
|
693
|
+
];
|
|
694
|
+
});
|
|
695
|
+
const livingLine = living.length
|
|
696
|
+
? `living, most likely (born less than ${DEATH_AFTER_YEARS} years ago, no death recorded) — not counted, no tasks: ${living.map((u) => `${u.person.id} ${displayName(u.person)}`).join(" · ")}`
|
|
697
|
+
: undefined;
|
|
698
|
+
const text = !all.length
|
|
699
|
+
? lines("everyone rests on a record of their own", livingLine)
|
|
700
|
+
: lines(`${all.length} without a record of their own — named in a record: ${counts.named} · leads only: ${counts.leads} · no record: ${counts.none}`, table(rows), moreLine(page, "strom person list --unproven"), livingLine, "", "tasks for them: strom review --unproven (a batch at a time) · strom review P… P… (these) · strom review P… (one)");
|
|
701
|
+
return {
|
|
702
|
+
text,
|
|
703
|
+
data: {
|
|
704
|
+
total: page.total,
|
|
705
|
+
page: page.page,
|
|
706
|
+
pages: page.pages,
|
|
707
|
+
counts,
|
|
708
|
+
living: living.map((u) => u.person.id),
|
|
709
|
+
persons: page.items.map((u) => ({
|
|
710
|
+
id: u.person.id,
|
|
711
|
+
name: displayName(u.person),
|
|
712
|
+
lifespan: lifespan(u.person),
|
|
713
|
+
kind: u.kind,
|
|
714
|
+
sources: u.sources,
|
|
715
|
+
notes: u.person.notes.length,
|
|
716
|
+
tasks: tasksOf(u.person.id),
|
|
717
|
+
...(gens ? { generation: gens.get(u.person.id) } : {}),
|
|
718
|
+
})),
|
|
719
|
+
},
|
|
720
|
+
};
|
|
721
|
+
}
|