strom-research 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +142 -0
- package/assets/lang/cs.json +302 -0
- package/assets/lang/de.json +302 -0
- package/assets/method/core.md +43 -0
- package/assets/method/enrich.md +11 -0
- package/assets/method/intake.md +30 -0
- package/assets/method/link.md +28 -0
- package/assets/method/locate.md +28 -0
- package/assets/method/narrate.md +13 -0
- package/assets/method/reading.md +62 -0
- package/assets/method/recording.md +59 -0
- package/assets/method/request.md +10 -0
- package/assets/method/verify.md +17 -0
- package/assets/plugins/README.md +23 -0
- package/assets/plugins/connectors/DISCOVERY.md +159 -0
- package/assets/plugins/connectors/README.md +376 -0
- package/assets/plugins/connectors/sdk.ts +168 -0
- package/assets/plugins/connectors/template.ts +38 -0
- package/assets/plugins/gitignore +4 -0
- package/dist/agents/files.js +313 -0
- package/dist/agents/global.js +257 -0
- package/dist/agents/launch.js +36 -0
- package/dist/agents/profiles.js +95 -0
- package/dist/brief/brief.js +345 -0
- package/dist/cli/commit.js +44 -0
- package/dist/cli/context.js +311 -0
- package/dist/cli/execute.js +154 -0
- package/dist/cli/fixes.js +78 -0
- package/dist/cli/format.js +53 -0
- package/dist/cli/help.js +59 -0
- package/dist/cli/main.js +152 -0
- package/dist/cli/menu.js +212 -0
- package/dist/cli/registry.js +96 -0
- package/dist/cli/ui.js +266 -0
- package/dist/cli/wizard.js +142 -0
- package/dist/cli.js +14 -0
- package/dist/commands/analysis.js +622 -0
- package/dist/commands/batch.js +181 -0
- package/dist/commands/checks.js +153 -0
- package/dist/commands/connectors.js +1377 -0
- package/dist/commands/guide.js +160 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/intake.js +234 -0
- package/dist/commands/media.js +406 -0
- package/dist/commands/meta.js +195 -0
- package/dist/commands/output.js +117 -0
- package/dist/commands/people.js +664 -0
- package/dist/commands/read.js +199 -0
- package/dist/commands/research.js +139 -0
- package/dist/commands/session.js +605 -0
- package/dist/commands/setup.js +465 -0
- package/dist/commands/sources.js +634 -0
- package/dist/commands/start.js +383 -0
- package/dist/commands/story.js +75 -0
- package/dist/commands/tasks.js +436 -0
- package/dist/commands/trees.js +128 -0
- package/dist/core/actions.js +852 -0
- package/dist/core/age.js +95 -0
- package/dist/core/apps.js +74 -0
- package/dist/core/assets.js +34 -0
- package/dist/core/awake.js +33 -0
- package/dist/core/browser.js +281 -0
- package/dist/core/calibration.js +48 -0
- package/dist/core/check.js +112 -0
- package/dist/core/chromium.js +88 -0
- package/dist/core/config.js +348 -0
- package/dist/core/connector.js +811 -0
- package/dist/core/deps.js +73 -0
- package/dist/core/dialog.js +61 -0
- package/dist/core/errors.js +89 -0
- package/dist/core/evidence.js +58 -0
- package/dist/core/frontier.js +219 -0
- package/dist/core/gdate.js +77 -0
- package/dist/core/git.js +300 -0
- package/dist/core/guard.js +124 -0
- package/dist/core/http2.js +76 -0
- package/dist/core/import.js +541 -0
- package/dist/core/install.js +28 -0
- package/dist/core/integrity.js +219 -0
- package/dist/core/json.js +87 -0
- package/dist/core/lang.js +70 -0
- package/dist/core/live.js +244 -0
- package/dist/core/lock.js +112 -0
- package/dist/core/logins.js +67 -0
- package/dist/core/media.js +223 -0
- package/dist/core/model.js +101 -0
- package/dist/core/net.js +366 -0
- package/dist/core/open.js +29 -0
- package/dist/core/paths.js +84 -0
- package/dist/core/people.js +283 -0
- package/dist/core/phrases.js +85 -0
- package/dist/core/queue.js +113 -0
- package/dist/core/reader.js +76 -0
- package/dist/core/records.js +105 -0
- package/dist/core/roles.js +30 -0
- package/dist/core/schema.js +261 -0
- package/dist/core/seal.js +77 -0
- package/dist/core/self.js +40 -0
- package/dist/core/session.js +155 -0
- package/dist/core/shortcut.js +90 -0
- package/dist/core/stories.js +61 -0
- package/dist/core/stromapp.js +138 -0
- package/dist/core/text.js +104 -0
- package/dist/core/tree.js +507 -0
- package/dist/core/uninstall.js +128 -0
- package/dist/core/update.js +193 -0
- package/dist/core/validate.js +260 -0
- package/dist/core/views.js +164 -0
- package/dist/core/which.js +51 -0
- package/dist/core/workers.js +42 -0
- package/dist/gedcom/export.js +454 -0
- package/dist/gedcom/labels.js +103 -0
- package/dist/gedcom/lines.js +91 -0
- package/dist/gedcom/parse.js +53 -0
- package/dist/gedcom/validate.js +183 -0
- package/dist/image/image.js +223 -0
- package/dist/image/index.js +114 -0
- package/dist/image/jpeg-decode.js +552 -0
- package/dist/image/jpeg-encode.js +254 -0
- package/dist/image/png.js +241 -0
- package/dist/runners/antigravity.js +70 -0
- package/dist/runners/claude.js +179 -0
- package/dist/runners/codex.js +45 -0
- package/dist/runners/index.js +13 -0
- package/dist/runners/jsonl.js +86 -0
- package/dist/runners/opencode.js +50 -0
- package/dist/runners/runner.js +63 -0
- package/dist/runners/script.js +58 -0
- package/package.json +44 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
// source · cite · repo · recordset · place — where the evidence comes from.
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { commands, register } from "../cli/registry.js";
|
|
4
|
+
import { lines, moreLine, paginate, table, truncate } from "../cli/format.js";
|
|
5
|
+
import { UsageError } from "../core/errors.js";
|
|
6
|
+
import { ACCESS, AUTOMATION, JURISDICTIONS, SOURCE_KINDS, } from "../core/model.js";
|
|
7
|
+
import { create, csvOpt, listOpt, normId, requireRecord, textOpt, update } from "../core/records.js";
|
|
8
|
+
import { makeNote, parseDate } from "../core/actions.js";
|
|
9
|
+
import { calibrationLine, fitCalibration } from "../core/calibration.js";
|
|
10
|
+
import { foldText } from "../core/text.js";
|
|
11
|
+
import { imageOfRef } from "../core/media.js";
|
|
12
|
+
import { formatName } from "../core/people.js";
|
|
13
|
+
function written(tree) {
|
|
14
|
+
return lines(...tree.written.map((o) => o.summary), tree.dryRun ? "(dry run — nothing written)" : undefined);
|
|
15
|
+
}
|
|
16
|
+
function str(v) {
|
|
17
|
+
return typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
18
|
+
}
|
|
19
|
+
function oneOf(v, allowed, name, fallback) {
|
|
20
|
+
if (v === undefined)
|
|
21
|
+
return fallback;
|
|
22
|
+
if (!allowed.includes(v))
|
|
23
|
+
throw new UsageError(`invalid --${name} "${String(v)}"`, { hint: allowed.join(", ") });
|
|
24
|
+
return v;
|
|
25
|
+
}
|
|
26
|
+
/** Like oneOf, but nothing when the option was not given (for edits). */
|
|
27
|
+
function optOneOf(v, allowed, name) {
|
|
28
|
+
return v === undefined ? undefined : oneOf(v, allowed, name, allowed[0]);
|
|
29
|
+
}
|
|
30
|
+
/** "B0001:57" or "M0012" → a registered image. Only registered images can be cited. */
|
|
31
|
+
function mediaRefs(tree, refs) {
|
|
32
|
+
return refs.map((ref) => {
|
|
33
|
+
const at = /^([Bb]\d+):(\d+)$/.exec(ref);
|
|
34
|
+
if (!at)
|
|
35
|
+
return requireRecord(tree, ref, "media").id;
|
|
36
|
+
const b = requireRecord(tree, at[1], "recordset").id;
|
|
37
|
+
const m = imageOfRef(tree.list("media"), b, Number(at[2]));
|
|
38
|
+
if (!m)
|
|
39
|
+
throw new UsageError(`image ${at[2]} of ${b} is not registered`, { hint: `strom media add <file> --recordset ${b} --image ${at[2]}` });
|
|
40
|
+
return m.id;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** "Teinitz an der Elbe@de:1850-1918" → another name of a place. */
|
|
44
|
+
function altName(a) {
|
|
45
|
+
const m = /^(.+?)(?:@([a-z]{2,3}))?(?::(\d{3,4})?-(\d{3,4})?)?$/.exec(a);
|
|
46
|
+
if (!m)
|
|
47
|
+
throw new UsageError(`invalid --alt "${a}"`, { hint: 'e.g. "Teinitz an der Elbe@de:1850-1918"' });
|
|
48
|
+
const n = { name: m[1].trim() };
|
|
49
|
+
if (m[2])
|
|
50
|
+
n.lang = m[2];
|
|
51
|
+
if (m[3])
|
|
52
|
+
n.from = Number(m[3]);
|
|
53
|
+
if (m[4])
|
|
54
|
+
n.to = Number(m[4]);
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* An edit: only the options given change; --note adds a note. Overwriting a
|
|
59
|
+
* value an evidence field already had (what a record says, where it is) needs
|
|
60
|
+
* --reason; filling in an empty one does not.
|
|
61
|
+
*/
|
|
62
|
+
function editFields(tree, ref, type, fields,
|
|
63
|
+
/** Evidence fields, with the value that means "not known yet" (filling it in is no change). */
|
|
64
|
+
evidence, opts, merge = () => ({})) {
|
|
65
|
+
const id = requireRecord(tree, ref, type).id;
|
|
66
|
+
const given = Object.entries(fields).filter(([, v]) => v !== undefined && !(Array.isArray(v) && v.length === 0));
|
|
67
|
+
const note = str(opts.note);
|
|
68
|
+
const reason = str(opts.reason);
|
|
69
|
+
return update(tree, id, type, (cur) => {
|
|
70
|
+
const merged = merge(cur);
|
|
71
|
+
const changes = { ...Object.fromEntries(given), ...merged };
|
|
72
|
+
const keys = Object.keys(changes);
|
|
73
|
+
if (!keys.length && !note)
|
|
74
|
+
throw new UsageError("nothing to change", { hint: `strom help ${type === "repository" ? "repo" : type} edit` });
|
|
75
|
+
const old = cur;
|
|
76
|
+
const overwritten = keys.filter((k) => k in evidence && old[k] !== undefined && old[k] !== evidence[k] && JSON.stringify(old[k]) !== JSON.stringify(changes[k]));
|
|
77
|
+
if (overwritten.length && !reason)
|
|
78
|
+
throw new UsageError(`changing ${overwritten.join(", ")} of ${id} needs --reason`, { hint: 'what a record says is evidence: e.g. --reason "re-read at full resolution"' });
|
|
79
|
+
return { ...cur, ...changes, ...(note ? { notes: [...cur.notes, makeNote(tree, note)] } : {}) };
|
|
80
|
+
}, { op: `${type}.edit`, summary: `${id} ${[...given.map(([k]) => k), ...(note ? ["note"] : [])].join(", ") || "edited"}`, reason });
|
|
81
|
+
}
|
|
82
|
+
/** The options of an add command, for its edit: all optional, no defaults. */
|
|
83
|
+
function editOptions(options, extra = []) {
|
|
84
|
+
return [...extra, ...(options ?? []).map((o) => ({ ...o, description: o.description.replace(/\s*\(default[^)]*\)/, "") }))];
|
|
85
|
+
}
|
|
86
|
+
function readText(ctx) {
|
|
87
|
+
return (p) => fs.readFileSync(ctx.resolvePath(p), "utf8");
|
|
88
|
+
}
|
|
89
|
+
function matches(text, q) {
|
|
90
|
+
return !!text && foldText(text).includes(foldText(q));
|
|
91
|
+
}
|
|
92
|
+
// ── sources ────────────────────────────────────────────────────────────────
|
|
93
|
+
register({
|
|
94
|
+
path: ["source", "add"],
|
|
95
|
+
summary: "Add a source: one record, document or tree that says something",
|
|
96
|
+
group: "sources",
|
|
97
|
+
tree: true,
|
|
98
|
+
writes: true,
|
|
99
|
+
description: "An original register entry or certificate is primary evidence; family trees and memories are\n" +
|
|
100
|
+
"authored/derivative — facts citing only them stay leads. --transcript @file reads a file.",
|
|
101
|
+
args: [{ name: "title", description: 'short title: "Baptism of Jan Novák, Týnec 1905"', required: true }],
|
|
102
|
+
options: [
|
|
103
|
+
{ name: "kind", type: "string", value: "<kind>", description: `${SOURCE_KINDS.slice(0, 8).join(", ")}, … (default other)` },
|
|
104
|
+
{ name: "recordset", type: "string", value: "<B…>", description: "the book or collection it is in" },
|
|
105
|
+
{ name: "repo", type: "string", value: "<R…>", description: "archive holding it" },
|
|
106
|
+
{ name: "input", type: "string", value: "<I…>", description: "the input (document from the user) it comes from" },
|
|
107
|
+
{ name: "locator", type: "string", value: "<where>", description: "folio, page, entry number, image" },
|
|
108
|
+
{ name: "date", type: "string", value: "<date>", description: "when the record was made (GEDCOM date)" },
|
|
109
|
+
{ name: "transcript", type: "string", value: "<text|@file>", description: "word-for-word transcription, original language" },
|
|
110
|
+
{ name: "translation", type: "string", value: "<text|@file>", description: "translation into the research language" },
|
|
111
|
+
{ name: "language", type: "string", value: "<code>", description: "language of the record, e.g. la, de, cs" },
|
|
112
|
+
{ name: "information", type: "string", value: "<kind>", description: "primary (eyewitness), secondary, unknown (default)" },
|
|
113
|
+
{ name: "form", type: "string", value: "<form>", description: "original (default), derivative (copy, index — the default for --kind index and in a record set of kind index only), authored (compiled work)" },
|
|
114
|
+
{ name: "url", type: "string", value: "<url>", description: "link to the image or catalog entry" },
|
|
115
|
+
{ name: "media", type: "string", multiple: true, value: "<M…|B…:n>", description: "the registered image(s) the record is on (repeatable)" },
|
|
116
|
+
{ name: "note", type: "string", value: "<text>", description: "short note" },
|
|
117
|
+
],
|
|
118
|
+
examples: [
|
|
119
|
+
'strom source add "Křest Jana Nováka 1905" --kind baptism --recordset B0001 --locator "fol. 45, č. 12" --language la --information primary --transcript @zapis.txt',
|
|
120
|
+
'strom source add "Rodinný strom od tety Marie" --kind family-tree --form authored --input I0001',
|
|
121
|
+
],
|
|
122
|
+
run(ctx, { args, opts }) {
|
|
123
|
+
const tree = ctx.tree();
|
|
124
|
+
const book = opts.recordset ? requireRecord(tree, opts.recordset, "recordset") : undefined;
|
|
125
|
+
if (opts.repo)
|
|
126
|
+
requireRecord(tree, opts.repo, "repository");
|
|
127
|
+
if (opts.input)
|
|
128
|
+
requireRecord(tree, opts.input, "input");
|
|
129
|
+
const media = mediaRefs(tree, csvOpt(opts.media));
|
|
130
|
+
// a line of an index is a copy made by another hand; a book with its own index holds originals too
|
|
131
|
+
const kind = oneOf(opts.kind, SOURCE_KINDS, "kind", "other");
|
|
132
|
+
const form = kind === "index" || (book && book.kinds.length > 0 && book.kinds.every((k) => k === "index")) ? "derivative" : "original";
|
|
133
|
+
const src = create(tree, "source", {
|
|
134
|
+
kind,
|
|
135
|
+
title: args[0].trim(),
|
|
136
|
+
recordset: opts.recordset ? normId(opts.recordset) : undefined,
|
|
137
|
+
repository: opts.repo ? normId(opts.repo) : undefined,
|
|
138
|
+
input: opts.input ? normId(opts.input) : undefined,
|
|
139
|
+
locator: str(opts.locator),
|
|
140
|
+
date: parseDate(opts.date, "record date"),
|
|
141
|
+
transcript: textOpt(opts.transcript, readText(ctx)),
|
|
142
|
+
translation: textOpt(opts.translation, readText(ctx)),
|
|
143
|
+
language: str(opts.language)?.toLowerCase(),
|
|
144
|
+
information: oneOf(opts.information, ["primary", "secondary", "unknown"], "information", "unknown"),
|
|
145
|
+
form: oneOf(opts.form, ["original", "derivative", "authored"], "form", form),
|
|
146
|
+
url: str(opts.url),
|
|
147
|
+
accessed: str(opts.url) ? new Date().toISOString().slice(0, 10) : undefined,
|
|
148
|
+
...(media.length ? { media } : {}),
|
|
149
|
+
note: str(opts.note),
|
|
150
|
+
}, (id) => `+${id} source "${truncate(args[0], 60)}"${media.length ? ` on ${media.join(" ")}` : ""}`, media);
|
|
151
|
+
return { text: lines(written(tree), "", `cite it: strom cite <event> ${src.id} --locator "…"`), data: { source: src } };
|
|
152
|
+
},
|
|
153
|
+
}, {
|
|
154
|
+
path: ["source", "list"],
|
|
155
|
+
summary: "List sources (filter by text, kind, recordset)",
|
|
156
|
+
group: "sources",
|
|
157
|
+
tree: true,
|
|
158
|
+
args: [{ name: "filter", description: "text in title, locator or transcript" }],
|
|
159
|
+
options: [
|
|
160
|
+
{ name: "kind", type: "string", value: "<kind>", description: "only this kind" },
|
|
161
|
+
{ name: "recordset", type: "string", value: "<B…>", description: "only from this recordset" },
|
|
162
|
+
{ name: "full", type: "boolean", description: "--json: whole records instead of one row each" },
|
|
163
|
+
],
|
|
164
|
+
run(ctx, { args, opts }) {
|
|
165
|
+
const tree = ctx.tree();
|
|
166
|
+
let all = tree.list("source").filter((s) => !s.retracted);
|
|
167
|
+
if (args[0])
|
|
168
|
+
all = all.filter((s) => matches(s.title, args[0]) || matches(s.locator, args[0]) || matches(s.transcript, args[0]));
|
|
169
|
+
if (opts.kind)
|
|
170
|
+
all = all.filter((s) => s.kind === opts.kind);
|
|
171
|
+
if (opts.recordset)
|
|
172
|
+
all = all.filter((s) => s.recordset === normId(opts.recordset));
|
|
173
|
+
const page = paginate(all, ctx.limit, ctx.page);
|
|
174
|
+
const text = all.length
|
|
175
|
+
? lines(table(page.items.map((s) => [s.id, s.kind, truncate(s.title, 60), s.locator ?? "", s.form === "original" ? "" : s.form])), moreLine(page, "strom source list"))
|
|
176
|
+
: "no sources match";
|
|
177
|
+
const rows = opts.full ? page.items : page.items.map((s) => ({ id: s.id, kind: s.kind, title: s.title, locator: s.locator, form: s.form, information: s.information }));
|
|
178
|
+
return { text, data: { total: page.total, sources: rows } };
|
|
179
|
+
},
|
|
180
|
+
}, {
|
|
181
|
+
path: ["source", "show"],
|
|
182
|
+
summary: "One source with its transcript and every fact that cites it",
|
|
183
|
+
group: "sources",
|
|
184
|
+
tree: true,
|
|
185
|
+
args: [{ name: "source", description: "source ID (S0001)", required: true }],
|
|
186
|
+
run(ctx, { args }) {
|
|
187
|
+
const tree = ctx.tree();
|
|
188
|
+
const s = requireRecord(tree, args[0], "source");
|
|
189
|
+
const citing = [];
|
|
190
|
+
const at = (c) => (c.locator ? ` (${c.locator})` : "");
|
|
191
|
+
for (const p of tree.list("person")) {
|
|
192
|
+
for (const n of p.names)
|
|
193
|
+
for (const c of n.citations ?? [])
|
|
194
|
+
if (c.source === s.id)
|
|
195
|
+
citing.push(`${p.id} name ${formatName(n)}${at(c)}`);
|
|
196
|
+
for (const e of p.events)
|
|
197
|
+
for (const c of e.citations)
|
|
198
|
+
if (c.source === s.id)
|
|
199
|
+
citing.push(`${p.id} ${e.id} ${e.kind}${at(c)}`);
|
|
200
|
+
}
|
|
201
|
+
for (const f of tree.list("family")) {
|
|
202
|
+
for (const c of f.citations ?? [])
|
|
203
|
+
if (c.source === s.id)
|
|
204
|
+
citing.push(`${f.id} the family${at(c)}`);
|
|
205
|
+
for (const e of f.events)
|
|
206
|
+
for (const c of e.citations)
|
|
207
|
+
if (c.source === s.id)
|
|
208
|
+
citing.push(`${f.id} ${e.id} ${e.kind}${at(c)}`);
|
|
209
|
+
}
|
|
210
|
+
const text = lines(`${s.id} ${s.title}`, `kind ${s.kind} · ${s.form} · information ${s.information}${s.language ? ` · language ${s.language}` : ""}${s.date ? ` · dated ${s.date}` : ""}`, s.recordset || s.repository ? `in ${[s.recordset, s.repository].filter(Boolean).join(" · ")}${s.locator ? ` · ${s.locator}` : ""}` : s.locator ? `at ${s.locator}` : undefined, s.url ? `url ${s.url}` : undefined, s.media?.length ? `images ${s.media.map((id) => { const m = tree.get(id); return m ? `${id}${m.recordset ? ` (${m.recordset}:${m.image ?? "?"})` : ""}` : id; }).join(" · ")} — strom media view <M…>` : undefined, s.transcript ? `\ntranscript\n${s.transcript}` : undefined, s.translation ? `\ntranslation\n${s.translation}` : undefined, citing.length ? `\ncited by\n${citing.map((c) => ` ${c}`).join("\n")}` : "\ncited by nothing yet", ...s.notes.map((n) => `note ${n.text}`));
|
|
211
|
+
return { text, data: { source: s, citedBy: citing } };
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
// ── repositories ───────────────────────────────────────────────────────────
|
|
215
|
+
register({
|
|
216
|
+
path: ["repo", "add"],
|
|
217
|
+
summary: "Add an archive, library or portal that holds records",
|
|
218
|
+
group: "sources",
|
|
219
|
+
tree: true,
|
|
220
|
+
writes: true,
|
|
221
|
+
description: "--automation says whether tools may download from it: record what its terms of use allow.",
|
|
222
|
+
args: [{ name: "name", description: 'e.g. "SOA Praha (eBadatelna)"', required: true }],
|
|
223
|
+
options: [
|
|
224
|
+
{ name: "country", type: "string", value: "<code>", description: "ISO country code, e.g. CZ, DE, PL" },
|
|
225
|
+
{ name: "region", type: "string", value: "<text>", description: "region it covers" },
|
|
226
|
+
{ name: "url", type: "string", value: "<url>", description: "website or portal" },
|
|
227
|
+
{ name: "terms", type: "string", value: "<text>", description: "terms of use in one sentence (+ link)" },
|
|
228
|
+
{ name: "automation", type: "string", value: "<a>", description: "allowed, manual (browser only), forbidden, unknown (default)" },
|
|
229
|
+
{ name: "note", type: "string", value: "<text>", description: "short note" },
|
|
230
|
+
],
|
|
231
|
+
examples: ['strom repo add "SOA Praha (eBadatelna)" --country CZ --url https://ebadatelna.soapraha.cz --automation manual'],
|
|
232
|
+
run(ctx, { args, opts }) {
|
|
233
|
+
const tree = ctx.tree();
|
|
234
|
+
const r = create(tree, "repository", {
|
|
235
|
+
name: args[0].trim(),
|
|
236
|
+
country: str(opts.country)?.toUpperCase(),
|
|
237
|
+
region: str(opts.region),
|
|
238
|
+
url: str(opts.url),
|
|
239
|
+
terms: str(opts.terms),
|
|
240
|
+
automation: oneOf(opts.automation, AUTOMATION, "automation", "unknown"),
|
|
241
|
+
note: str(opts.note),
|
|
242
|
+
}, (id) => `+${id} repository "${args[0]}"`);
|
|
243
|
+
return { text: written(tree), data: { repository: r } };
|
|
244
|
+
},
|
|
245
|
+
}, {
|
|
246
|
+
path: ["repo", "list"],
|
|
247
|
+
summary: "Archives and portals known in this tree",
|
|
248
|
+
group: "sources",
|
|
249
|
+
tree: true,
|
|
250
|
+
run(ctx) {
|
|
251
|
+
const all = ctx.tree().list("repository");
|
|
252
|
+
return {
|
|
253
|
+
text: all.length ? table(all.map((r) => [r.id, r.name, r.country ?? "", r.automation, r.url ?? ""])) : 'none yet → strom repo add "<name>" --country <CC> --url <url>',
|
|
254
|
+
data: { repositories: all },
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
}, {
|
|
258
|
+
path: ["repo", "show"],
|
|
259
|
+
summary: "One archive: terms, record sets, lessons",
|
|
260
|
+
group: "sources",
|
|
261
|
+
tree: true,
|
|
262
|
+
args: [{ name: "repo", description: "repository ID (R0001)", required: true }],
|
|
263
|
+
run(ctx, { args }) {
|
|
264
|
+
const tree = ctx.tree();
|
|
265
|
+
const r = requireRecord(tree, args[0], "repository");
|
|
266
|
+
const sets = tree.list("recordset").filter((b) => b.repository === r.id);
|
|
267
|
+
const text = lines(`${r.id} ${r.name}${r.country ? ` (${r.country}${r.region ? `, ${r.region}` : ""})` : ""}`, r.url ? `url ${r.url}` : undefined, `automation ${r.automation}${r.automation === "forbidden" ? " — never download with tools" : r.automation === "manual" ? " — browser only" : ""}`, r.terms ? `terms ${r.terms}` : undefined, sets.length ? `\nrecord sets\n${table(sets.map((b) => [` ${b.id}`, b.title, b.years ?? "", b.access]))}` : undefined, ...r.notes.map((n) => `note ${n.text}`));
|
|
268
|
+
return { text, data: { repository: r, recordsets: sets } };
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
// ── record sets ────────────────────────────────────────────────────────────
|
|
272
|
+
register({
|
|
273
|
+
path: ["recordset", "add"],
|
|
274
|
+
summary: "Add a book, film or collection of records (e.g. a parish register volume)",
|
|
275
|
+
group: "sources",
|
|
276
|
+
tree: true,
|
|
277
|
+
writes: true,
|
|
278
|
+
args: [{ name: "title", description: 'e.g. "Týnec nad Labem 17, births 1861–1869"', required: true }],
|
|
279
|
+
options: [
|
|
280
|
+
{ name: "repo", type: "string", value: "<R…>", description: "archive holding it" },
|
|
281
|
+
{ name: "call-number", type: "string", value: "<sig>", description: "signature / call number" },
|
|
282
|
+
{ name: "kinds", type: "string", value: "<list>", description: "record kinds, comma separated: baptism,marriage,burial — with index when it is or has an index" },
|
|
283
|
+
{ name: "places", type: "string", value: "<list>", description: "places covered, comma separated" },
|
|
284
|
+
{ name: "years", type: "string", value: "<from-to>", description: "e.g. 1784-1820" },
|
|
285
|
+
{ name: "access", type: "string", value: "<a>", description: `${ACCESS.join(", ")} (default unknown)` },
|
|
286
|
+
{ name: "url", type: "string", value: "<url>", description: "link to the images or catalog" },
|
|
287
|
+
{ name: "images", type: "string", value: "<n>", description: "number of images" },
|
|
288
|
+
{ name: "layout", type: "string", value: "<text>", description: "how pages are laid out (columns, two years per page, …)" },
|
|
289
|
+
{ name: "note", type: "string", value: "<text>", description: "short note" },
|
|
290
|
+
],
|
|
291
|
+
examples: ['strom recordset add "Týnec nad Labem 17, N 1861-1869" --repo R0001 --kinds baptism --places "Týnec nad Labem,Bělušice" --years 1861-1869 --access online-free --url https://…'],
|
|
292
|
+
run(ctx, { args, opts }) {
|
|
293
|
+
const tree = ctx.tree();
|
|
294
|
+
if (opts.repo)
|
|
295
|
+
requireRecord(tree, opts.repo, "repository");
|
|
296
|
+
const years = str(opts.years);
|
|
297
|
+
if (years && !/^\d{3,4}(-\d{3,4})?$/.test(years))
|
|
298
|
+
throw new UsageError(`invalid --years "${years}"`, { hint: "use 1784 or 1784-1820" });
|
|
299
|
+
const images = opts.images === undefined ? undefined : Number(opts.images);
|
|
300
|
+
if (images !== undefined && (!Number.isInteger(images) || images < 1))
|
|
301
|
+
throw new UsageError("--images must be a positive number");
|
|
302
|
+
const b = create(tree, "recordset", {
|
|
303
|
+
title: args[0].trim(),
|
|
304
|
+
repository: opts.repo ? normId(opts.repo) : undefined,
|
|
305
|
+
callNumber: str(opts["call-number"]),
|
|
306
|
+
kinds: csvOpt(opts.kinds),
|
|
307
|
+
places: csvOpt(opts.places),
|
|
308
|
+
years,
|
|
309
|
+
access: oneOf(opts.access, ACCESS, "access", "unknown"),
|
|
310
|
+
url: str(opts.url),
|
|
311
|
+
images,
|
|
312
|
+
calibration: [],
|
|
313
|
+
layout: str(opts.layout),
|
|
314
|
+
note: str(opts.note),
|
|
315
|
+
}, (id) => `+${id} record set "${truncate(args[0], 60)}"`);
|
|
316
|
+
return { text: written(tree), data: { recordset: b } };
|
|
317
|
+
},
|
|
318
|
+
}, {
|
|
319
|
+
path: ["recordset", "list"],
|
|
320
|
+
summary: "Books and collections known in this tree",
|
|
321
|
+
group: "sources",
|
|
322
|
+
tree: true,
|
|
323
|
+
args: [{ name: "filter", description: "text in title, place or call number" }],
|
|
324
|
+
options: [{ name: "full", type: "boolean", description: "--json: whole records instead of one row each" }],
|
|
325
|
+
run(ctx, { args, opts }) {
|
|
326
|
+
const tree = ctx.tree();
|
|
327
|
+
let all = tree.list("recordset");
|
|
328
|
+
if (args[0])
|
|
329
|
+
all = all.filter((b) => matches(b.title, args[0]) || b.places.some((p) => matches(p, args[0])) || matches(b.callNumber, args[0]));
|
|
330
|
+
return {
|
|
331
|
+
text: all.length ? table(all.map((b) => [b.id, truncate(b.title, 60), b.years ?? "", b.access, b.calibration.length ? "calibrated" : ""])) : "no record sets match",
|
|
332
|
+
data: { recordsets: opts.full ? all : all.map((b) => ({ id: b.id, title: b.title, years: b.years, access: b.access, places: b.places, calibrated: b.calibration.length > 0 })) },
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
}, {
|
|
336
|
+
path: ["recordset", "show"],
|
|
337
|
+
summary: "One record set: access, calibration, what has been searched in it",
|
|
338
|
+
group: "sources",
|
|
339
|
+
tree: true,
|
|
340
|
+
args: [{ name: "recordset", description: "record set ID (B0001)", required: true }],
|
|
341
|
+
run(ctx, { args }) {
|
|
342
|
+
const tree = ctx.tree();
|
|
343
|
+
const b = requireRecord(tree, args[0], "recordset");
|
|
344
|
+
const repo = b.repository ? tree.get(b.repository) : undefined;
|
|
345
|
+
const text = lines(`${b.id} ${b.title}`, repo ? `in ${repo.id} ${repo.name}${b.callNumber ? ` · ${b.callNumber}` : ""} · automation ${repo.automation}` : b.callNumber ? `call number ${b.callNumber}` : undefined, `covers ${[b.kinds.join(", "), b.places.join(", "), b.years].filter(Boolean).join(" · ") || "(not described)"}`, `access ${b.access}${b.url ? ` · ${b.url}` : ""}${b.images ? ` · ${b.images} images` : ""}`, b.layout ? `layout ${b.layout}` : undefined, calibrationLine(b), ...b.notes.map((n) => `note ${n.text}`), "", `searched in it: strom searched ${b.id}`);
|
|
346
|
+
return { text, data: { recordset: b, calibration: fitCalibration(b.calibration) } };
|
|
347
|
+
},
|
|
348
|
+
}, {
|
|
349
|
+
path: ["recordset", "calibrate"],
|
|
350
|
+
summary: "Record which page/folio is on which image (measured, never guessed)",
|
|
351
|
+
group: "sources",
|
|
352
|
+
tree: true,
|
|
353
|
+
writes: true,
|
|
354
|
+
description: "Measure at least two distant images. strom fits page = a·image + b and says if the book is not linear.",
|
|
355
|
+
args: [{ name: "recordset", description: "record set ID (B0001)", required: true }],
|
|
356
|
+
options: [{ name: "point", type: "string", multiple: true, value: "<image=page>", description: "a measured point, e.g. 95=189 or 12=7r (repeatable)" }],
|
|
357
|
+
examples: ["strom recordset calibrate B0001 --point 95=189 --point 100=199"],
|
|
358
|
+
run(ctx, { args, opts }) {
|
|
359
|
+
const tree = ctx.tree();
|
|
360
|
+
const raw = listOpt(opts.point);
|
|
361
|
+
if (raw.length === 0)
|
|
362
|
+
throw new UsageError("give at least one --point image=page");
|
|
363
|
+
const points = raw.map((p) => {
|
|
364
|
+
const m = /^(\d+)\s*=\s*(.+)$/.exec(p);
|
|
365
|
+
if (!m)
|
|
366
|
+
throw new UsageError(`invalid --point "${p}"`, { hint: "use image=page, e.g. 95=189" });
|
|
367
|
+
return { image: Number(m[1]), page: m[2].trim() };
|
|
368
|
+
});
|
|
369
|
+
const id = normId(args[0], "recordset");
|
|
370
|
+
const b = update(tree, id, "recordset", (r) => {
|
|
371
|
+
const byImage = new Map(r.calibration.map((c) => [c.image, c]));
|
|
372
|
+
for (const p of points)
|
|
373
|
+
byImage.set(p.image, p);
|
|
374
|
+
return { ...r, calibration: [...byImage.values()].sort((x, y) => x.image - y.image) };
|
|
375
|
+
}, { op: "recordset.calibrate", summary: `${id} calibration ${points.map((p) => `${p.image}=${p.page}`).join(" ")}` });
|
|
376
|
+
return { text: lines(written(tree), calibrationLine(b)), data: { recordset: b, fit: fitCalibration(b.calibration) } };
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
// ── places ─────────────────────────────────────────────────────────────────
|
|
380
|
+
function placeName(p) {
|
|
381
|
+
return p.names[0]?.name ?? p.id;
|
|
382
|
+
}
|
|
383
|
+
export function findPlaces(tree, q) {
|
|
384
|
+
const key = foldText(q);
|
|
385
|
+
return tree.list("place").filter((p) => p.names.some((n) => foldText(n.name) === key || foldText(n.name).startsWith(key)));
|
|
386
|
+
}
|
|
387
|
+
register({
|
|
388
|
+
path: ["place", "add"],
|
|
389
|
+
summary: "Add a place with its names over time, coordinates and parent place",
|
|
390
|
+
group: "sources",
|
|
391
|
+
tree: true,
|
|
392
|
+
writes: true,
|
|
393
|
+
args: [{ name: "name", description: "current name", required: true }],
|
|
394
|
+
options: [
|
|
395
|
+
{ name: "alt", type: "string", multiple: true, value: "<name[@lang][:from-to]>", description: 'other name, e.g. "Teinitz an der Elbe@de:1850-1918" (repeatable)' },
|
|
396
|
+
{ name: "kind", type: "string", value: "<kind>", description: "village, town, parish, estate, district, …" },
|
|
397
|
+
{ name: "lat", type: "string", value: "<deg>", description: "latitude" },
|
|
398
|
+
{ name: "lon", type: "string", value: "<deg>", description: "longitude" },
|
|
399
|
+
{ name: "parent", type: "string", value: "<L…>", description: "place it belongs to" },
|
|
400
|
+
{ name: "note", type: "string", value: "<text>", description: "short note" },
|
|
401
|
+
],
|
|
402
|
+
examples: ['strom place add "Týnec nad Labem" --alt "Teinitz an der Elbe@de" --kind town --lat 50.042 --lon 15.358'],
|
|
403
|
+
run(ctx, { args, opts }) {
|
|
404
|
+
const tree = ctx.tree();
|
|
405
|
+
const names = [{ name: args[0].trim() }];
|
|
406
|
+
for (const a of listOpt(opts.alt))
|
|
407
|
+
names.push(altName(a));
|
|
408
|
+
const lat = opts.lat === undefined ? undefined : Number(opts.lat);
|
|
409
|
+
const lon = opts.lon === undefined ? undefined : Number(opts.lon);
|
|
410
|
+
if ((lat === undefined) !== (lon === undefined))
|
|
411
|
+
throw new UsageError("give both --lat and --lon");
|
|
412
|
+
if (opts.parent)
|
|
413
|
+
requireRecord(tree, opts.parent, "place");
|
|
414
|
+
const existing = findPlaces(tree, args[0]).filter((p) => foldText(placeName(p)) === foldText(args[0]));
|
|
415
|
+
const p = create(tree, "place", {
|
|
416
|
+
names,
|
|
417
|
+
kind: str(opts.kind),
|
|
418
|
+
coords: lat !== undefined && lon !== undefined ? { lat, lon } : undefined,
|
|
419
|
+
parent: opts.parent ? normId(opts.parent) : undefined,
|
|
420
|
+
jurisdictions: [],
|
|
421
|
+
note: str(opts.note),
|
|
422
|
+
}, (id) => `+${id} place "${args[0]}"`);
|
|
423
|
+
const warn = existing.length ? `note: a place with this name exists already: ${existing.map((e) => e.id).join(", ")}` : undefined;
|
|
424
|
+
return { text: lines(written(tree), warn), data: { place: p } };
|
|
425
|
+
},
|
|
426
|
+
}, {
|
|
427
|
+
path: ["place", "list"],
|
|
428
|
+
summary: "Places known in this tree",
|
|
429
|
+
group: "sources",
|
|
430
|
+
tree: true,
|
|
431
|
+
args: [{ name: "filter", description: "part of a name" }],
|
|
432
|
+
run(ctx, { args }) {
|
|
433
|
+
const tree = ctx.tree();
|
|
434
|
+
const all = args[0] ? tree.list("place").filter((p) => p.names.some((n) => matches(n.name, args[0]))) : tree.list("place");
|
|
435
|
+
return {
|
|
436
|
+
text: all.length
|
|
437
|
+
? table(all.map((p) => [p.id, placeName(p), p.names.slice(1).map((n) => n.name).join(", "), p.jurisdictions.map((j) => `${j.kind}: ${j.name}`).join("; ")]))
|
|
438
|
+
: "no places match",
|
|
439
|
+
data: { places: all },
|
|
440
|
+
};
|
|
441
|
+
},
|
|
442
|
+
}, {
|
|
443
|
+
path: ["place", "show"],
|
|
444
|
+
summary: "One place: names, jurisdictions over time, where its records are",
|
|
445
|
+
group: "sources",
|
|
446
|
+
tree: true,
|
|
447
|
+
args: [{ name: "place", description: "place ID (L0001) or name", required: true }],
|
|
448
|
+
run(ctx, { args }) {
|
|
449
|
+
const tree = ctx.tree();
|
|
450
|
+
const ref = args[0];
|
|
451
|
+
let p;
|
|
452
|
+
if (/^L\d+$/i.test(ref))
|
|
453
|
+
p = requireRecord(tree, ref, "place");
|
|
454
|
+
else {
|
|
455
|
+
const hits = findPlaces(tree, ref);
|
|
456
|
+
if (hits.length !== 1)
|
|
457
|
+
throw new UsageError(hits.length ? `"${ref}" matches ${hits.length} places` : `no place "${ref}"`, { hint: "strom place list" });
|
|
458
|
+
p = hits[0];
|
|
459
|
+
}
|
|
460
|
+
const span = (from, to) => (from || to ? ` (${from ?? ""}–${to ?? ""})` : "");
|
|
461
|
+
const text = lines(`${p.id} ${placeName(p)}${p.kind ? ` · ${p.kind}` : ""}${p.coords ? ` · ${p.coords.lat}, ${p.coords.lon}` : ""}`, p.names.length > 1 ? `names ${p.names.map((n) => `${n.name}${n.lang ? `@${n.lang}` : ""}${span(n.from, n.to)}`).join(" · ")}` : undefined, p.parent ? `in ${p.parent}` : undefined, p.jurisdictions.length
|
|
462
|
+
? "jurisdictions\n" +
|
|
463
|
+
table(p.jurisdictions.map((j) => [` ${j.kind}`, j.name + span(j.from, j.to), j.repository ?? "", (j.recordsets ?? []).join(" ")]))
|
|
464
|
+
: "jurisdictions (none yet) → strom place jurisdiction " + p.id + " --kind parish --name …", ...p.notes.map((n) => `note ${n.text}`));
|
|
465
|
+
return { text, data: { place: p } };
|
|
466
|
+
},
|
|
467
|
+
}, {
|
|
468
|
+
path: ["place", "jurisdiction"],
|
|
469
|
+
summary: "Record which parish, civil office, estate … a place belonged to, and when",
|
|
470
|
+
group: "sources",
|
|
471
|
+
tree: true,
|
|
472
|
+
writes: true,
|
|
473
|
+
description: "This is how strom knows where the records of a place are for a given year.",
|
|
474
|
+
args: [{ name: "place", description: "place ID (L0001)", required: true }],
|
|
475
|
+
options: [
|
|
476
|
+
{ name: "kind", type: "string", value: "<kind>", description: JURISDICTIONS.join(", ") },
|
|
477
|
+
{ name: "name", type: "string", value: "<name>", description: "e.g. the parish name" },
|
|
478
|
+
{ name: "from", type: "string", value: "<year>", description: "since" },
|
|
479
|
+
{ name: "to", type: "string", value: "<year>", description: "until" },
|
|
480
|
+
{ name: "repo", type: "string", value: "<R…>", description: "archive holding its records" },
|
|
481
|
+
{ name: "recordset", type: "string", multiple: true, value: "<B…>", description: "its record sets (repeatable)" },
|
|
482
|
+
],
|
|
483
|
+
examples: ["strom place jurisdiction L0001 --kind parish --name \"Týnec nad Labem\" --from 1784 --repo R0001 --recordset B0001"],
|
|
484
|
+
run(ctx, { args, opts }) {
|
|
485
|
+
const tree = ctx.tree();
|
|
486
|
+
if (!opts.kind || !opts.name)
|
|
487
|
+
throw new UsageError("--kind and --name are required");
|
|
488
|
+
const kind = oneOf(opts.kind, JURISDICTIONS, "kind", "other");
|
|
489
|
+
const from = opts.from === undefined ? undefined : Number(opts.from);
|
|
490
|
+
const to = opts.to === undefined ? undefined : Number(opts.to);
|
|
491
|
+
if (opts.repo)
|
|
492
|
+
requireRecord(tree, opts.repo, "repository");
|
|
493
|
+
const recordsets = csvOpt(opts.recordset).map((b) => requireRecord(tree, b, "recordset").id);
|
|
494
|
+
const id = normId(args[0], "place");
|
|
495
|
+
const p = update(tree, id, "place", (pl) => {
|
|
496
|
+
const j = { kind, name: String(opts.name).trim() };
|
|
497
|
+
if (from)
|
|
498
|
+
j.from = from;
|
|
499
|
+
if (to)
|
|
500
|
+
j.to = to;
|
|
501
|
+
if (opts.repo)
|
|
502
|
+
j.repository = normId(opts.repo);
|
|
503
|
+
j.recordsets = recordsets; // always a list, even an empty one: the record is validated so
|
|
504
|
+
return { ...pl, jurisdictions: [...pl.jurisdictions, j] };
|
|
505
|
+
}, { op: "place.jurisdiction", summary: `${id} ${kind} ${opts.name}` });
|
|
506
|
+
return { text: written(tree), data: { place: p } };
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
// ── edits: the options of each add, all optional ──────────────────────────
|
|
510
|
+
function addOptions(path) {
|
|
511
|
+
return commands().find((d) => d.path.join(" ") === path)?.options;
|
|
512
|
+
}
|
|
513
|
+
register({
|
|
514
|
+
path: ["source", "edit"],
|
|
515
|
+
summary: "Correct or complete a source: transcript, translation, evidence quality, images, where it is",
|
|
516
|
+
group: "sources",
|
|
517
|
+
tree: true,
|
|
518
|
+
writes: true,
|
|
519
|
+
description: "Only the options given change; --media adds images, --note adds a note. Changing what a record says or\n" +
|
|
520
|
+
"how good it is (kind, form, information, date, locator, transcript, record set) needs --reason; filling in doesn't.",
|
|
521
|
+
args: [{ name: "source", description: "source ID (S0001)", required: true }],
|
|
522
|
+
options: editOptions(addOptions("source add"), [{ name: "title", type: "string", value: "<text>", description: "a better title" }]).filter((o) => o.name !== "input"),
|
|
523
|
+
examples: ['strom source edit S0001 --translation "Jan, son of Josef" --media B0001:2', 'strom source edit S0001 --information secondary --reason "the entry was written years later"'],
|
|
524
|
+
run(ctx, { args, opts }) {
|
|
525
|
+
const tree = ctx.tree();
|
|
526
|
+
if (opts.recordset)
|
|
527
|
+
requireRecord(tree, opts.recordset, "recordset");
|
|
528
|
+
if (opts.repo)
|
|
529
|
+
requireRecord(tree, opts.repo, "repository");
|
|
530
|
+
const media = mediaRefs(tree, csvOpt(opts.media));
|
|
531
|
+
const s = editFields(tree, args[0], "source", {
|
|
532
|
+
title: str(opts.title),
|
|
533
|
+
kind: optOneOf(opts.kind, SOURCE_KINDS, "kind"),
|
|
534
|
+
recordset: opts.recordset ? normId(opts.recordset) : undefined,
|
|
535
|
+
repository: opts.repo ? normId(opts.repo) : undefined,
|
|
536
|
+
locator: str(opts.locator),
|
|
537
|
+
date: opts.date === undefined ? undefined : parseDate(opts.date, "record date"),
|
|
538
|
+
transcript: textOpt(opts.transcript, readText(ctx)),
|
|
539
|
+
translation: textOpt(opts.translation, readText(ctx)),
|
|
540
|
+
language: str(opts.language)?.toLowerCase(),
|
|
541
|
+
information: optOneOf(opts.information, ["primary", "secondary", "unknown"], "information"),
|
|
542
|
+
form: optOneOf(opts.form, ["original", "derivative", "authored"], "form"),
|
|
543
|
+
url: str(opts.url),
|
|
544
|
+
accessed: str(opts.url) ? new Date().toISOString().slice(0, 10) : undefined,
|
|
545
|
+
}, { kind: "other", form: undefined, information: "unknown", date: undefined, locator: undefined, transcript: undefined, recordset: undefined }, opts, (cur) => (media.some((m) => !(cur.media ?? []).includes(m)) ? { media: [...new Set([...(cur.media ?? []), ...media])] } : {}));
|
|
546
|
+
return { text: written(tree), data: { source: s } };
|
|
547
|
+
},
|
|
548
|
+
}, {
|
|
549
|
+
path: ["repo", "edit"],
|
|
550
|
+
summary: "Correct or complete an archive: its terms of use, whether tools may download, its site",
|
|
551
|
+
group: "sources",
|
|
552
|
+
tree: true,
|
|
553
|
+
writes: true,
|
|
554
|
+
args: [{ name: "repo", description: "archive ID (R0001)", required: true }],
|
|
555
|
+
options: editOptions(addOptions("repo add"), [{ name: "name", type: "string", value: "<text>", description: "its name" }]),
|
|
556
|
+
examples: ['strom repo edit R0001 --automation forbidden --terms "no automated downloads (https://…/terms)"'],
|
|
557
|
+
run(ctx, { args, opts }) {
|
|
558
|
+
const tree = ctx.tree();
|
|
559
|
+
const r = editFields(tree, args[0], "repository", {
|
|
560
|
+
name: str(opts.name),
|
|
561
|
+
country: str(opts.country)?.toUpperCase(),
|
|
562
|
+
region: str(opts.region),
|
|
563
|
+
url: str(opts.url),
|
|
564
|
+
terms: str(opts.terms),
|
|
565
|
+
automation: optOneOf(opts.automation, AUTOMATION, "automation"),
|
|
566
|
+
}, {}, opts);
|
|
567
|
+
return { text: written(tree), data: { repository: r } };
|
|
568
|
+
},
|
|
569
|
+
}, {
|
|
570
|
+
path: ["recordset", "edit"],
|
|
571
|
+
summary: "Correct or complete a record set: what it covers (kinds, places, years), access, layout",
|
|
572
|
+
group: "sources",
|
|
573
|
+
tree: true,
|
|
574
|
+
writes: true,
|
|
575
|
+
description: "--kinds and --places replace the lists (comma separated).",
|
|
576
|
+
args: [{ name: "recordset", description: "record set ID (B0001)", required: true }],
|
|
577
|
+
options: editOptions(addOptions("recordset add"), [{ name: "title", type: "string", value: "<text>", description: "its title" }]),
|
|
578
|
+
examples: ['strom recordset edit B0001 --places "Týnec nad Labem,Bělušice,Lžovice" --layout "two pages per image; baptisms left, burials right"'],
|
|
579
|
+
run(ctx, { args, opts }) {
|
|
580
|
+
const tree = ctx.tree();
|
|
581
|
+
if (opts.repo)
|
|
582
|
+
requireRecord(tree, opts.repo, "repository");
|
|
583
|
+
const years = str(opts.years);
|
|
584
|
+
if (years && !/^\d{3,4}(-\d{3,4})?$/.test(years))
|
|
585
|
+
throw new UsageError(`invalid --years "${years}"`, { hint: "use 1784 or 1784-1820" });
|
|
586
|
+
const images = opts.images === undefined ? undefined : Number(opts.images);
|
|
587
|
+
if (images !== undefined && (!Number.isInteger(images) || images < 1))
|
|
588
|
+
throw new UsageError("--images must be a positive number");
|
|
589
|
+
const b = editFields(tree, args[0], "recordset", {
|
|
590
|
+
title: str(opts.title),
|
|
591
|
+
repository: opts.repo ? normId(opts.repo) : undefined,
|
|
592
|
+
callNumber: str(opts["call-number"]),
|
|
593
|
+
kinds: csvOpt(opts.kinds),
|
|
594
|
+
places: csvOpt(opts.places),
|
|
595
|
+
years,
|
|
596
|
+
access: optOneOf(opts.access, ACCESS, "access"),
|
|
597
|
+
url: str(opts.url),
|
|
598
|
+
images,
|
|
599
|
+
layout: str(opts.layout),
|
|
600
|
+
}, {}, opts);
|
|
601
|
+
return { text: written(tree), data: { recordset: b } };
|
|
602
|
+
},
|
|
603
|
+
}, {
|
|
604
|
+
path: ["place", "edit"],
|
|
605
|
+
summary: "Complete a place: other names, kind, coordinates, the place it belongs to",
|
|
606
|
+
group: "sources",
|
|
607
|
+
tree: true,
|
|
608
|
+
writes: true,
|
|
609
|
+
description: "--alt adds names; a place's names are never removed.",
|
|
610
|
+
args: [{ name: "place", description: "place ID (L0001)", required: true }],
|
|
611
|
+
options: editOptions(addOptions("place add")),
|
|
612
|
+
examples: ['strom place edit L0001 --alt "Teinitz an der Elbe@de:1850-1918" --lat 50.042 --lon 15.358'],
|
|
613
|
+
run(ctx, { args, opts }) {
|
|
614
|
+
const tree = ctx.tree();
|
|
615
|
+
const lat = opts.lat === undefined ? undefined : Number(opts.lat);
|
|
616
|
+
const lon = opts.lon === undefined ? undefined : Number(opts.lon);
|
|
617
|
+
if ((lat === undefined) !== (lon === undefined))
|
|
618
|
+
throw new UsageError("give both --lat and --lon");
|
|
619
|
+
if ((lat !== undefined && !Number.isFinite(lat)) || (lon !== undefined && !Number.isFinite(lon)))
|
|
620
|
+
throw new UsageError("--lat and --lon are numbers, e.g. 50.042");
|
|
621
|
+
if (opts.parent)
|
|
622
|
+
requireRecord(tree, opts.parent, "place");
|
|
623
|
+
const alt = listOpt(opts.alt).map(altName);
|
|
624
|
+
const p = editFields(tree, args[0], "place", {
|
|
625
|
+
kind: str(opts.kind),
|
|
626
|
+
coords: lat !== undefined && lon !== undefined ? { lat, lon } : undefined,
|
|
627
|
+
parent: opts.parent ? normId(opts.parent) : undefined,
|
|
628
|
+
}, {}, opts, (cur) => {
|
|
629
|
+
const add = alt.filter((n) => !cur.names.some((x) => foldText(x.name) === foldText(n.name) && x.lang === n.lang));
|
|
630
|
+
return add.length ? { names: [...cur.names, ...add] } : {};
|
|
631
|
+
});
|
|
632
|
+
return { text: written(tree), data: { place: p } };
|
|
633
|
+
},
|
|
634
|
+
});
|