pi-monofold 0.12.3 → 0.12.5

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/README.md CHANGED
@@ -52,7 +52,7 @@ pi install -l git:github.com/eiei114/pi-monofold
52
52
  Pin a version:
53
53
 
54
54
  ```powershell
55
- pi install git:github.com/eiei114/pi-monofold@v0.12.3
55
+ pi install git:github.com/eiei114/pi-monofold@v0.12.5
56
56
  ```
57
57
 
58
58
  Try without installing:
@@ -76,7 +76,7 @@ pi install -l npm:pi-monofold
76
76
  Pin a version:
77
77
 
78
78
  ```powershell
79
- pi install npm:pi-monofold@0.12.3
79
+ pi install npm:pi-monofold@0.12.5
80
80
  ```
81
81
 
82
82
  Try without installing:
@@ -170,14 +170,19 @@ pi-monofold/
170
170
  │ ├── focus-session-state.test.ts
171
171
  │ ├── focus-skills-auto-load.test.ts
172
172
  │ ├── focus-skills.test.ts
173
+ │ ├── changelog-hygiene.test.ts
174
+ │ ├── contributing-docs.test.ts
173
175
  │ ├── github-actions-pin.test.ts
174
176
  │ ├── monofold-read-caps.test.ts
175
177
  │ ├── monofold-read-ops.test.ts
176
178
  │ ├── path-normalize.test.ts
177
179
  │ ├── readme-package-contents.test.ts
180
+ │ ├── release-docs.test.ts
178
181
  │ ├── readme-version-pin.test.ts
179
182
  │ ├── runtime-path-overlays.test.ts
180
- │ └── unknown-path-allows.test.ts
183
+ │ ├── tsconfig-publishable-files.test.ts
184
+ │ ├── unknown-path-allows.test.ts
185
+ │ └── workspace-path-lookup.test.ts
181
186
  ├── CHANGELOG.md
182
187
  ├── SECURITY.md
183
188
  ├── file-read-preview.ts
@@ -195,6 +200,7 @@ pi-monofold/
195
200
  ├── README.md
196
201
  ├── unknown-path-allows.ts
197
202
  ├── validation.ts
203
+ ├── workspace-path-lookup.ts
198
204
  └── tsconfig.json
