@oh-my-pi/pi-utils 17.2.9 → 17.2.11
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/CHANGELOG.md +31 -0
- package/dist/types/acp/connection.d.ts +118 -0
- package/dist/types/acp/protocol.d.ts +526 -0
- package/dist/types/acp/schema.d.ts +41 -0
- package/dist/types/acp/stream.d.ts +8 -0
- package/dist/types/acp/transport.d.ts +81 -0
- package/dist/types/acp.d.ts +6 -0
- package/dist/types/browsers.d.ts +68 -0
- package/dist/types/chalk.d.ts +125 -0
- package/dist/types/dates.d.ts +7 -0
- package/dist/types/docx/converter.d.ts +46 -0
- package/dist/types/docx/xml.d.ts +26 -0
- package/dist/types/docx/zip.d.ts +6 -0
- package/dist/types/docx.d.ts +11 -0
- package/dist/types/dom/core.d.ts +431 -0
- package/dist/types/dom/parser.d.ts +7 -0
- package/dist/types/dom/selector.d.ts +5 -0
- package/dist/types/dom.d.ts +5 -0
- package/dist/types/frontmatter.d.ts +21 -0
- package/dist/types/headers.d.ts +34 -0
- package/dist/types/logger/rotating-file.d.ts +18 -0
- package/dist/types/lru.d.ts +46 -0
- package/dist/types/marked/core.d.ts +445 -0
- package/dist/types/marked.d.ts +2 -0
- package/dist/types/postmortem.d.ts +14 -0
- package/dist/types/procmgr.d.ts +16 -0
- package/dist/types/prompt.d.ts +2 -2
- package/dist/types/readability/readability.d.ts +9 -0
- package/dist/types/readability/readerable.d.ts +10 -0
- package/dist/types/readability/types.d.ts +70 -0
- package/dist/types/readability.d.ts +4 -0
- package/dist/types/template.d.ts +62 -0
- package/dist/types/turndown/gfm.d.ts +11 -0
- package/dist/types/turndown/html.d.ts +5 -0
- package/dist/types/turndown/service.d.ts +21 -0
- package/dist/types/turndown/types.d.ts +70 -0
- package/dist/types/turndown.d.ts +4 -0
- package/dist/types/vterm/buffer.d.ts +99 -0
- package/dist/types/vterm/terminal.d.ts +44 -0
- package/dist/types/vterm.d.ts +8 -0
- package/dist/types/xml.d.ts +31 -0
- package/package.json +2 -5
- package/src/acp/connection.ts +344 -0
- package/src/acp/protocol.ts +466 -0
- package/src/acp/schema.ts +160 -0
- package/src/acp/stream.ts +82 -0
- package/src/acp/transport.ts +213 -0
- package/src/acp.ts +6 -0
- package/src/browsers.ts +501 -0
- package/src/chalk.ts +312 -0
- package/src/dates.ts +194 -0
- package/src/docx/converter.ts +681 -0
- package/src/docx/xml.ts +166 -0
- package/src/docx/zip.ts +87 -0
- package/src/docx.ts +20 -0
- package/src/dom/core.ts +1254 -0
- package/src/dom/parser.ts +370 -0
- package/src/dom/selector.ts +290 -0
- package/src/dom.ts +33 -0
- package/src/frontmatter.ts +44 -14
- package/src/headers.ts +167 -0
- package/src/logger/rotating-file.ts +149 -0
- package/src/logger.ts +12 -28
- package/src/lru.ts +185 -0
- package/src/marked/core.ts +1576 -0
- package/src/marked.ts +2 -0
- package/src/postmortem.ts +44 -1
- package/src/procmgr.ts +21 -4
- package/src/prompt.ts +5 -22
- package/src/readability/readability.ts +533 -0
- package/src/readability/readerable.ts +51 -0
- package/src/readability/types.ts +72 -0
- package/src/readability.ts +11 -0
- package/src/template.ts +586 -0
- package/src/turndown/gfm.ts +106 -0
- package/src/turndown/html.ts +257 -0
- package/src/turndown/service.ts +334 -0
- package/src/turndown/types.ts +81 -0
- package/src/turndown.ts +5 -0
- package/src/vterm/buffer.ts +218 -0
- package/src/vterm/terminal.ts +773 -0
- package/src/vterm.ts +8 -0
- package/src/xml.ts +313 -0
- package/src/winston-daily-rotate-file.d.ts +0 -6
package/src/frontmatter.ts
CHANGED
|
@@ -12,15 +12,20 @@ function kebabToCamel(key: string): string {
|
|
|
12
12
|
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Recursively normalize object keys from kebab-case to camelCase — the
|
|
17
|
+
* representation convention for frontmatter consumed inside this codebase.
|
|
18
|
+
* Exported for loaders that parse with `rawKeys: true` to validate exact
|
|
19
|
+
* spec-defined keys, then normalize for storage.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeFrontmatterKeys<T>(obj: T): T {
|
|
17
22
|
if (obj === null || typeof obj !== "object") return obj;
|
|
18
23
|
if (Array.isArray(obj)) {
|
|
19
24
|
let changed = false;
|
|
20
25
|
const out: unknown[] = new Array(obj.length);
|
|
21
26
|
for (let i = 0; i < obj.length; i++) {
|
|
22
27
|
const v = obj[i];
|
|
23
|
-
const nv =
|
|
28
|
+
const nv = normalizeFrontmatterKeys(v);
|
|
24
29
|
out[i] = nv;
|
|
25
30
|
if (nv !== v) changed = true;
|
|
26
31
|
}
|
|
@@ -30,7 +35,7 @@ function normalizeKeys<T>(obj: T): T {
|
|
|
30
35
|
const result: Record<string, unknown> = {};
|
|
31
36
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
32
37
|
const nk = key.includes("-") ? kebabToCamel(key) : key;
|
|
33
|
-
const nv =
|
|
38
|
+
const nv = normalizeFrontmatterKeys(value);
|
|
34
39
|
result[nk] = nv;
|
|
35
40
|
if (nk !== key || nv !== value) changed = true;
|
|
36
41
|
}
|
|
@@ -55,8 +60,8 @@ function quoteAmbiguousPlainScalars(metadata: string): string | undefined {
|
|
|
55
60
|
return changed ? lines.join("\n") : undefined;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
function parseYamlRecord(metadata: string): Record<string, unknown> | null {
|
|
59
|
-
const loaded = YAML.parse(metadata.replaceAll("\t", " "));
|
|
63
|
+
function parseYamlRecord(metadata: string, repairTabs: boolean): Record<string, unknown> | null {
|
|
64
|
+
const loaded = YAML.parse(repairTabs ? metadata.replaceAll("\t", " ") : metadata);
|
|
60
65
|
if (loaded === null || loaded === undefined) return null;
|
|
61
66
|
if (typeof loaded !== "object" || Array.isArray(loaded)) return null;
|
|
62
67
|
return loaded as Record<string, unknown>;
|
|
@@ -97,6 +102,20 @@ export interface FrontmatterOptions {
|
|
|
97
102
|
normalize?: boolean;
|
|
98
103
|
/** Level of error handling */
|
|
99
104
|
level?: "off" | "warn" | "fatal";
|
|
105
|
+
/**
|
|
106
|
+
* Attempt lenient recovery of near-miss input before failing: quote
|
|
107
|
+
* ambiguous plain scalars, replace tabs with spaces, and strip leading HTML
|
|
108
|
+
* comments ahead of the opening delimiter. Default `true`. Spec-conformant
|
|
109
|
+
* loaders set `false` so malformed input is rejected instead of silently
|
|
110
|
+
* repaired (CRLF newline normalization still applies).
|
|
111
|
+
*/
|
|
112
|
+
repair?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Preserve frontmatter keys verbatim instead of normalizing kebab-case to
|
|
115
|
+
* camelCase. Default `false`. Strict spec loaders use this so a standard
|
|
116
|
+
* key (e.g. `allowed-tools`) is never aliased with its camelCase form.
|
|
117
|
+
*/
|
|
118
|
+
rawKeys?: boolean;
|
|
100
119
|
}
|
|
101
120
|
|
|
102
121
|
/**
|
|
@@ -107,11 +126,22 @@ export function parseFrontmatter(
|
|
|
107
126
|
content: string,
|
|
108
127
|
options?: FrontmatterOptions,
|
|
109
128
|
): { frontmatter: Record<string, unknown>; body: string } {
|
|
110
|
-
const {
|
|
129
|
+
const {
|
|
130
|
+
location,
|
|
131
|
+
source,
|
|
132
|
+
fallback,
|
|
133
|
+
normalize = true,
|
|
134
|
+
level = "warn",
|
|
135
|
+
repair = true,
|
|
136
|
+
rawKeys = false,
|
|
137
|
+
} = options ?? {};
|
|
138
|
+
const finalizeKeys = (fm: Record<string, unknown>): Record<string, unknown> =>
|
|
139
|
+
rawKeys ? fm : normalizeFrontmatterKeys(fm);
|
|
111
140
|
const loc = location ?? source;
|
|
112
141
|
const frontmatter: Record<string, unknown> = { ...fallback };
|
|
113
142
|
|
|
114
|
-
const
|
|
143
|
+
const newlineNormalized = normalize ? content.replace(/\r\n?/g, "\n") : content;
|
|
144
|
+
const normalized = normalize && repair ? stripHtmlComments(newlineNormalized) : newlineNormalized;
|
|
115
145
|
if (!normalized.startsWith("---")) {
|
|
116
146
|
return { frontmatter, body: normalized };
|
|
117
147
|
}
|
|
@@ -125,14 +155,14 @@ export function parseFrontmatter(
|
|
|
125
155
|
const body = normalized.slice(endIndex + 4).trim();
|
|
126
156
|
|
|
127
157
|
try {
|
|
128
|
-
const loaded = parseYamlRecord(metadata);
|
|
129
|
-
return { frontmatter:
|
|
158
|
+
const loaded = parseYamlRecord(metadata, repair);
|
|
159
|
+
return { frontmatter: finalizeKeys({ ...frontmatter, ...loaded }), body };
|
|
130
160
|
} catch (error) {
|
|
131
|
-
const quotedMetadata = quoteAmbiguousPlainScalars(metadata);
|
|
161
|
+
const quotedMetadata = repair ? quoteAmbiguousPlainScalars(metadata) : undefined;
|
|
132
162
|
if (quotedMetadata) {
|
|
133
163
|
try {
|
|
134
|
-
const loaded = parseYamlRecord(quotedMetadata);
|
|
135
|
-
return { frontmatter:
|
|
164
|
+
const loaded = parseYamlRecord(quotedMetadata, true);
|
|
165
|
+
return { frontmatter: finalizeKeys({ ...frontmatter, ...loaded }), body };
|
|
136
166
|
} catch {
|
|
137
167
|
// Fall through to the existing warning + simple key/value fallback.
|
|
138
168
|
}
|
|
@@ -170,6 +200,6 @@ export function parseFrontmatter(
|
|
|
170
200
|
frontmatter[match[1]] = value;
|
|
171
201
|
}
|
|
172
202
|
|
|
173
|
-
return { frontmatter:
|
|
203
|
+
return { frontmatter: finalizeKeys(frontmatter), body };
|
|
174
204
|
}
|
|
175
205
|
}
|
package/src/headers.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/** Behavior-compatible reimplementation of header-generator's used surface. */
|
|
2
|
+
|
|
3
|
+
/** A browser family supported by the curated header profiles. */
|
|
4
|
+
export type BrowserName = "chrome" | "firefox" | "safari";
|
|
5
|
+
|
|
6
|
+
/** A desktop operating system supported by the curated header profiles. */
|
|
7
|
+
export type OperatingSystem = "windows" | "macos" | "linux";
|
|
8
|
+
|
|
9
|
+
/** Constructor and per-call constraints for header generation. */
|
|
10
|
+
export interface HeaderGeneratorOptions {
|
|
11
|
+
/** Browser families eligible for a draw. */
|
|
12
|
+
browsers: BrowserName[];
|
|
13
|
+
/** Browser selection query; the supported `last 3 versions` query uses the curated versions. */
|
|
14
|
+
browserListQuery: string;
|
|
15
|
+
/** Desktop operating systems eligible for a draw. */
|
|
16
|
+
operatingSystems: OperatingSystem[];
|
|
17
|
+
/** Device classes eligible for a draw. */
|
|
18
|
+
devices: "desktop"[];
|
|
19
|
+
/** Ordered locales for the Accept-Language value. */
|
|
20
|
+
locales: string[];
|
|
21
|
+
/** HTTP protocol generation mode. */
|
|
22
|
+
httpVersion: "1" | "2";
|
|
23
|
+
/** Whether impossible constraints throw instead of relaxing to a coherent profile. */
|
|
24
|
+
strict: boolean;
|
|
25
|
+
/** Random source returning a value in the range from zero (inclusive) to one (exclusive). */
|
|
26
|
+
rng: () => number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A generated HTTP request header map. */
|
|
30
|
+
export type Headers = Record<string, string>;
|
|
31
|
+
|
|
32
|
+
type ResolvedOptions = Omit<HeaderGeneratorOptions, "rng">;
|
|
33
|
+
|
|
34
|
+
type BrowserProfile = {
|
|
35
|
+
browser: BrowserName;
|
|
36
|
+
operatingSystem: OperatingSystem;
|
|
37
|
+
version: number;
|
|
38
|
+
userAgent: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const DEFAULT_OPTIONS: ResolvedOptions = {
|
|
42
|
+
browsers: ["chrome", "firefox", "safari"],
|
|
43
|
+
browserListQuery: "",
|
|
44
|
+
operatingSystems: ["windows", "macos", "linux"],
|
|
45
|
+
devices: ["desktop"],
|
|
46
|
+
locales: ["en-US", "en"],
|
|
47
|
+
httpVersion: "2",
|
|
48
|
+
strict: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const VERSIONS: Readonly<Record<BrowserName, readonly number[]>> = {
|
|
52
|
+
chrome: [149, 150, 151],
|
|
53
|
+
firefox: [147, 148, 149],
|
|
54
|
+
safari: [26, 26.1, 26.2],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function pick<T>(values: readonly T[], rng: () => number): T {
|
|
58
|
+
const random = rng();
|
|
59
|
+
const index = Math.min(values.length - 1, Math.max(0, Math.floor(random * values.length)));
|
|
60
|
+
const value = values[index];
|
|
61
|
+
if (value === undefined) throw new Error("Cannot choose from an empty header profile list");
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatLocales(locales: readonly string[]): string {
|
|
66
|
+
return locales
|
|
67
|
+
.slice(0, 10)
|
|
68
|
+
.map((locale, index) => {
|
|
69
|
+
if (index === 0) return locale;
|
|
70
|
+
const quality = Math.max(0.1, 1 - index / 10).toFixed(1);
|
|
71
|
+
return `${locale};q=${quality}`;
|
|
72
|
+
})
|
|
73
|
+
.join(",");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function makeUserAgent(browser: BrowserName, operatingSystem: OperatingSystem, version: number): string {
|
|
77
|
+
if (browser === "safari") {
|
|
78
|
+
return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/${version.toFixed(1)} Safari/605.1.15`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const platform =
|
|
82
|
+
operatingSystem === "windows"
|
|
83
|
+
? "Windows NT 10.0; Win64; x64"
|
|
84
|
+
: operatingSystem === "macos"
|
|
85
|
+
? "Macintosh; Intel Mac OS X 10_15_7"
|
|
86
|
+
: "X11; Linux x86_64";
|
|
87
|
+
if (browser === "firefox") {
|
|
88
|
+
return `Mozilla/5.0 (${platform}; rv:${version}.0) Gecko/20100101 Firefox/${version}.0`;
|
|
89
|
+
}
|
|
90
|
+
return `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version}.0.0.0 Safari/537.36`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeProfile(options: ResolvedOptions, rng: () => number): BrowserProfile {
|
|
94
|
+
const candidates: Array<{ browser: BrowserName; operatingSystem: OperatingSystem }> = [];
|
|
95
|
+
for (const browser of options.browsers) {
|
|
96
|
+
for (const operatingSystem of options.operatingSystems) {
|
|
97
|
+
if (browser === "safari" && operatingSystem !== "macos") continue;
|
|
98
|
+
candidates.push({ browser, operatingSystem });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (candidates.length === 0) {
|
|
103
|
+
if (options.strict) throw new Error("No coherent browser profile matches the requested options");
|
|
104
|
+
candidates.push({ browser: "chrome", operatingSystem: "windows" });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const candidate = pick(candidates, rng);
|
|
108
|
+
const version = pick(VERSIONS[candidate.browser], rng);
|
|
109
|
+
return {
|
|
110
|
+
...candidate,
|
|
111
|
+
version,
|
|
112
|
+
userAgent: makeUserAgent(candidate.browser, candidate.operatingSystem, version),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Generates coherent modern desktop browser navigation headers. */
|
|
117
|
+
export class HeaderGenerator {
|
|
118
|
+
#options: ResolvedOptions;
|
|
119
|
+
#rng: () => number;
|
|
120
|
+
|
|
121
|
+
/** Creates a generator with reusable constraints and an optionally injectable RNG. */
|
|
122
|
+
constructor(options: Partial<HeaderGeneratorOptions> = {}) {
|
|
123
|
+
this.#rng = options.rng ?? Math.random;
|
|
124
|
+
this.#options = { ...DEFAULT_OPTIONS, ...options };
|
|
125
|
+
if (this.#options.devices.some(device => device !== "desktop") && this.#options.strict) {
|
|
126
|
+
throw new Error("Only desktop browser profiles are available");
|
|
127
|
+
}
|
|
128
|
+
if (this.#options.locales.length === 0 && this.#options.strict) {
|
|
129
|
+
throw new Error("At least one locale is required");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Generates one header set, applying per-call constraints and request overrides. */
|
|
134
|
+
getHeaders(options: Partial<HeaderGeneratorOptions> = {}, overrides: Headers = {}): Headers {
|
|
135
|
+
const resolved = { ...this.#options, ...options };
|
|
136
|
+
const rng = options.rng ?? this.#rng;
|
|
137
|
+
const profile = makeProfile(resolved, rng);
|
|
138
|
+
const headers: Headers = {
|
|
139
|
+
accept:
|
|
140
|
+
profile.browser === "chrome"
|
|
141
|
+
? "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
|
142
|
+
: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
143
|
+
"user-agent": profile.userAgent,
|
|
144
|
+
"accept-encoding": profile.browser === "safari" ? "gzip, deflate, br" : "gzip, deflate, br, zstd",
|
|
145
|
+
"accept-language": formatLocales(resolved.locales.length > 0 ? resolved.locales : DEFAULT_OPTIONS.locales),
|
|
146
|
+
"upgrade-insecure-requests": "1",
|
|
147
|
+
"sec-fetch-dest": "document",
|
|
148
|
+
"sec-fetch-mode": "navigate",
|
|
149
|
+
"sec-fetch-site": "none",
|
|
150
|
+
"sec-fetch-user": "?1",
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (profile.browser === "chrome") {
|
|
154
|
+
headers["sec-ch-ua"] =
|
|
155
|
+
`"Google Chrome";v="${profile.version}", "Chromium";v="${profile.version}", "Not_A Brand";v="24"`;
|
|
156
|
+
headers["sec-ch-ua-mobile"] = "?0";
|
|
157
|
+
headers["sec-ch-ua-platform"] =
|
|
158
|
+
profile.operatingSystem === "windows"
|
|
159
|
+
? '"Windows"'
|
|
160
|
+
: profile.operatingSystem === "macos"
|
|
161
|
+
? '"macOS"'
|
|
162
|
+
: '"Linux"';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { ...headers, ...overrides };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Behavior-compatible reimplementation of winston-daily-rotate-file's used surface. */
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
|
|
7
|
+
interface AuditEntry {
|
|
8
|
+
readonly date: number;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly hash: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AuditState {
|
|
14
|
+
readonly keep: { readonly days: false; readonly amount: number };
|
|
15
|
+
readonly auditLog: string;
|
|
16
|
+
readonly files: AuditEntry[];
|
|
17
|
+
readonly hashType: "sha256";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Configuration for a process-local rotating file sink. */
|
|
21
|
+
export interface RotatingFileOptions {
|
|
22
|
+
readonly directory: string;
|
|
23
|
+
readonly filenamePrefix: string;
|
|
24
|
+
readonly filenameSuffix: string;
|
|
25
|
+
readonly auditFile: string;
|
|
26
|
+
readonly maxBytes: number;
|
|
27
|
+
readonly maxFiles: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isAuditEntry(value: unknown): value is AuditEntry {
|
|
31
|
+
if (value === null || typeof value !== "object") return false;
|
|
32
|
+
const entry = value as Record<string, unknown>;
|
|
33
|
+
return typeof entry.date === "number" && typeof entry.name === "string" && typeof entry.hash === "string";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Synchronous append sink with local-day and size rotation plus bounded retention. */
|
|
37
|
+
export class RotatingFileSink {
|
|
38
|
+
readonly #directory: string;
|
|
39
|
+
readonly #filenamePrefix: string;
|
|
40
|
+
readonly #filenameSuffix: string;
|
|
41
|
+
readonly #auditFile: string;
|
|
42
|
+
readonly #maxBytes: number;
|
|
43
|
+
readonly #maxFiles: number;
|
|
44
|
+
#files: AuditEntry[];
|
|
45
|
+
#activeDay: string | undefined;
|
|
46
|
+
#activeIndex = 0;
|
|
47
|
+
#activePath: string | undefined;
|
|
48
|
+
#activeBytes = 0;
|
|
49
|
+
#closed = false;
|
|
50
|
+
|
|
51
|
+
constructor(options: RotatingFileOptions) {
|
|
52
|
+
this.#directory = options.directory;
|
|
53
|
+
this.#filenamePrefix = options.filenamePrefix;
|
|
54
|
+
this.#filenameSuffix = options.filenameSuffix;
|
|
55
|
+
this.#auditFile = options.auditFile;
|
|
56
|
+
this.#maxBytes = options.maxBytes;
|
|
57
|
+
this.#maxFiles = options.maxFiles;
|
|
58
|
+
this.#files = this.#readAudit();
|
|
59
|
+
const now = new Date();
|
|
60
|
+
this.#selectFile(this.#localDay(now));
|
|
61
|
+
const activePath = this.#activePath;
|
|
62
|
+
if (activePath) {
|
|
63
|
+
this.#registerFile(activePath, now.getTime());
|
|
64
|
+
fs.closeSync(fs.openSync(activePath, "a"));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Append one already-formatted log record. */
|
|
69
|
+
write(line: string): void {
|
|
70
|
+
if (this.#closed) return;
|
|
71
|
+
const now = new Date();
|
|
72
|
+
this.#selectFile(this.#localDay(now));
|
|
73
|
+
const activePath = this.#activePath;
|
|
74
|
+
if (!activePath) return;
|
|
75
|
+
this.#registerFile(activePath, now.getTime());
|
|
76
|
+
const record = `${line}${os.EOL}`;
|
|
77
|
+
fs.appendFileSync(activePath, record, "utf8");
|
|
78
|
+
this.#activeBytes += Buffer.byteLength(record);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Stop accepting records. Synchronous writes require no drain phase. */
|
|
82
|
+
close(): void {
|
|
83
|
+
this.#closed = true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#localDay(date: Date): string {
|
|
87
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#selectFile(day: string): void {
|
|
91
|
+
if (day !== this.#activeDay) {
|
|
92
|
+
this.#activeDay = day;
|
|
93
|
+
this.#activeIndex = 0;
|
|
94
|
+
this.#setActivePath(day, 0);
|
|
95
|
+
}
|
|
96
|
+
while (this.#activeBytes > this.#maxBytes) {
|
|
97
|
+
this.#activeIndex++;
|
|
98
|
+
this.#setActivePath(day, this.#activeIndex);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#setActivePath(day: string, index: number): void {
|
|
103
|
+
const suffix = index === 0 ? "" : `.${index}`;
|
|
104
|
+
this.#activePath = path.join(
|
|
105
|
+
this.#directory,
|
|
106
|
+
`${this.#filenamePrefix}.${day}.${this.#filenameSuffix}.log${suffix}`,
|
|
107
|
+
);
|
|
108
|
+
try {
|
|
109
|
+
this.#activeBytes = fs.statSync(this.#activePath).size;
|
|
110
|
+
} catch {
|
|
111
|
+
this.#activeBytes = 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#registerFile(filePath: string, date: number): void {
|
|
116
|
+
if (this.#files.some(file => file.name === filePath)) return;
|
|
117
|
+
const hash = crypto.createHash("sha256").update(`${filePath}LOG_FILE${date}`).digest("hex");
|
|
118
|
+
this.#files.push({ date, name: filePath, hash });
|
|
119
|
+
while (this.#files.length > this.#maxFiles) {
|
|
120
|
+
const removed = this.#files.shift();
|
|
121
|
+
if (!removed) break;
|
|
122
|
+
try {
|
|
123
|
+
fs.rmSync(removed.name, { force: true });
|
|
124
|
+
} catch {
|
|
125
|
+
// Retention is best-effort; the current record must still be written.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.#writeAudit();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#readAudit(): AuditEntry[] {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = JSON.parse(fs.readFileSync(this.#auditFile, "utf8")) as { files?: unknown };
|
|
134
|
+
return Array.isArray(parsed.files) ? parsed.files.filter(isAuditEntry) : [];
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#writeAudit(): void {
|
|
141
|
+
const state: AuditState = {
|
|
142
|
+
keep: { days: false, amount: this.#maxFiles },
|
|
143
|
+
auditLog: this.#auditFile,
|
|
144
|
+
files: this.#files,
|
|
145
|
+
hashType: "sha256",
|
|
146
|
+
};
|
|
147
|
+
fs.writeFileSync(this.#auditFile, JSON.stringify(state, undefined, 4), "utf8");
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/logger.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/// <reference path="./winston-daily-rotate-file.d.ts" />
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Centralized logger for omp.
|
|
5
3
|
*
|
|
@@ -16,11 +14,8 @@ import * as fs from "node:fs";
|
|
|
16
14
|
import * as os from "node:os";
|
|
17
15
|
import * as path from "node:path";
|
|
18
16
|
import { isPromise } from "node:util/types";
|
|
19
|
-
import type DailyRotateFile from "winston-daily-rotate-file";
|
|
20
|
-
// Import the implementation directly because the package index imports and mutates
|
|
21
|
-
// Winston. The exact workspace catalog pin protects this internal entrypoint.
|
|
22
|
-
import DailyRotateFileImplementation from "winston-daily-rotate-file/daily-rotate-file.js";
|
|
23
17
|
import { getLogsDir } from "./dirs";
|
|
18
|
+
import { RotatingFileSink } from "./logger/rotating-file";
|
|
24
19
|
import { drainModuleLoadEvents } from "./timing-buffer";
|
|
25
20
|
/** Severity names accepted by the centralized logger. */
|
|
26
21
|
export type LogLevel = "error" | "warn" | "info" | "debug";
|
|
@@ -200,24 +195,18 @@ function formatLogInfo(info: NormalizedLogInfo): string {
|
|
|
200
195
|
return JSON.stringify(entry, jsonReplacer) as string;
|
|
201
196
|
}
|
|
202
197
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
close(): void;
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
/** Build a rotating file transport with process-local rotation and shared retention. */
|
|
209
|
-
function makeFileTransport(dir?: string): FileTransport {
|
|
198
|
+
/** Build a rotating file sink with process-local rotation and shared retention. */
|
|
199
|
+
function makeFileTransport(dir?: string): RotatingFileSink {
|
|
210
200
|
const logsDir = ensureDir(dir ?? getLogsDir());
|
|
211
201
|
pruneStaleProcessLogs(logsDir);
|
|
212
|
-
return new
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
202
|
+
return new RotatingFileSink({
|
|
203
|
+
directory: logsDir,
|
|
204
|
+
filenamePrefix: "omp",
|
|
205
|
+
filenameSuffix: String(process.pid),
|
|
206
|
+
maxBytes: 10 * 1024 * 1024,
|
|
217
207
|
maxFiles: 5,
|
|
218
|
-
zippedArchive: false,
|
|
219
208
|
auditFile: path.join(logsDir, `.omp.${process.pid}-audit.json`),
|
|
220
|
-
})
|
|
209
|
+
});
|
|
221
210
|
}
|
|
222
211
|
|
|
223
212
|
/**
|
|
@@ -227,16 +216,13 @@ function makeFileTransport(dir?: string): FileTransport {
|
|
|
227
216
|
let transportOpts: { console?: boolean; file?: boolean | string } = { file: true };
|
|
228
217
|
|
|
229
218
|
interface LocalTransports {
|
|
230
|
-
readonly file:
|
|
219
|
+
readonly file: RotatingFileSink | undefined;
|
|
231
220
|
readonly console: boolean;
|
|
232
221
|
}
|
|
233
222
|
|
|
234
223
|
/** Local transports, constructed lazily on first log emission. */
|
|
235
224
|
let activeTransports: LocalTransports | undefined;
|
|
236
225
|
|
|
237
|
-
const TRANSPORT_MESSAGE = Symbol.for("message");
|
|
238
|
-
const onTransportLogged = (): void => {};
|
|
239
|
-
|
|
240
226
|
function buildTransports(opts: { console?: boolean; file?: boolean | string }): LocalTransports {
|
|
241
227
|
return {
|
|
242
228
|
file: opts.file ? makeFileTransport(typeof opts.file === "string" ? opts.file : undefined) : undefined,
|
|
@@ -254,11 +240,9 @@ function emitLocally(level: LogLevel, message: string, context: Record<string, u
|
|
|
254
240
|
const info = normalizeLogInfo(level, message, context);
|
|
255
241
|
if (!transports.file && !transports.console) return;
|
|
256
242
|
|
|
257
|
-
// Winston applied the shared format before dispatch, then the same format a
|
|
258
|
-
// second time inside Console. Keep those evaluation and timestamp semantics.
|
|
259
243
|
const line = formatLogInfo(info);
|
|
260
|
-
if (transports.file) transports.file.
|
|
261
|
-
if (transports.console)
|
|
244
|
+
if (transports.file) transports.file.write(line);
|
|
245
|
+
if (transports.console) fs.writeSync(1, `${formatLogInfo(info)}${os.EOL}`);
|
|
262
246
|
}
|
|
263
247
|
|
|
264
248
|
/**
|