@guckdev/core 0.1.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/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +178 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/redact.d.ts +3 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +68 -0
- package/dist/redact.js.map +1 -0
- package/dist/schema.d.ts +113 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -0
- package/dist/store/backends/cloudwatch.d.ts +4 -0
- package/dist/store/backends/cloudwatch.d.ts.map +1 -0
- package/dist/store/backends/cloudwatch.js +302 -0
- package/dist/store/backends/cloudwatch.js.map +1 -0
- package/dist/store/backends/k8s.d.ts +4 -0
- package/dist/store/backends/k8s.d.ts.map +1 -0
- package/dist/store/backends/k8s.js +307 -0
- package/dist/store/backends/k8s.js.map +1 -0
- package/dist/store/backends/local.d.ts +10 -0
- package/dist/store/backends/local.d.ts.map +1 -0
- package/dist/store/backends/local.js +31 -0
- package/dist/store/backends/local.js.map +1 -0
- package/dist/store/backends/types.d.ts +25 -0
- package/dist/store/backends/types.d.ts.map +1 -0
- package/dist/store/backends/types.js +2 -0
- package/dist/store/backends/types.js.map +1 -0
- package/dist/store/file-store.d.ts +21 -0
- package/dist/store/file-store.d.ts.map +1 -0
- package/dist/store/file-store.js +169 -0
- package/dist/store/file-store.js.map +1 -0
- package/dist/store/filters.d.ts +4 -0
- package/dist/store/filters.d.ts.map +1 -0
- package/dist/store/filters.js +73 -0
- package/dist/store/filters.js.map +1 -0
- package/dist/store/read-store.d.ts +35 -0
- package/dist/store/read-store.d.ts.map +1 -0
- package/dist/store/read-store.js +256 -0
- package/dist/store/read-store.js.map +1 -0
- package/dist/store/time.d.ts +4 -0
- package/dist/store/time.d.ts.map +1 -0
- package/dist/store/time.js +47 -0
- package/dist/store/time.js.map +1 -0
- package/package.json +38 -0
- package/src/config.ts +210 -0
- package/src/index.ts +6 -0
- package/src/redact.ts +83 -0
- package/src/schema.ts +130 -0
- package/src/store/backends/cloudwatch.ts +373 -0
- package/src/store/backends/k8s.ts +400 -0
- package/src/store/backends/local.ts +47 -0
- package/src/store/backends/types.ts +18 -0
- package/src/store/file-store.ts +217 -0
- package/src/store/filters.ts +83 -0
- package/src/store/read-store.ts +340 -0
- package/src/store/time.ts +54 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { GuckEvent, GuckLevel, GuckSearchParams } from "../schema.js";
|
|
2
|
+
import { normalizeTimestamp } from "./time.js";
|
|
3
|
+
|
|
4
|
+
const matchesContains = (event: GuckEvent, needle: string): boolean => {
|
|
5
|
+
const lower = needle.toLowerCase();
|
|
6
|
+
const message = event.message?.toLowerCase() ?? "";
|
|
7
|
+
if (message.includes(lower)) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (event.type.toLowerCase().includes(lower)) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (event.session_id?.toLowerCase().includes(lower)) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (event.data) {
|
|
17
|
+
try {
|
|
18
|
+
const serialized = JSON.stringify(event.data).toLowerCase();
|
|
19
|
+
if (serialized.includes(lower)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getEventTimestamp = (event: GuckEvent): number | undefined => {
|
|
30
|
+
return normalizeTimestamp(event.ts);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const eventMatches = (
|
|
34
|
+
event: GuckEvent,
|
|
35
|
+
params: GuckSearchParams,
|
|
36
|
+
sinceMs?: number,
|
|
37
|
+
untilMs?: number,
|
|
38
|
+
): boolean => {
|
|
39
|
+
if (params.service && event.service !== params.service) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (params.session_id && event.session_id !== params.session_id) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (params.run_id && event.run_id !== params.run_id) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (params.types && params.types.length > 0 && !params.types.includes(event.type)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (params.levels && params.levels.length > 0 && !params.levels.includes(event.level)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (params.contains && !matchesContains(event, params.contains)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const ts = getEventTimestamp(event);
|
|
58
|
+
if (sinceMs !== undefined && ts !== undefined && ts < sinceMs) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (untilMs !== undefined && ts !== undefined && ts > untilMs) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const normalizeLevel = (level?: string): GuckLevel | undefined => {
|
|
68
|
+
if (!level) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const value = level.toLowerCase();
|
|
72
|
+
if (
|
|
73
|
+
value === "trace" ||
|
|
74
|
+
value === "debug" ||
|
|
75
|
+
value === "info" ||
|
|
76
|
+
value === "warn" ||
|
|
77
|
+
value === "error" ||
|
|
78
|
+
value === "fatal"
|
|
79
|
+
) {
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
};
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveStoreDir } from "../config.js";
|
|
3
|
+
import {
|
|
4
|
+
GuckConfig,
|
|
5
|
+
GuckEvent,
|
|
6
|
+
GuckReadBackendConfig,
|
|
7
|
+
GuckSearchParams,
|
|
8
|
+
GuckSessionsParams,
|
|
9
|
+
GuckStatsParams,
|
|
10
|
+
GuckTailParams,
|
|
11
|
+
} from "../schema.js";
|
|
12
|
+
import { normalizeTimestamp } from "./time.js";
|
|
13
|
+
import { createCloudWatchBackend } from "./backends/cloudwatch.js";
|
|
14
|
+
import { createK8sBackend } from "./backends/k8s.js";
|
|
15
|
+
import { createLocalBackend } from "./backends/local.js";
|
|
16
|
+
import { ReadBackend } from "./backends/types.js";
|
|
17
|
+
|
|
18
|
+
type BackendEntry = {
|
|
19
|
+
type: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
backend: ReadBackend;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type BackendError = { backend: string; backend_id?: string; message: string };
|
|
25
|
+
|
|
26
|
+
type ResolvedBackends = {
|
|
27
|
+
backends: BackendEntry[];
|
|
28
|
+
errors: BackendError[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const resolveLocalDir = (rootDir: string, storeDir: string, dir?: string): string => {
|
|
32
|
+
if (!dir) {
|
|
33
|
+
return storeDir;
|
|
34
|
+
}
|
|
35
|
+
if (path.isAbsolute(dir)) {
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
return path.join(rootDir, dir);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const resolveBackends = (config: GuckConfig, rootDir: string): ResolvedBackends => {
|
|
42
|
+
const errors: BackendError[] = [];
|
|
43
|
+
const backends: BackendEntry[] = [];
|
|
44
|
+
const storeDir = resolveStoreDir(config, rootDir);
|
|
45
|
+
const readConfig = config.read ?? { backend: "local" };
|
|
46
|
+
|
|
47
|
+
const addBackend = (backendConfig: GuckReadBackendConfig): void => {
|
|
48
|
+
if (backendConfig.type === "local") {
|
|
49
|
+
const dir = resolveLocalDir(rootDir, storeDir, backendConfig.dir);
|
|
50
|
+
backends.push({
|
|
51
|
+
type: "local",
|
|
52
|
+
id: backendConfig.id,
|
|
53
|
+
backend: createLocalBackend({ storeDir: dir, config, backendId: backendConfig.id }),
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (backendConfig.type === "cloudwatch") {
|
|
59
|
+
backends.push({
|
|
60
|
+
type: "cloudwatch",
|
|
61
|
+
id: backendConfig.id,
|
|
62
|
+
backend: createCloudWatchBackend(backendConfig),
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (backendConfig.type === "k8s") {
|
|
68
|
+
backends.push({
|
|
69
|
+
type: "k8s",
|
|
70
|
+
id: backendConfig.id,
|
|
71
|
+
backend: createK8sBackend(backendConfig),
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const unknown = backendConfig as { type: string; id?: string };
|
|
77
|
+
errors.push({
|
|
78
|
+
backend: unknown.type,
|
|
79
|
+
backend_id: unknown.id,
|
|
80
|
+
message: `Unknown backend type: ${unknown.type}`,
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (readConfig.backend !== "multi") {
|
|
85
|
+
addBackend({ type: "local" });
|
|
86
|
+
return { backends, errors };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const configured = readConfig.backends ?? [];
|
|
90
|
+
if (configured.length === 0) {
|
|
91
|
+
addBackend({ type: "local" });
|
|
92
|
+
return { backends, errors };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const backendConfig of configured) {
|
|
96
|
+
addBackend(backendConfig);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { backends, errors };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const filterBackends = (
|
|
103
|
+
entries: BackendEntry[],
|
|
104
|
+
filters?: string[],
|
|
105
|
+
): BackendEntry[] => {
|
|
106
|
+
if (!filters || filters.length === 0) {
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
const filterSet = new Set(filters);
|
|
110
|
+
return entries.filter((entry) => {
|
|
111
|
+
if (entry.id && filterSet.has(entry.id)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (filterSet.has(entry.type)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const sortEventsDesc = (events: GuckEvent[]): GuckEvent[] => {
|
|
122
|
+
return [...events].sort((a, b) => {
|
|
123
|
+
const aTs = normalizeTimestamp(a.ts) ?? 0;
|
|
124
|
+
const bTs = normalizeTimestamp(b.ts) ?? 0;
|
|
125
|
+
return bTs - aTs;
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const readSearch = async (
|
|
130
|
+
config: GuckConfig,
|
|
131
|
+
rootDir: string,
|
|
132
|
+
params: GuckSearchParams,
|
|
133
|
+
): Promise<{ events: GuckEvent[]; truncated: boolean; errors?: BackendError[] }> => {
|
|
134
|
+
const { backends, errors: resolveErrors } = resolveBackends(config, rootDir);
|
|
135
|
+
const selected = filterBackends(backends, params.backends);
|
|
136
|
+
const errors: BackendError[] = [...resolveErrors];
|
|
137
|
+
|
|
138
|
+
if (params.backends && params.backends.length > 0 && selected.length === 0) {
|
|
139
|
+
errors.push({
|
|
140
|
+
backend: "multi",
|
|
141
|
+
message: `No backends matched filter: ${params.backends.join(", ")}`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const results = await Promise.all(
|
|
146
|
+
selected.map(async (entry) => {
|
|
147
|
+
try {
|
|
148
|
+
const result = await entry.backend.search(params);
|
|
149
|
+
return { entry, result };
|
|
150
|
+
} catch (error) {
|
|
151
|
+
errors.push({
|
|
152
|
+
backend: entry.type,
|
|
153
|
+
backend_id: entry.id,
|
|
154
|
+
message: error instanceof Error ? error.message : String(error),
|
|
155
|
+
});
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const limit = params.limit ?? config.mcp.max_results;
|
|
162
|
+
const events: GuckEvent[] = [];
|
|
163
|
+
let truncated = false;
|
|
164
|
+
|
|
165
|
+
for (const item of results) {
|
|
166
|
+
if (!item) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
events.push(...item.result.events);
|
|
170
|
+
if (item.result.truncated) {
|
|
171
|
+
truncated = true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const sorted = sortEventsDesc(events);
|
|
176
|
+
const mergedCount = sorted.length;
|
|
177
|
+
const sliced = sorted.slice(0, limit);
|
|
178
|
+
if (mergedCount > limit) {
|
|
179
|
+
truncated = true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
events: sliced,
|
|
184
|
+
truncated,
|
|
185
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const readStats = async (
|
|
190
|
+
config: GuckConfig,
|
|
191
|
+
rootDir: string,
|
|
192
|
+
params: GuckStatsParams,
|
|
193
|
+
): Promise<{ buckets: Array<{ key: string; count: number }>; errors?: BackendError[] }> => {
|
|
194
|
+
const { backends, errors: resolveErrors } = resolveBackends(config, rootDir);
|
|
195
|
+
const selected = filterBackends(backends, params.backends);
|
|
196
|
+
const errors: BackendError[] = [...resolveErrors];
|
|
197
|
+
|
|
198
|
+
if (params.backends && params.backends.length > 0 && selected.length === 0) {
|
|
199
|
+
errors.push({
|
|
200
|
+
backend: "multi",
|
|
201
|
+
message: `No backends matched filter: ${params.backends.join(", ")}`,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const results = await Promise.all(
|
|
206
|
+
selected.map(async (entry) => {
|
|
207
|
+
try {
|
|
208
|
+
const result = await entry.backend.stats(params);
|
|
209
|
+
return { entry, result };
|
|
210
|
+
} catch (error) {
|
|
211
|
+
errors.push({
|
|
212
|
+
backend: entry.type,
|
|
213
|
+
backend_id: entry.id,
|
|
214
|
+
message: error instanceof Error ? error.message : String(error),
|
|
215
|
+
});
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const buckets = new Map<string, number>();
|
|
222
|
+
for (const item of results) {
|
|
223
|
+
if (!item) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
for (const bucket of item.result.buckets) {
|
|
227
|
+
buckets.set(bucket.key, (buckets.get(bucket.key) ?? 0) + bucket.count);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const limit = params.limit ?? config.mcp.max_results;
|
|
232
|
+
const merged = [...buckets.entries()]
|
|
233
|
+
.sort((a, b) => b[1] - a[1])
|
|
234
|
+
.slice(0, limit)
|
|
235
|
+
.map(([key, count]) => ({ key, count }));
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
buckets: merged,
|
|
239
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const readSessions = async (
|
|
244
|
+
config: GuckConfig,
|
|
245
|
+
rootDir: string,
|
|
246
|
+
params: GuckSessionsParams,
|
|
247
|
+
): Promise<{
|
|
248
|
+
sessions: Array<{ session_id: string; last_ts: string; event_count: number; error_count: number }>;
|
|
249
|
+
errors?: BackendError[];
|
|
250
|
+
}> => {
|
|
251
|
+
const { backends, errors: resolveErrors } = resolveBackends(config, rootDir);
|
|
252
|
+
const selected = filterBackends(backends, params.backends);
|
|
253
|
+
const errors: BackendError[] = [...resolveErrors];
|
|
254
|
+
|
|
255
|
+
if (params.backends && params.backends.length > 0 && selected.length === 0) {
|
|
256
|
+
errors.push({
|
|
257
|
+
backend: "multi",
|
|
258
|
+
message: `No backends matched filter: ${params.backends.join(", ")}`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const results = await Promise.all(
|
|
263
|
+
selected.map(async (entry) => {
|
|
264
|
+
try {
|
|
265
|
+
const result = await entry.backend.sessions(params);
|
|
266
|
+
return { entry, result };
|
|
267
|
+
} catch (error) {
|
|
268
|
+
errors.push({
|
|
269
|
+
backend: entry.type,
|
|
270
|
+
backend_id: entry.id,
|
|
271
|
+
message: error instanceof Error ? error.message : String(error),
|
|
272
|
+
});
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const sessions = new Map<
|
|
279
|
+
string,
|
|
280
|
+
{ session_id: string; last_ts: string; event_count: number; error_count: number }
|
|
281
|
+
>();
|
|
282
|
+
for (const item of results) {
|
|
283
|
+
if (!item) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
for (const session of item.result.sessions) {
|
|
287
|
+
const existing = sessions.get(session.session_id) ?? {
|
|
288
|
+
session_id: session.session_id,
|
|
289
|
+
last_ts: session.last_ts,
|
|
290
|
+
event_count: 0,
|
|
291
|
+
error_count: 0,
|
|
292
|
+
};
|
|
293
|
+
existing.event_count += session.event_count;
|
|
294
|
+
existing.error_count += session.error_count;
|
|
295
|
+
const existingTs = normalizeTimestamp(existing.last_ts) ?? 0;
|
|
296
|
+
const sessionTs = normalizeTimestamp(session.last_ts) ?? 0;
|
|
297
|
+
if (sessionTs > existingTs) {
|
|
298
|
+
existing.last_ts = session.last_ts;
|
|
299
|
+
}
|
|
300
|
+
sessions.set(session.session_id, existing);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const limit = params.limit ?? config.mcp.max_results;
|
|
305
|
+
const merged = [...sessions.values()]
|
|
306
|
+
.sort((a, b) => (normalizeTimestamp(b.last_ts) ?? 0) - (normalizeTimestamp(a.last_ts) ?? 0))
|
|
307
|
+
.slice(0, limit);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
sessions: merged,
|
|
311
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export const readTail = async (
|
|
316
|
+
config: GuckConfig,
|
|
317
|
+
rootDir: string,
|
|
318
|
+
params: GuckTailParams & { since?: string },
|
|
319
|
+
): Promise<{ events: GuckEvent[]; truncated: boolean; errors?: BackendError[] }> => {
|
|
320
|
+
const limit = params.limit ?? Math.min(config.mcp.max_results, 50);
|
|
321
|
+
const searchParams: GuckSearchParams = {
|
|
322
|
+
service: params.service,
|
|
323
|
+
session_id: params.session_id,
|
|
324
|
+
run_id: params.run_id,
|
|
325
|
+
since: params.since,
|
|
326
|
+
limit: config.mcp.max_results,
|
|
327
|
+
backends: params.backends,
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const result = await readSearch(config, rootDir, searchParams);
|
|
331
|
+
const sorted = sortEventsDesc(result.events);
|
|
332
|
+
const sliced = sorted.slice(0, limit);
|
|
333
|
+
const truncated = result.truncated || sorted.length > limit;
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
events: sliced,
|
|
337
|
+
truncated,
|
|
338
|
+
errors: result.errors,
|
|
339
|
+
};
|
|
340
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const DURATION_RE = /^(\d+)(ms|s|m|h|d)$/i;
|
|
2
|
+
|
|
3
|
+
export const parseTimeInput = (value?: string): number | undefined => {
|
|
4
|
+
if (!value) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!trimmed) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const durationMatch = trimmed.match(DURATION_RE);
|
|
14
|
+
if (durationMatch) {
|
|
15
|
+
const amount = Number(durationMatch[1]);
|
|
16
|
+
const unit = durationMatch[2]?.toLowerCase() ?? "";
|
|
17
|
+
const unitMs =
|
|
18
|
+
unit === "ms"
|
|
19
|
+
? 1
|
|
20
|
+
: unit === "s"
|
|
21
|
+
? 1000
|
|
22
|
+
: unit === "m"
|
|
23
|
+
? 60_000
|
|
24
|
+
: unit === "h"
|
|
25
|
+
? 3_600_000
|
|
26
|
+
: 86_400_000;
|
|
27
|
+
return Date.now() - amount * unitMs;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parsed = Date.parse(trimmed);
|
|
31
|
+
if (!Number.isNaN(parsed)) {
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const normalizeTimestamp = (value: unknown): number | undefined => {
|
|
39
|
+
if (typeof value === "number") {
|
|
40
|
+
return Number.isFinite(value) ? value : undefined;
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === "string") {
|
|
43
|
+
const parsed = Date.parse(value);
|
|
44
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const formatDateSegment = (date: Date): string => {
|
|
50
|
+
const year = date.getUTCFullYear();
|
|
51
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
52
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
53
|
+
return `${year}-${month}-${day}`;
|
|
54
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"types": ["node"]
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts"]
|
|
19
|
+
}
|