@oh-my-pi/pi-utils 17.2.12 → 17.2.14

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.13] - 2026-08-11
6
+
7
+ ### Changed
8
+
9
+ - Changed stale process-log retention from the newest five files globally to one newest file per completed process and day within the current and previous four local calendar days. This preserves bounded daily diagnostic coverage while continuing to remove one-use audit files.
10
+ - Changed outbound User-Agent consumers to share the versioned `USER_AGENT` constant (`omp/<version>`).
11
+
12
+ ### Fixed
13
+
14
+ - Fixed Mermaid ASCII state pseudostates rendering empty boxes, miscoloring final-state borders, and inverting rounded corners in bottom-to-top diagrams.
15
+
5
16
  ## [17.2.11] - 2026-08-07
6
17
 
7
18
  ### Added
@@ -18,6 +18,8 @@ export declare const CONFIG_DIR_NAME: string;
18
18
  export declare const MAIN_CONFIG_FILENAMES: readonly ["config.yml", "config.yaml"];
19
19
  /** Version (e.g. "1.0.0") */
20
20
  export declare const VERSION: string;
21
+ /** Default User-Agent header string (e.g. "omp/17.2.12") */
22
+ export declare const USER_AGENT: string;
21
23
  /** Minimum Bun version */
22
24
  export declare const MIN_BUN_VERSION: string;
23
25
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-utils",
4
- "version": "17.2.12",
4
+ "version": "17.2.14",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-natives": "17.2.12"
34
+ "@oh-my-pi/pi-natives": "17.2.14"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/bun": "^1.3.14"
package/src/dirs.ts CHANGED
@@ -28,6 +28,9 @@ export const MAIN_CONFIG_FILENAMES = ["config.yml", "config.yaml"] as const;
28
28
  /** Version (e.g. "1.0.0") */
29
29
  export const VERSION: string = version;
30
30
 
31
+ /** Default User-Agent header string (e.g. "omp/17.2.12") */
32
+ export const USER_AGENT = `omp/${VERSION}`;
33
+
31
34
  /** Minimum Bun version */
32
35
  export const MIN_BUN_VERSION: string = engines.bun.replace(/[^0-9.]/g, "");
33
36
 
package/src/logger.ts CHANGED
@@ -53,9 +53,11 @@ function emitToSinks(level: LogLevel, message: string, context: Record<string, u
53
53
  }
54
54
  }
55
55
 
56
- const PROCESS_LOG_PATTERN = /^omp\.\d{4}-\d{2}-\d{2}\.(\d+)\.log(?:\.\d+)?$/;
56
+ const PROCESS_LOG_PATTERN = /^omp\.(\d{4}-\d{2}-\d{2})\.(\d+)\.log(?:\.(\d+))?$/;
57
57
  const PROCESS_AUDIT_PATTERN = /^\.omp\.(\d+)-audit\.json$/;
58
- const RETAINED_STALE_LOG_FILES = 5;
58
+ const RETAINED_STALE_LOGS_PER_PROCESS_DAY = 1;
59
+ const RETAINED_STALE_AUDIT_FILES = 0;
60
+ const RETAINED_STALE_LOG_DAYS = 5;
59
61
 
