@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.
Files changed (84) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/types/acp/connection.d.ts +118 -0
  3. package/dist/types/acp/protocol.d.ts +526 -0
  4. package/dist/types/acp/schema.d.ts +41 -0
  5. package/dist/types/acp/stream.d.ts +8 -0
  6. package/dist/types/acp/transport.d.ts +81 -0
  7. package/dist/types/acp.d.ts +6 -0
  8. package/dist/types/browsers.d.ts +68 -0
  9. package/dist/types/chalk.d.ts +125 -0
  10. package/dist/types/dates.d.ts +7 -0
  11. package/dist/types/docx/converter.d.ts +46 -0
  12. package/dist/types/docx/xml.d.ts +26 -0
  13. package/dist/types/docx/zip.d.ts +6 -0
  14. package/dist/types/docx.d.ts +11 -0
  15. package/dist/types/dom/core.d.ts +431 -0
  16. package/dist/types/dom/parser.d.ts +7 -0
  17. package/dist/types/dom/selector.d.ts +5 -0
  18. package/dist/types/dom.d.ts +5 -0
  19. package/dist/types/frontmatter.d.ts +21 -0
  20. package/dist/types/headers.d.ts +34 -0
  21. package/dist/types/logger/rotating-file.d.ts +18 -0
  22. package/dist/types/lru.d.ts +46 -0
  23. package/dist/types/marked/core.d.ts +445 -0
  24. package/dist/types/marked.d.ts +2 -0
  25. package/dist/types/postmortem.d.ts +14 -0
  26. package/dist/types/procmgr.d.ts +16 -0
  27. package/dist/types/prompt.d.ts +2 -2
  28. package/dist/types/readability/readability.d.ts +9 -0
  29. package/dist/types/readability/readerable.d.ts +10 -0
  30. package/dist/types/readability/types.d.ts +70 -0
  31. package/dist/types/readability.d.ts +4 -0
  32. package/dist/types/template.d.ts +62 -0
  33. package/dist/types/turndown/gfm.d.ts +11 -0
  34. package/dist/types/turndown/html.d.ts +5 -0
  35. package/dist/types/turndown/service.d.ts +21 -0
  36. package/dist/types/turndown/types.d.ts +70 -0
  37. package/dist/types/turndown.d.ts +4 -0
  38. package/dist/types/vterm/buffer.d.ts +99 -0
  39. package/dist/types/vterm/terminal.d.ts +44 -0
  40. package/dist/types/vterm.d.ts +8 -0
  41. package/dist/types/xml.d.ts +31 -0
  42. package/package.json +2 -5
  43. package/src/acp/connection.ts +344 -0
  44. package/src/acp/protocol.ts +466 -0
  45. package/src/acp/schema.ts +160 -0
  46. package/src/acp/stream.ts +82 -0
  47. package/src/acp/transport.ts +213 -0
  48. package/src/acp.ts +6 -0
  49. package/src/browsers.ts +501 -0
  50. package/src/chalk.ts +312 -0
  51. package/src/dates.ts +194 -0
  52. package/src/docx/converter.ts +681 -0
  53. package/src/docx/xml.ts +166 -0
  54. package/src/docx/zip.ts +87 -0
  55. package/src/docx.ts +20 -0
  56. package/src/dom/core.ts +1254 -0
  57. package/src/dom/parser.ts +370 -0
  58. package/src/dom/selector.ts +290 -0
  59. package/src/dom.ts +33 -0
  60. package/src/frontmatter.ts +44 -14
  61. package/src/headers.ts +167 -0
  62. package/src/logger/rotating-file.ts +149 -0
  63. package/src/logger.ts +12 -28
  64. package/src/lru.ts +185 -0
  65. package/src/marked/core.ts +1576 -0
  66. package/src/marked.ts +2 -0
  67. package/src/postmortem.ts +44 -1
  68. package/src/procmgr.ts +21 -4
  69. package/src/prompt.ts +5 -22
  70. package/src/readability/readability.ts +533 -0
  71. package/src/readability/readerable.ts +51 -0
  72. package/src/readability/types.ts +72 -0
  73. package/src/readability.ts +11 -0
  74. package/src/template.ts +586 -0
  75. package/src/turndown/gfm.ts +106 -0
  76. package/src/turndown/html.ts +257 -0
  77. package/src/turndown/service.ts +334 -0
  78. package/src/turndown/types.ts +81 -0
  79. package/src/turndown.ts +5 -0
  80. package/src/vterm/buffer.ts +218 -0
  81. package/src/vterm/terminal.ts +773 -0
  82. package/src/vterm.ts +8 -0
  83. package/src/xml.ts +313 -0
  84. package/src/winston-daily-rotate-file.d.ts +0 -6
@@ -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
- /** Recursively normalize object keys from kebab-case to camelCase */
16
- function normalizeKeys<T>(obj: T): T {
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 = normalizeKeys(v);
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 = normalizeKeys(value);
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 { location, source, fallback, normalize = true, level = "warn" } = options ?? {};
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 normalized = normalize ? stripHtmlComments(content.replace(/\r\n?/g, "\n")) : content;
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: normalizeKeys({ ...frontmatter, ...loaded }), body };
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: normalizeKeys({ ...frontmatter, ...loaded }), body };
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: normalizeKeys(frontmatter) as Record<string, unknown>, body };
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
- type FileTransport = DailyRotateFile & {
204
- log(info: Record<symbol, string>, callback: () => void): void;
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 DailyRotateFileImplementation({
213
- dirname: logsDir,
214
- filename: `omp.%DATE%.${process.pid}.log`,
215
- datePattern: "YYYY-MM-DD",
216
- maxSize: "10m",
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
- }) as FileTransport;
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: FileTransport | undefined;
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.log({ [TRANSPORT_MESSAGE]: line }, onTransportLogged);
261
- if (transports.console) process.stdout.write(`${formatLogInfo(info)}${os.EOL}`);
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
  /**