@trazum/cli 1.40.0 → 1.42.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/README.md +2 -0
- package/dist/connect.d.ts +72 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +204 -0
- package/dist/connect.js.map +1 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +99 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +101 -1
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/types.d.ts +61 -1
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/index.js +379 -72
- package/dist/index.js.map +1 -1
- package/dist/store-fs.d.ts +59 -0
- package/dist/store-fs.d.ts.map +1 -0
- package/dist/store-fs.js +145 -0
- package/dist/store-fs.js.map +1 -0
- package/package.json +2 -2
- package/src/connect.ts +245 -0
- package/src/i18n/en.ts +125 -1
- package/src/i18n/es.ts +127 -1
- package/src/i18n/types.ts +63 -1
- package/src/index.ts +503 -67
- package/src/store-fs.ts +157 -0
package/src/store-fs.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the store actually lives.
|
|
3
|
+
*
|
|
4
|
+
* The core decides what a record is and when two are the same; this decides
|
|
5
|
+
* where the bytes go. Split that way for the reason every module here is:
|
|
6
|
+
* `@trazum/core` stays browser-safe, and the CLI keeps its monopoly on I/O.
|
|
7
|
+
*
|
|
8
|
+
* **Append-only, one buffer per write.** A pull appends a single block and
|
|
9
|
+
* never rewrites what is already there. Two consequences worth stating: a
|
|
10
|
+
* crash during a write loses the tail of one block rather than a year of
|
|
11
|
+
* measurements, and two runs writing at once interleave whole blocks rather
|
|
12
|
+
* than half-lines. Compaction is a separate, explicit errand — `store
|
|
13
|
+
* --prune` — because collapsing a log is the one operation that destroys
|
|
14
|
+
* something, and it should never happen as a side effect of a pull.
|
|
15
|
+
*
|
|
16
|
+
* **A line that will not parse is kept, counted and skipped.** The store is a
|
|
17
|
+
* file a human may open, a backup may truncate and a merge may mangle. Losing
|
|
18
|
+
* the whole month because one line is broken would be the worst possible
|
|
19
|
+
* response; so would silently pretending the month is complete.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
import { resolveStore } from '@trazum/core';
|
|
25
|
+
import type { ResolvedStore, StoreRecord } from '@trazum/core';
|
|
26
|
+
|
|
27
|
+
/** The directory name, relative to wherever the caller roots the store. */
|
|
28
|
+
export const STORE_DIR = '.trazum/store';
|
|
29
|
+
|
|
30
|
+
/** Records are filed by the UTC month their window starts in. */
|
|
31
|
+
function monthOf(record: StoreRecord): string {
|
|
32
|
+
return new Date(record.fromMs).toISOString().slice(0, 7);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StoreReadResult {
|
|
36
|
+
resolved: ResolvedStore;
|
|
37
|
+
/** Lines that would not parse: counted and named by file, never dropped quietly. */
|
|
38
|
+
unreadable: { file: string; line: number }[];
|
|
39
|
+
/** Files read, so an empty store can be told from an unread one. */
|
|
40
|
+
files: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads every record in the store.
|
|
45
|
+
*
|
|
46
|
+
* Returns an empty result rather than throwing when the store does not exist:
|
|
47
|
+
* "you have not stored anything yet" is a state, not an error, and the caller
|
|
48
|
+
* says so in a sentence that names `trazum connect`.
|
|
49
|
+
*/
|
|
50
|
+
export async function readStore(root: string): Promise<StoreReadResult> {
|
|
51
|
+
const dir = join(root, STORE_DIR);
|
|
52
|
+
const records: StoreRecord[] = [];
|
|
53
|
+
const unreadable: { file: string; line: number }[] = [];
|
|
54
|
+
const files: string[] = [];
|
|
55
|
+
|
|
56
|
+
let providers: string[];
|
|
57
|
+
try {
|
|
58
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
59
|
+
providers = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
60
|
+
} catch {
|
|
61
|
+
return { resolved: resolveStore([]), unreadable, files };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const provider of providers.sort()) {
|
|
65
|
+
const providerDir = join(dir, provider);
|
|
66
|
+
let months: string[];
|
|
67
|
+
try {
|
|
68
|
+
months = (await readdir(providerDir)).filter((name) => name.endsWith('.jsonl')).sort();
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (const month of months) {
|
|
73
|
+
const path = join(providerDir, month);
|
|
74
|
+
files.push(join(STORE_DIR, provider, month));
|
|
75
|
+
const text = await readFile(path, 'utf8');
|
|
76
|
+
for (const [index, line] of text.split('\n').entries()) {
|
|
77
|
+
if (line.trim() === '') continue;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(line) as StoreRecord;
|
|
80
|
+
if (typeof parsed?.provider === 'string' && typeof parsed?.fromMs === 'number') {
|
|
81
|
+
records.push(parsed);
|
|
82
|
+
} else {
|
|
83
|
+
unreadable.push({ file: join(STORE_DIR, provider, month), line: index + 1 });
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
unreadable.push({ file: join(STORE_DIR, provider, month), line: index + 1 });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { resolved: resolveStore(records), unreadable, files };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Appends records, grouped into one write per month file.
|
|
97
|
+
*
|
|
98
|
+
* Nothing already on disk is read, rewritten or resolved here: convergence
|
|
99
|
+
* happens when the store is *read*, which is what keeps a write cheap enough
|
|
100
|
+
* to run on a schedule and impossible to corrupt by racing.
|
|
101
|
+
*/
|
|
102
|
+
export async function appendRecords(root: string, records: readonly StoreRecord[]): Promise<number> {
|
|
103
|
+
if (records.length === 0) return 0;
|
|
104
|
+
const byFile = new Map<string, StoreRecord[]>();
|
|
105
|
+
for (const record of records) {
|
|
106
|
+
const key = join(record.provider, `${monthOf(record)}.jsonl`);
|
|
107
|
+
const list = byFile.get(key) ?? [];
|
|
108
|
+
list.push(record);
|
|
109
|
+
byFile.set(key, list);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const [relative, list] of byFile) {
|
|
113
|
+
const path = join(root, STORE_DIR, relative);
|
|
114
|
+
await mkdir(join(path, '..'), { recursive: true });
|
|
115
|
+
const block = `${list.map((record) => JSON.stringify(record)).join('\n')}\n`;
|
|
116
|
+
await writeFile(path, block, { flag: 'a', mode: 0o600 });
|
|
117
|
+
}
|
|
118
|
+
return records.length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Rewrites the store with exactly the records given.
|
|
123
|
+
*
|
|
124
|
+
* The one operation that destroys something, so it is only ever reached from
|
|
125
|
+
* an explicit `--prune`. Each month file is written whole, and a month left
|
|
126
|
+
* with nothing is written empty rather than removed — a missing file and an
|
|
127
|
+
* empty one say different things to whoever looks next.
|
|
128
|
+
*/
|
|
129
|
+
export async function rewriteStore(root: string, records: readonly StoreRecord[]): Promise<void> {
|
|
130
|
+
const dir = join(root, STORE_DIR);
|
|
131
|
+
const existing = new Set<string>();
|
|
132
|
+
try {
|
|
133
|
+
for (const provider of await readdir(dir)) {
|
|
134
|
+
for (const month of await readdir(join(dir, provider)).catch(() => [])) {
|
|
135
|
+
if (month.endsWith('.jsonl')) existing.add(join(provider, month));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Nothing stored yet: the writes below create what is needed.
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const byFile = new Map<string, StoreRecord[]>();
|
|
143
|
+
for (const record of records) {
|
|
144
|
+
const key = join(record.provider, `${monthOf(record)}.jsonl`);
|
|
145
|
+
const list = byFile.get(key) ?? [];
|
|
146
|
+
list.push(record);
|
|
147
|
+
byFile.set(key, list);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const relative of new Set([...existing, ...byFile.keys()])) {
|
|
151
|
+
const list = byFile.get(relative) ?? [];
|
|
152
|
+
const path = join(dir, relative);
|
|
153
|
+
await mkdir(join(path, '..'), { recursive: true });
|
|
154
|
+
const block = list.length === 0 ? '' : `${list.map((r) => JSON.stringify(r)).join('\n')}\n`;
|
|
155
|
+
await writeFile(path, block, { mode: 0o600 });
|
|
156
|
+
}
|
|
157
|
+
}
|