@oh-my-pi/pi-utils 15.10.12 → 15.11.1
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/types/index.d.ts +1 -0
- package/dist/types/path-tree.d.ts +76 -0
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/path-tree.ts +147 -0
- package/src/postmortem.ts +3 -3
- package/src/prompt.ts +3 -1
package/dist/types/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./json";
|
|
|
12
12
|
export * as logger from "./logger";
|
|
13
13
|
export * from "./mermaid-ascii";
|
|
14
14
|
export * from "./mime";
|
|
15
|
+
export * from "./path-tree";
|
|
15
16
|
export * from "./peek-file";
|
|
16
17
|
export * as postmortem from "./postmortem";
|
|
17
18
|
export * as procmgr from "./procmgr";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-level path tree shared by grouped file listings (find / grep / ast tools)
|
|
3
|
+
* and compaction file-operation lists.
|
|
4
|
+
*
|
|
5
|
+
* Flat path lists used to group by the *immediate* parent directory and print the
|
|
6
|
+
* full directory path in every header. For results spread across a deep tree — or
|
|
7
|
+
* rooted outside cwd, where paths stay absolute — that repeated the shared prefix
|
|
8
|
+
* on every line. The tree below folds single-child directory chains (so the common
|
|
9
|
+
* prefix collapses into one header) and nests the rest, charging the model one
|
|
10
|
+
* token per path segment instead of one per file.
|
|
11
|
+
*/
|
|
12
|
+
/** True for `scheme://…` entries that have no meaningful directory structure. */
|
|
13
|
+
export declare function isUrlLikePath(filePath: string): boolean;
|
|
14
|
+
export interface PathTreeNode {
|
|
15
|
+
/** Direct file leaves, in first-seen order. */
|
|
16
|
+
files: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
key: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** Dedup set for `files` (a glob can surface the same path twice on retry). */
|
|
21
|
+
fileNames: Set<string>;
|
|
22
|
+
/** Child directories, in first-seen order. */
|
|
23
|
+
subdirs: Array<{
|
|
24
|
+
name: string;
|
|
25
|
+
node: PathTreeNode;
|
|
26
|
+
}>;
|
|
27
|
+
/** Dedup index for `subdirs`. */
|
|
28
|
+
dirIndex: Map<string, PathTreeNode>;
|
|
29
|
+
}
|
|
30
|
+
export interface PathTreeInput {
|
|
31
|
+
/** Path string; absolute, cwd-relative, or url-like. Backslashes are normalized. */
|
|
32
|
+
path: string;
|
|
33
|
+
/** Whether the leaf itself is a directory (trailing-slash match from find). */
|
|
34
|
+
isDir: boolean;
|
|
35
|
+
/** Opaque key carried onto file events for section lookup. Defaults to `path`. */
|
|
36
|
+
key?: string;
|
|
37
|
+
}
|
|
38
|
+
/** One node emitted while walking the tree: a folded directory or a file leaf. */
|
|
39
|
+
export interface GroupedTreeEvent {
|
|
40
|
+
kind: "dir" | "file";
|
|
41
|
+
/** 0-based nesting depth (root children are depth 0). */
|
|
42
|
+
depth: number;
|
|
43
|
+
/** Folded chain for dirs (e.g. `a/b/c`, no trailing slash); basename for files. */
|
|
44
|
+
name: string;
|
|
45
|
+
/** File key for `kind === "file"`; empty string for directories. */
|
|
46
|
+
key: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a directory tree from a flat list of paths. URL-like entries are kept
|
|
50
|
+
* whole as root-level file leaves (they have no meaningful directory structure).
|
|
51
|
+
* Absolute paths carry a leading empty segment so they share a common `/` root
|
|
52
|
+
* and fold like any other prefix.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildPathTree(entries: Iterable<PathTreeInput>): PathTreeNode;
|
|
55
|
+
/**
|
|
56
|
+
* Depth-first walk yielding directory and file events. Directories collapse their
|
|
57
|
+
* single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
|
|
58
|
+
* header. Each node's direct files are emitted before its subdirectories, keeping
|
|
59
|
+
* a file unambiguously attached to the header above it.
|
|
60
|
+
*/
|
|
61
|
+
export declare function walkPathTree(node: PathTreeNode, depth?: number): Generator<GroupedTreeEvent>;
|
|
62
|
+
/**
|
|
63
|
+
* Render a flat path list as a grouped, prefix-folded directory tree without
|
|
64
|
+
* per-file bodies (find-tool output shape, also used by compaction `<files>`
|
|
65
|
+
* lists). Single-child directory chains fold into one header (`# a/b/c/`),
|
|
66
|
+
* each level adds one `#`, and files are listed bare under the deepest
|
|
67
|
+
* directory header that owns them. Trailing-slash entries are directory
|
|
68
|
+
* leaves and keep their slash in the header.
|
|
69
|
+
*
|
|
70
|
+
* `annotate` receives each file's full original path and its return value is
|
|
71
|
+
* appended verbatim to the file line (e.g. ` (RW)`).
|
|
72
|
+
*
|
|
73
|
+
* Order follows the input: a directory appears when its first member is
|
|
74
|
+
* emitted, and a node's own files precede its subdirectories.
|
|
75
|
+
*/
|
|
76
|
+
export declare function formatGroupedPaths(paths: readonly string[], annotate?: (path: string) => string): string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-utils",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.11.1",
|
|
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": "15.
|
|
34
|
+
"@oh-my-pi/pi-natives": "15.11.1",
|
|
35
35
|
"beautiful-mermaid": "^1.1.3",
|
|
36
36
|
"handlebars": "^4.7.9",
|
|
37
37
|
"winston": "^3.19.0",
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./json";
|
|
|
12
12
|
export * as logger from "./logger";
|
|
13
13
|
export * from "./mermaid-ascii";
|
|
14
14
|
export * from "./mime";
|
|
15
|
+
export * from "./path-tree";
|
|
15
16
|
export * from "./peek-file";
|
|
16
17
|
export * as postmortem from "./postmortem";
|
|
17
18
|
export * as procmgr from "./procmgr";
|
package/src/path-tree.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-level path tree shared by grouped file listings (find / grep / ast tools)
|
|
3
|
+
* and compaction file-operation lists.
|
|
4
|
+
*
|
|
5
|
+
* Flat path lists used to group by the *immediate* parent directory and print the
|
|
6
|
+
* full directory path in every header. For results spread across a deep tree — or
|
|
7
|
+
* rooted outside cwd, where paths stay absolute — that repeated the shared prefix
|
|
8
|
+
* on every line. The tree below folds single-child directory chains (so the common
|
|
9
|
+
* prefix collapses into one header) and nests the rest, charging the model one
|
|
10
|
+
* token per path segment instead of one per file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const URL_LIKE_PATH_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
14
|
+
|
|
15
|
+
/** True for `scheme://…` entries that have no meaningful directory structure. */
|
|
16
|
+
export function isUrlLikePath(filePath: string): boolean {
|
|
17
|
+
return URL_LIKE_PATH_RE.test(filePath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PathTreeNode {
|
|
21
|
+
/** Direct file leaves, in first-seen order. */
|
|
22
|
+
files: Array<{ name: string; key: string }>;
|
|
23
|
+
/** Dedup set for `files` (a glob can surface the same path twice on retry). */
|
|
24
|
+
fileNames: Set<string>;
|
|
25
|
+
/** Child directories, in first-seen order. */
|
|
26
|
+
subdirs: Array<{ name: string; node: PathTreeNode }>;
|
|
27
|
+
/** Dedup index for `subdirs`. */
|
|
28
|
+
dirIndex: Map<string, PathTreeNode>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PathTreeInput {
|
|
32
|
+
/** Path string; absolute, cwd-relative, or url-like. Backslashes are normalized. */
|
|
33
|
+
path: string;
|
|
34
|
+
/** Whether the leaf itself is a directory (trailing-slash match from find). */
|
|
35
|
+
isDir: boolean;
|
|
36
|
+
/** Opaque key carried onto file events for section lookup. Defaults to `path`. */
|
|
37
|
+
key?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One node emitted while walking the tree: a folded directory or a file leaf. */
|
|
41
|
+
export interface GroupedTreeEvent {
|
|
42
|
+
kind: "dir" | "file";
|
|
43
|
+
/** 0-based nesting depth (root children are depth 0). */
|
|
44
|
+
depth: number;
|
|
45
|
+
/** Folded chain for dirs (e.g. `a/b/c`, no trailing slash); basename for files. */
|
|
46
|
+
name: string;
|
|
47
|
+
/** File key for `kind === "file"`; empty string for directories. */
|
|
48
|
+
key: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createNode(): PathTreeNode {
|
|
52
|
+
return { files: [], fileNames: new Set(), subdirs: [], dirIndex: new Map() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addFile(node: PathTreeNode, name: string, key: string): void {
|
|
56
|
+
if (node.fileNames.has(name)) return;
|
|
57
|
+
node.fileNames.add(name);
|
|
58
|
+
node.files.push({ name, key });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build a directory tree from a flat list of paths. URL-like entries are kept
|
|
63
|
+
* whole as root-level file leaves (they have no meaningful directory structure).
|
|
64
|
+
* Absolute paths carry a leading empty segment so they share a common `/` root
|
|
65
|
+
* and fold like any other prefix.
|
|
66
|
+
*/
|
|
67
|
+
export function buildPathTree(entries: Iterable<PathTreeInput>): PathTreeNode {
|
|
68
|
+
const root = createNode();
|
|
69
|
+
for (const { path: rawPath, isDir, key } of entries) {
|
|
70
|
+
const normalized = rawPath.replace(/\\/g, "/");
|
|
71
|
+
const fileKey = key ?? rawPath;
|
|
72
|
+
if (isUrlLikePath(normalized)) {
|
|
73
|
+
addFile(root, normalized, fileKey);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const trimmed = normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
77
|
+
if (trimmed.length === 0) continue;
|
|
78
|
+
const segments = trimmed.split("/");
|
|
79
|
+
const dirCount = isDir ? segments.length : segments.length - 1;
|
|
80
|
+
let node = root;
|
|
81
|
+
for (let i = 0; i < dirCount; i++) {
|
|
82
|
+
const segment = segments[i]!;
|
|
83
|
+
let child = node.dirIndex.get(segment);
|
|
84
|
+
if (!child) {
|
|
85
|
+
child = createNode();
|
|
86
|
+
node.dirIndex.set(segment, child);
|
|
87
|
+
node.subdirs.push({ name: segment, node: child });
|
|
88
|
+
}
|
|
89
|
+
node = child;
|
|
90
|
+
}
|
|
91
|
+
if (!isDir) {
|
|
92
|
+
addFile(node, segments[segments.length - 1]!, fileKey);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return root;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Depth-first walk yielding directory and file events. Directories collapse their
|
|
100
|
+
* single-child chains (`a` → `a/b` → `a/b/c`) so a shared prefix becomes one
|
|
101
|
+
* header. Each node's direct files are emitted before its subdirectories, keeping
|
|
102
|
+
* a file unambiguously attached to the header above it.
|
|
103
|
+
*/
|
|
104
|
+
export function* walkPathTree(node: PathTreeNode, depth = 0): Generator<GroupedTreeEvent> {
|
|
105
|
+
for (const file of node.files) {
|
|
106
|
+
yield { kind: "file", depth, name: file.name, key: file.key };
|
|
107
|
+
}
|
|
108
|
+
for (const subdir of node.subdirs) {
|
|
109
|
+
let dirNode = subdir.node;
|
|
110
|
+
const parts = [subdir.name];
|
|
111
|
+
while (dirNode.files.length === 0 && dirNode.subdirs.length === 1) {
|
|
112
|
+
const only = dirNode.subdirs[0]!;
|
|
113
|
+
parts.push(only.name);
|
|
114
|
+
dirNode = only.node;
|
|
115
|
+
}
|
|
116
|
+
yield { kind: "dir", depth, name: parts.join("/"), key: "" };
|
|
117
|
+
yield* walkPathTree(dirNode, depth + 1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Render a flat path list as a grouped, prefix-folded directory tree without
|
|
123
|
+
* per-file bodies (find-tool output shape, also used by compaction `<files>`
|
|
124
|
+
* lists). Single-child directory chains fold into one header (`# a/b/c/`),
|
|
125
|
+
* each level adds one `#`, and files are listed bare under the deepest
|
|
126
|
+
* directory header that owns them. Trailing-slash entries are directory
|
|
127
|
+
* leaves and keep their slash in the header.
|
|
128
|
+
*
|
|
129
|
+
* `annotate` receives each file's full original path and its return value is
|
|
130
|
+
* appended verbatim to the file line (e.g. ` (RW)`).
|
|
131
|
+
*
|
|
132
|
+
* Order follows the input: a directory appears when its first member is
|
|
133
|
+
* emitted, and a node's own files precede its subdirectories.
|
|
134
|
+
*/
|
|
135
|
+
export function formatGroupedPaths(paths: readonly string[], annotate?: (path: string) => string): string {
|
|
136
|
+
if (paths.length === 0) return "";
|
|
137
|
+
const tree = buildPathTree(paths.map(entry => ({ path: entry, isDir: entry.endsWith("/") })));
|
|
138
|
+
const lines: string[] = [];
|
|
139
|
+
for (const event of walkPathTree(tree)) {
|
|
140
|
+
if (event.kind === "dir") {
|
|
141
|
+
lines.push(`${"#".repeat(event.depth + 1)} ${event.name}/`);
|
|
142
|
+
} else {
|
|
143
|
+
lines.push(annotate ? `${event.name}${annotate(event.key)}` : event.name);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
package/src/postmortem.ts
CHANGED
|
@@ -38,7 +38,6 @@ function runCleanup(reason: Reason): Promise<void> {
|
|
|
38
38
|
cleanupStage = "running";
|
|
39
39
|
break;
|
|
40
40
|
case "running":
|
|
41
|
-
logger.error("Cleanup invoked recursively", { stack: new Error().stack });
|
|
42
41
|
return Promise.resolve();
|
|
43
42
|
case "complete":
|
|
44
43
|
return Promise.resolve();
|
|
@@ -150,8 +149,9 @@ export function register(id: string, callback: (reason: Reason) => void | Promis
|
|
|
150
149
|
};
|
|
151
150
|
|
|
152
151
|
if (cleanupStage !== "idle") {
|
|
153
|
-
//
|
|
154
|
-
|
|
152
|
+
// Cleanup is already in progress or complete; run late registrations once
|
|
153
|
+
// without re-entering the global cleanup pass.
|
|
154
|
+
logger.debug("Cleanup already started; running late callback once", { id });
|
|
155
155
|
try {
|
|
156
156
|
callback(Reason.MANUAL);
|
|
157
157
|
} catch (e) {
|
package/src/prompt.ts
CHANGED
|
@@ -342,10 +342,12 @@ handlebars.registerHelper(
|
|
|
342
342
|
/**
|
|
343
343
|
* {{join array ", "}}
|
|
344
344
|
* Joins an array with a separator (default: ", ").
|
|
345
|
+
* Note: Use \n/\t in the separator for newlines/tabs (unescaped automatically,
|
|
346
|
+
* same convention as {{#list}} — Handlebars string literals carry no escapes).
|
|
345
347
|
*/
|
|
346
348
|
handlebars.registerHelper("join", (context: unknown[], separator?: unknown): string => {
|
|
347
349
|
if (!Array.isArray(context)) return "";
|
|
348
|
-
const sep = typeof separator === "string" ? separator : ", ";
|
|
350
|
+
const sep = typeof separator === "string" ? separator.replace(/\\n/g, "\n").replace(/\\t/g, "\t") : ", ";
|
|
349
351
|
return context.join(sep);
|
|
350
352
|
});
|
|
351
353
|
|