60
62
  function processIsRunning(pid: number): boolean {
61
63
  try {
@@ -67,8 +69,10 @@ function processIsRunning(pid: number): boolean {
67
69
  }
68
70
 
69
71
  /**
70
- * Retain the newest completed-process logs globally and remove their one-use
71
- * audit files. Live PID namespaces are never touched.
72
+ * Retain one newest completed-process log per process/day within the current
73
+ * and previous four local calendar days, and remove one-use audit files. Live
74
+ * PID namespaces are never touched. The calendar-day boundary preserves daily
75
+ * diagnostic coverage while bounding completed-process storage and scans.
72
76
  */
73
77
  function pruneStaleProcessLogs(dir: string): void {
74
78
  let entries: fs.Dirent[];
@@ -77,38 +81,69 @@ function pruneStaleProcessLogs(dir: string): void {
77
81
  } catch {
78
82
  return;
79
83
  }
80
-
81
- const staleLogs: Array<{ path: string; mtimeMs: number }> = [];
84
+ const current = new Date();
85
+ const currentDate =
86
+ `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-` +
87
+ String(current.getDate()).padStart(2, "0");
88
+ const cutoff = new Date(current);
89
+ cutoff.setDate(cutoff.getDate() - (RETAINED_STALE_LOG_DAYS - 1));
90
+ const cutoffDate =
91
+ `${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-` +
92
+ String(cutoff.getDate()).padStart(2, "0");
93
+
94
+ const staleLogsByProcessDay = new Map<string, Array<{ path: string; mtimeMs: number; rollover: number }>>();
82
95
  for (const entry of entries) {
83
96
  if (!entry.isFile()) continue;
84
97
  const logMatch = PROCESS_LOG_PATTERN.exec(entry.name);
85
98
  const auditMatch = PROCESS_AUDIT_PATTERN.exec(entry.name);
86
- const pidText = logMatch?.[1] ?? auditMatch?.[1];
99
+ const pidText = logMatch?.[2] ?? auditMatch?.[1];
87
100
  if (!pidText || processIsRunning(Number(pidText))) continue;
88
101
  const entryPath = path.join(dir, entry.name);
89
102
 
90
103
  if (auditMatch) {
104
+ if (RETAINED_STALE_AUDIT_FILES === 0) {
105
+ try {
106
+ fs.rmSync(entryPath, { force: true });
107
+ } catch {
108
+ // Retention is best-effort; logging must still initialize.
109
+ }
110
+ }
111
+ continue;
112
+ }
113
+ if (!logMatch?.[1]) continue;
114
+ if (logMatch[1] < cutoffDate || logMatch[1] > currentDate) {
91
115
  try {
92
116
  fs.rmSync(entryPath, { force: true });
93
117
  } catch {
94
- // Retention is best-effort; logging must still initialize.
118
+ // Another process may have pruned the same stale namespace.
95
119
  }
96
120
  continue;
97
121
  }
98
122
 
99
123
  try {
100
- staleLogs.push({ path: entryPath, mtimeMs: fs.statSync(entryPath).mtimeMs });
124
+ const key = `${pidText}:${logMatch[1]}`;
125
+ const staleLogs = staleLogsByProcessDay.get(key) ?? [];
126
+ staleLogs.push({
127
+ path: entryPath,
128
+ mtimeMs: fs.statSync(entryPath).mtimeMs,
129
+ rollover: Number(logMatch[3] ?? 0),
130
+ });
131
+ staleLogsByProcessDay.set(key, staleLogs);
101
132
  } catch {
102
133
  // Another process may have pruned the same stale namespace.
103
134
  }
104
135
  }
105
136
 
106
- staleLogs.sort((a, b) => b.mtimeMs - a.mtimeMs);
107
- for (const stale of staleLogs.slice(RETAINED_STALE_LOG_FILES)) {
108
- try {
109
- fs.rmSync(stale.path, { force: true });
110
- } catch {
111
- // Another process may have pruned the same stale namespace.
137
+ for (const staleLogs of staleLogsByProcessDay.values()) {
138
+ staleLogs.sort(
139
+ (a, b) => b.mtimeMs - a.mtimeMs || b.rollover - a.rollover || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0),
140
+ );
141
+ for (const stale of staleLogs.slice(RETAINED_STALE_LOGS_PER_PROCESS_DAY)) {
142
+ try {
143
+ fs.rmSync(stale.path, { force: true });
144
+ } catch {
145
+ // Another process may have pruned the same stale namespace.
146
+ }
112
147
  }
113
148
  }
114
149
  }
@@ -370,6 +370,8 @@ const VERTICAL_FLIP_MAP: Record<string, string> = {
370
370
  // Unicode corners
371
371
  '┌': '└', '└': '┌',
372
372
  '┐': '┘', '┘': '┐',
373
+ '╭': '╰', '╰': '╭',
374
+ '╮': '╯', '╯': '╮',
373
375
  // Unicode junctions (T-pieces flip vertically)
374
376
  '┬': '┴', '┴': '┬',
375
377
  // Box-start junctions (exit points from node boxes)
@@ -76,16 +76,18 @@ function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
76
76
  // Get corner characters for this shape type
77
77
  const corners = getCorners(node.shape, useAscii)
78
78
 
79
- // State-end uses double border to differentiate from state-start
80
- const isDoubleBox = node.shape === 'state-end'
81
- const hChar = useAscii ? (isDoubleBox ? '=' : '-') : (isDoubleBox ? '═' : '─')
82
- const vChar = useAscii ? (isDoubleBox ? '‖' : '|') : (isDoubleBox ? '║' : '│')
83
-
84
- // Double-box corners (for state-end)
85
- const doubleCorners = useAscii
79
+ const isStateStart = node.shape === 'state-start'
80
+ const isStateEnd = node.shape === 'state-end'
81
+ const hChar = useAscii ? (isStateEnd ? '=' : '-') : (isStateEnd ? '═' : '─')
82
+ const vChar = useAscii ? (isStateEnd ? '‖' : '|') : (isStateEnd ? '║' : '│')
83
+
84
+ const stateStartCorners = useAscii
85
+ ? { tl: '+', tr: '+', bl: '+', br: '+' }
86
+ : { tl: '╭', tr: '╮', bl: '╰', br: '╯' }
87
+ const stateEndCorners = useAscii
86
88
  ? { tl: '#', tr: '#', bl: '#', br: '#' }
87
89
  : { tl: '╔', tr: '╗', bl: '╚', br: '╝' }
88
- const effectiveCorners = isDoubleBox ? doubleCorners : corners
90
+ const effectiveCorners = isStateEnd ? stateEndCorners : isStateStart ? stateStartCorners : corners
89
91
 
90
92
  // Draw box border with shape-specific corners
91
93
  for (let x = from.x + 1; x < to.x; x++) box[x]![from.y] = hChar
@@ -97,8 +99,8 @@ function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
97
99
  box[from.x]![to.y] = effectiveCorners.bl
98
100
  box[to.x]![to.y] = effectiveCorners.br
99
101
 
100
- // Center the multi-line display label inside the box
101
- const label = node.displayLabel
102
+ // Pseudostates have no source label; restore their UML marker explicitly.
103
+ const label = node.displayLabel || (isStateStart ? (useAscii ? '*' : '●') : isStateEnd ? (useAscii ? '*' : '◎') : '')
102
104
  const lines = splitLines(label)
103
105
  const textCenterY = from.y + Math.floor(h / 2)
104
106
  const startY = textCenterY - Math.floor((lines.length - 1) / 2)
@@ -1223,7 +1225,8 @@ function fillRolesFromCanvases(
1223
1225
 
1224
1226
  /**
1225
1227
  * Special handling for node boxes: border chars get 'border' role, text gets 'text' role.
1226
- * Detects text by checking if character is alphanumeric or common punctuation.
1228
+ * Common final-state border characters (`#` and `=`) count only on the outer edge,
1229
+ * so identical characters inside ordinary node labels retain the text role.
1227
1230
  */
1228
1231
  function fillRolesForNodeBox(
1229
1232
  roleCanvas: RoleCanvas,
@@ -1231,7 +1234,9 @@ function fillRolesForNodeBox(
1231
1234
  offset: DrawingCoord,
1232
1235
  ): void {
1233
1236
  const isBorderChar = (c: string) => /^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|.':]$/.test(c)
1234
-
1237
+ const isStateEndBorderChar = (c: string) => /^[╔╗╚╝═║#=‖]$/.test(c)
1238
+ const maxX = canvas.length - 1
1239
+ const maxY = (canvas[0]?.length ?? 1) - 1
1235
1240
  for (let x = 0; x < canvas.length; x++) {
1236
1241
  for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
1237
1242
  const char = canvas[x]?.[y]
@@ -1240,7 +1245,9 @@ function fillRolesForNodeBox(
1240
1245
  const ry = y + offset.y
1241
1246
  // Use setRole which auto-expands the role canvas if needed
1242
1247
  if (rx >= 0 && ry >= 0) {
1243
- setRole(roleCanvas, rx, ry, isBorderChar(char) ? 'border' : 'text')
1248
+ const isOuterEdge = x === 0 || x === maxX || y === 0 || y === maxY
1249
+ const role = isBorderChar(char) || (isOuterEdge && isStateEndBorderChar(char)) ? 'border' : 'text'
1250
+ setRole(roleCanvas, rx, ry, role)
1244
1251
  }
1245
1252
  }
1246
1253
  }