199
205
  ```
200
206
 
@@ -238,4 +244,4 @@ Report vulnerabilities per [SECURITY.md](./SECURITY.md).
238
244
 
239
245
  ## License
240
246
 
241
- [MIT](LICENSE)
247
+ [MIT](LICENSE)
package/index.ts CHANGED
@@ -25,6 +25,12 @@ import {
25
25
  setActiveFocusPresetId,
26
26
  warnZeroTargetMatchesForPreset,
27
27
  } from "./focus-preset.js";
28
+ import {
29
+ buildPathLookupWorkspaces,
30
+ findWorkspaceForAbsolutePath,
31
+ isPathInsideWorkspace,
32
+ resolveAbsoluteTargetPath,
33
+ } from "./workspace-path-lookup.js";
28
34
  import {
29
35
  resolveWriteRouteType,
30
36
  type MonofoldRouteType,
@@ -130,6 +136,7 @@ type LoadedConfig = {
130
136
  root: string;
131
137
  raw: MultiWorkspaceConfig;
132
138
  workspaces: ResolvedWorkspace[];
139
+ pathLookupWorkspaces: ResolvedWorkspace[];
133
140
  activeRuntime: RuntimeInfo;
134
141
  };
135
142
 
@@ -278,10 +285,7 @@ function asPathOverlayMap(label: string, value: unknown): RuntimePathOverlayMap
278
285
  }
279
286
 
280
287
  function isInside(parent: string, child: string): boolean {
281
- const normalizedParent = normalizeGuardPath(parent);
282
- const normalizedChild = normalizeGuardPath(child);
283
- const relative = path.relative(normalizedParent, normalizedChild);
284
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
288
+ return isPathInsideWorkspace(parent, child);
285
289
  }
286
290
 
287
291
  function assertWorkspaceInternalRelative(label: string, value: string): void {
@@ -585,6 +589,7 @@ async function validateConfigObject(cwd: string, configPath: string, parsed: unk
585
589
  workspaces: workspaces.filter((workspace) => workspace.kind === "workspace"),
586
590
  },
587
591
  workspaces,
592
+ pathLookupWorkspaces: buildPathLookupWorkspaces(workspaces),
588
593
  activeRuntime,
589
594
  };
590
595
  }
@@ -1498,8 +1503,8 @@ function classifyPath(targetPath: string): "docs" | "code" | "unknown" {
1498
1503
  }
1499
1504
 
1500
1505
  function findWorkspaceForPath(loaded: LoadedConfig, targetPath: string): ResolvedWorkspace | undefined {
1501
- const absolute = normalizeGuardPath(path.isAbsolute(targetPath) ? targetPath : path.resolve(loaded.root, targetPath));
1502
- return [...loaded.workspaces].sort((a, b) => b.resolvedPath.length - a.resolvedPath.length).find((workspace) => isInside(workspace.resolvedPath, absolute));
1506
+ const absolute = resolveAbsoluteTargetPath(loaded.root, targetPath);
1507
+ return findWorkspaceForAbsolutePath(loaded.pathLookupWorkspaces, absolute);
1503
1508
  }
1504
1509
 
1505
1510
  async function confirm(ctx: ExtensionContext, title: string, body: string): Promise<boolean> {
@@ -51,7 +51,8 @@ export async function shallowTree(
51
51
  }
52
52
  const entries = await readdir(path.join(root, prefix), { withFileTypes: true });
53
53
  const lines: string[] = [];
54
- for (const entry of entries.filter((e) => e.name !== ".git" && e.name !== "node_modules")) {
54
+ for (const entry of entries) {
55
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
55
56
  if (budget && budget.remaining <= 0) {
56
57
  budget.truncated = true;
57
58
  break;
@@ -60,7 +61,10 @@ export async function shallowTree(
60
61
  lines.push(entry.isDirectory() ? `${rel}/` : rel);
61
62
  if (budget) budget.remaining -= 1;
62
63
  if (entry.isDirectory() && depth > 0) {
63
- lines.push(...(await shallowTree(root, depth - 1, rel, budget)));
64
+ const childLines = await shallowTree(root, depth - 1, rel, budget);
65
+ for (const childLine of childLines) {
66
+ lines.push(childLine);
67
+ }
64
68
  }
65
69
  }
66
70
  return lines;
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  },
13
13
  "description": "Pi extension that folds multiple repositories and folders into a guarded virtual monorepo for AI agents.",
14
14
  "type": "module",
15
- "version": "0.12.3",
15
+ "version": "0.12.5",
16
16
  "pi": {
17
17
  "extensions": [
18
18
  "./index.ts"
@@ -32,7 +32,8 @@
32
32
  "focus-session-state.ts",
33
33
  "monofold-read-ops.ts",
34
34
  "read-caps.ts",
35
- "validation.ts"
35
+ "validation.ts",
36
+ "workspace-path-lookup.ts"
36
37
  ],
37
38
  "name": "pi-monofold",
38
39
  "devDependencies": {
@@ -40,8 +41,8 @@
40
41
  "typebox": "^1.1.38",
41
42
  "@types/node": "^26.0.0",
42
43
  "tsx": "^4.20.5",
43
- "@earendil-works/pi-coding-agent": "^0.83.0",
44
- "@earendil-works/pi-ai": "^0.83.0"
44
+ "@earendil-works/pi-coding-agent": "^0.84.1",
45
+ "@earendil-works/pi-ai": "^0.84.2"
45
46
  },
46
47
  "dependencies": {
47
48
  "yaml": "^2.8.1"
package/path-normalize.ts CHANGED
@@ -18,8 +18,8 @@ function repairMsysPath(value: string): string {
18
18
  }
19
19
 
20
20
  /** Normalizes local paths before workspace guard comparisons. */
21
- export function normalizeGuardPath(input: string): string {
21
+ export function normalizeGuardPath(input: string, impl: Pick<typeof path, "resolve"> = path): string {
22
22
  const trimmed = input.trim();
23
23
  if (!trimmed) return trimmed;
24
- return path.resolve(repairMsysPath(trimmed));
24
+ return impl.resolve(repairMsysPath(trimmed));
25
25
  }
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import { normalizeGuardPath } from "./path-normalize.js";
3
+
4
+ type PathImpl = Pick<typeof path, "isAbsolute" | "relative" | "resolve" | "sep">;
5
+
6
+ /** Detects Windows drive-letter (`C:/...`) or UNC (`\\...`) syntax on any host. */
7
+ function isWindowsStylePath(value: string): boolean {
8
+ return /^[a-zA-Z]:[\\/]/.test(value) || value.startsWith("\\\\");
9
+ }
10
+
11
+ /** Selects the path implementation matching the given path syntax. */
12
+ function selectPathImpl(...values: string[]): PathImpl {
13
+ return values.some(isWindowsStylePath) ? path.win32 : path;
14
+ }
15
+
16
+ /** Returns true when `child` is inside or equal to `parent`. */
17
+ export function isPathInsideWorkspace(parent: string, child: string): boolean {
18
+ const impl = selectPathImpl(parent, child);
19
+ const normalizedParent = normalizeGuardPath(parent, impl);
20
+ const normalizedChild = normalizeGuardPath(child, impl);
21
+ const relative = impl.relative(normalizedParent, normalizedChild);
22
+ return (
23
+ relative === "" ||
24
+ (relative !== ".." && !relative.startsWith(`..${impl.sep}`) && !impl.isAbsolute(relative))
25
+ );
26
+ }
27
+
28
+ /** Pre-sorts workspaces once so path guards prefer the longest matching root. */
29
+ export function buildPathLookupWorkspaces<T extends { resolvedPath: string }>(workspaces: T[]): T[] {
30
+ return [...workspaces].sort((a, b) => b.resolvedPath.length - a.resolvedPath.length);
31
+ }
32
+
33
+ export function resolveAbsoluteTargetPath(root: string, targetPath: string): string {
34
+ const impl = selectPathImpl(root, targetPath);
35
+ return normalizeGuardPath(impl.isAbsolute(targetPath) ? targetPath : impl.resolve(root, targetPath), impl);
36
+ }
37
+
38
+ export function findWorkspaceForAbsolutePath<T extends { resolvedPath: string }>(
39
+ pathLookupWorkspaces: readonly T[],
40
+ absolutePath: string,
41
+ ): T | undefined {
42
+ const impl = selectPathImpl(absolutePath, ...pathLookupWorkspaces.map((workspace) => workspace.resolvedPath));
43
+ const absolute = normalizeGuardPath(absolutePath, impl);
44
+ return pathLookupWorkspaces.find((workspace) => isPathInsideWorkspace(workspace.resolvedPath, absolute));
45
+ }