@uehaj/sys1grep 0.5.0-next.1 → 0.5.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/settings.mjs ADDED
@@ -0,0 +1,58 @@
1
+ // ~/.config/sys1grep/settings.json: the defaults for sys1grep's environment variables, one field each. Every field is
2
+ // optional; a variable set in the environment wins over its field.
3
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname } from 'node:path';
6
+
7
+ export const SETTINGS_FILE = `${homedir()}/.config/sys1grep/settings.json`;
8
+ export const SETTINGS_SHOWN = '~/.config/sys1grep/settings.json';
9
+ // field: the variable it stands for (SYS1GREP_ + this). opts is an array, one argument each, not a string to split.
10
+ export const FIELDS = { url: 'URL', key: 'API_KEY', model: 'MODEL', opts: 'OPTS', summarizer: 'SUMMARIZER', summarizerModel: 'SUMMARIZER_MODEL', summarizerKey: 'SUMMARIZER_API_KEY', lang: 'LANG' };
11
+
12
+ // The checked settings, or null when there is no file. Throws an Error that names the field, never a value, for
13
+ // invalid JSON or a known field of the wrong type. An unknown field only warns (stderr) and is dropped: an older
14
+ // sys1grep must not break on a settings.json a newer one added a field to.
15
+ export const parseSettings = text => {
16
+ text = text.replace(/^\uFEFF/, ''); // Some editors prefix UTF-8 JSON with a BOM; an empty file means no defaults.
17
+ let s;
18
+ try { s = JSON.parse(text || '{}'); } catch { throw new Error('not valid JSON'); } // V8's message quotes the text, which may hold a key
19
+ if (s === null || typeof s !== 'object' || Array.isArray(s)) throw new Error('not a JSON object');
20
+ for (const [k, v] of Object.entries(s)) {
21
+ if (!Object.hasOwn(FIELDS, k)) {
22
+ console.error(`sys1grep: warning: ${SETTINGS_SHOWN}: unknown field ${k} (known: ${Object.keys(FIELDS).join(', ')}); ignored`);
23
+ delete s[k];
24
+ continue;
25
+ }
26
+ if (k === 'opts' ? !(Array.isArray(v) && v.every(a => typeof a === 'string')) : typeof v !== 'string')
27
+ throw new Error(`${k} must be ${k === 'opts' ? 'an array of strings' : 'a string'}`);
28
+ }
29
+ return s;
30
+ };
31
+ // Writes s after the same checks, as a whole new file (0600, its directory 0700) renamed over the old one, so a reader
32
+ // never sees half a file and the key is never readable by others, not even for a moment.
33
+ export const writeSettings = s => {
34
+ const text = `${JSON.stringify(parseSettings(JSON.stringify(s)), null, 2)}\n`, tmp = `${SETTINGS_FILE}.${process.pid}.tmp`;
35
+ mkdirSync(dirname(SETTINGS_FILE), { recursive: true, mode: 0o700 });
36
+ rmSync(tmp, { force: true });
37
+ writeFileSync(tmp, text, { mode: 0o600, flag: 'wx' });
38
+ renameSync(tmp, SETTINGS_FILE);
39
+ };
40
+ export const readSettings = () => {
41
+ let text;
42
+ try { text = readFileSync(SETTINGS_FILE, 'utf8'); } catch (e) { if (e.code === 'ENOENT') return null; throw e; }
43
+ return parseSettings(text);
44
+ };
45
+
46
+ // --sys1-api-key's value, wherever opts carries it: masked the same way for sys1grep.mjs's --verbose line and for
47
+ // the --serve settings panel's GET, so the two never drift apart.
48
+ export const maskOpts = opts => opts.map((tok, i, toks) => (toks[i - 1] === '--sys1-api-key' ? '***' : tok.replace(/^--sys1-api-key=.*$/, '--sys1-api-key=***')));
49
+ // A url's userinfo: the name and password a URL can carry before its "@", masked the same way, for the settings
50
+ // panel's GET.
51
+ export const maskUrl = u => {
52
+ try {
53
+ const x = new URL(u);
54
+ if (!x.username && !x.password) return u;
55
+ x.username = x.password = '***';
56
+ return x.toString();
57
+ } catch { return u; }
58
+ };