pi-monofold 0.11.1 → 0.12.0
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 +1 -0
- package/index.ts +80 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ See [docs/usage.md](./docs/usage.md) for configuration, commands, agent tools, a
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
22
22
|
- **Virtual monorepo manifest** — declare workspaces and project workspaces in `.pi/monofold.yaml`
|
|
23
|
+
- **Multi-runtime path overlays** — keep one logical workspace definition while swapping absolute repo roots per runtime
|
|
23
24
|
- **Routed Markdown writes** — route PRDs, progress notes, and other doc types to configured folders
|
|
24
25
|
- **Workspace-aware reads** — list, read, search, and tree views scoped to readable workspaces, with bounded previews by default
|
|
25
26
|
- **Capability guard** — block or confirm `read` / `write` / `edit` / `grep` / `find` / `bash` based on workspace tags
|
package/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { access, copyFile, mkdir, readFile, readdir, realpath, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { userInfo } from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import YAML from "yaml";
|
|
7
8
|
import {
|
|
@@ -69,9 +70,12 @@ type RouteConfig = {
|
|
|
69
70
|
metadata?: Record<string, unknown>;
|
|
70
71
|
};
|
|
71
72
|
|
|
73
|
+
type RuntimePathOverlayMap = Record<string, string>;
|
|
74
|
+
|
|
72
75
|
type WorkspaceConfig = {
|
|
73
76
|
name?: string;
|
|
74
77
|
path: string;
|
|
78
|
+
pathOverlays?: RuntimePathOverlayMap;
|
|
75
79
|
tags: string[];
|
|
76
80
|
capabilities: CapabilityTag[];
|
|
77
81
|
contextFiles?: string[];
|
|
@@ -111,6 +115,14 @@ type ResolvedWorkspace = WorkspaceConfig & {
|
|
|
111
115
|
normalizedRoutes: Partial<Record<RouteType, RouteConfig>>;
|
|
112
116
|
effectiveContextFiles: string[];
|
|
113
117
|
commitScope?: string;
|
|
118
|
+
runtimePathSource: "base" | "overlay";
|
|
119
|
+
runtimeResolvedPath: string;
|
|
120
|
+
activeRuntimeId: string;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type RuntimeInfo = {
|
|
124
|
+
id: string;
|
|
125
|
+
source: "env" | "detected";
|
|
114
126
|
};
|
|
115
127
|
|
|
116
128
|
type LoadedConfig = {
|
|
@@ -118,6 +130,7 @@ type LoadedConfig = {
|
|
|
118
130
|
root: string;
|
|
119
131
|
raw: MultiWorkspaceConfig;
|
|
120
132
|
workspaces: ResolvedWorkspace[];
|
|
133
|
+
activeRuntime: RuntimeInfo;
|
|
121
134
|
};
|
|
122
135
|
|
|
123
136
|
type TargetInput = {
|
|
@@ -189,9 +202,11 @@ const CODE_EXTENSIONS = new Set([
|
|
|
189
202
|
".scss",
|
|
190
203
|
".html",
|
|
191
204
|
]);
|
|
205
|
+
const WINDOWS_ABSOLUTE_PATH_RE = /^[A-Za-z]:[\/]/;
|
|
206
|
+
const UNC_ABSOLUTE_PATH_RE = /^\\[^\\]+\\[^\\]+/;
|
|
192
207
|
const ROOT_KEYS = new Set(["version", "defaults", "focusPresets", "workspaces"]);
|
|
193
208
|
const DEFAULT_KEYS = new Set(["contextFiles", "filenameTemplate", "metadata"]);
|
|
194
|
-
const WORKSPACE_KEYS = new Set(["name", "path", "tags", "capabilities", "contextFiles", "routes", "projects"]);
|
|
209
|
+
const WORKSPACE_KEYS = new Set(["name", "path", "pathOverlays", "tags", "capabilities", "contextFiles", "routes", "projects"]);
|
|
195
210
|
const PROJECT_KEYS = new Set(["name", "path", "tags", "capabilities", "contextFiles", "routes"]);
|
|
196
211
|
const ROUTE_KEYS = new Set(["path", "filenameTemplate", "metadata"]);
|
|
197
212
|
const FOCUS_STATUS_ID = "monofold-focus";
|
|
@@ -218,6 +233,50 @@ function normalizeSlashes(value: string): string {
|
|
|
218
233
|
return value.replace(/\\/g, "/");
|
|
219
234
|
}
|
|
220
235
|
|
|
236
|
+
function normalizeRuntimeId(value: string): string {
|
|
237
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "runtime";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isPortableAbsolutePath(value: string): boolean {
|
|
241
|
+
return path.isAbsolute(value) || WINDOWS_ABSOLUTE_PATH_RE.test(value) || UNC_ABSOLUTE_PATH_RE.test(value);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function resolvePortablePath(root: string, configuredPath: string): string {
|
|
245
|
+
return isPortableAbsolutePath(configuredPath) ? configuredPath : path.resolve(root, configuredPath);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function detectActiveRuntime(): RuntimeInfo {
|
|
249
|
+
const explicit = process.env.PI_MONOFOLD_RUNTIME ?? process.env.MONOFOLD_RUNTIME;
|
|
250
|
+
if (typeof explicit === "string" && explicit.trim().length > 0) {
|
|
251
|
+
return { id: normalizeRuntimeId(explicit), source: "env" };
|
|
252
|
+
}
|
|
253
|
+
const platform = process.platform === "darwin" ? "mac" : process.platform === "win32" ? "win" : process.platform;
|
|
254
|
+
let username = "user";
|
|
255
|
+
try {
|
|
256
|
+
username = userInfo().username || username;
|
|
257
|
+
} catch {
|
|
258
|
+
// ignore userInfo failures and keep fallback username
|
|
259
|
+
}
|
|
260
|
+
return { id: `${platform}-${normalizeRuntimeId(username)}`, source: "detected" };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function asPathOverlayMap(label: string, value: unknown): RuntimePathOverlayMap | undefined {
|
|
264
|
+
if (value === undefined) return undefined;
|
|
265
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
266
|
+
const normalized: RuntimePathOverlayMap = {};
|
|
267
|
+
for (const [runtimeId, overlayPath] of Object.entries(value)) {
|
|
268
|
+
const normalizedRuntimeId = normalizeRuntimeId(runtimeId);
|
|
269
|
+
if (typeof overlayPath !== "string" || overlayPath.trim().length === 0) {
|
|
270
|
+
throw new Error(`${label}.${runtimeId} must be a non-empty string path`);
|
|
271
|
+
}
|
|
272
|
+
if (!isPortableAbsolutePath(overlayPath)) {
|
|
273
|
+
throw new Error(`${label}.${runtimeId} must be an absolute path: ${overlayPath}`);
|
|
274
|
+
}
|
|
275
|
+
normalized[normalizedRuntimeId] = overlayPath;
|
|
276
|
+
}
|
|
277
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
221
280
|
function isInside(parent: string, child: string): boolean {
|
|
222
281
|
const normalizedParent = normalizeGuardPath(parent);
|
|
223
282
|
const normalizedChild = normalizeGuardPath(child);
|
|
@@ -370,6 +429,7 @@ async function validateConfigObject(cwd: string, configPath: string, parsed: unk
|
|
|
370
429
|
const defaultFilenameTemplate = typeof defaults?.filenameTemplate === "string" ? defaults.filenameTemplate : undefined;
|
|
371
430
|
const defaultMetadata = isRecord(defaults?.metadata) ? (defaults.metadata as Record<string, unknown>) : undefined;
|
|
372
431
|
const focusPresets = parseFocusPresets(parsed.focusPresets, "focusPresets");
|
|
432
|
+
const activeRuntime = detectActiveRuntime();
|
|
373
433
|
|
|
374
434
|
const workspaces: ResolvedWorkspace[] = [];
|
|
375
435
|
for (let index = 0; index < parsed.workspaces.length; index += 1) {
|
|
@@ -378,6 +438,7 @@ async function validateConfigObject(cwd: string, configPath: string, parsed: unk
|
|
|
378
438
|
assertKnownKeys(`workspaces[${index}]`, item, WORKSPACE_KEYS);
|
|
379
439
|
if (item.name !== undefined && typeof item.name !== "string") throw new Error(`workspaces[${index}].name must be string`);
|
|
380
440
|
if (typeof item.path !== "string") throw new Error(`workspaces[${index}].path is required`);
|
|
441
|
+
const pathOverlays = asPathOverlayMap(`workspaces[${index}].pathOverlays`, item.pathOverlays);
|
|
381
442
|
const tags = asStringArray(`workspaces[${index}].tags`, item.tags);
|
|
382
443
|
const capabilities = asCapabilityArray(item.capabilities);
|
|
383
444
|
const contextFiles = asStringArray(`workspaces[${index}].contextFiles`, item.contextFiles, false);
|
|
@@ -402,13 +463,19 @@ async function validateConfigObject(cwd: string, configPath: string, parsed: unk
|
|
|
402
463
|
}
|
|
403
464
|
}
|
|
404
465
|
|
|
405
|
-
const
|
|
406
|
-
const
|
|
466
|
+
const runtimeOverlayPath = pathOverlays?.[activeRuntime.id];
|
|
467
|
+
const runtimePathSource = runtimeOverlayPath ? "overlay" : "base";
|
|
468
|
+
const resolvedPath = resolvePortablePath(cwd, runtimeOverlayPath ?? item.path);
|
|
469
|
+
const realPath = await existingRealPath(
|
|
470
|
+
runtimePathSource === "overlay" ? `workspaces[${index}].pathOverlays.${activeRuntime.id}` : `workspaces[${index}].path`,
|
|
471
|
+
resolvedPath,
|
|
472
|
+
);
|
|
407
473
|
const workspace: ResolvedWorkspace = {
|
|
408
474
|
kind: "workspace",
|
|
409
475
|
targetId: `#${index}`,
|
|
410
476
|
name: item.name as string | undefined,
|
|
411
477
|
path: item.path,
|
|
478
|
+
pathOverlays,
|
|
412
479
|
displayPath: item.path,
|
|
413
480
|
tags: uniqueStrings(tags),
|
|
414
481
|
capabilities,
|
|
@@ -419,6 +486,9 @@ async function validateConfigObject(cwd: string, configPath: string, parsed: unk
|
|
|
419
486
|
realPath,
|
|
420
487
|
normalizedRoutes,
|
|
421
488
|
effectiveContextFiles: [...defaultContextFiles, ...contextFiles],
|
|
489
|
+
runtimePathSource,
|
|
490
|
+
runtimeResolvedPath: resolvedPath,
|
|
491
|
+
activeRuntimeId: activeRuntime.id,
|
|
422
492
|
};
|
|
423
493
|
workspaces.push(workspace);
|
|
424
494
|
|
|
@@ -481,6 +551,9 @@ async function validateConfigObject(cwd: string, configPath: string, parsed: unk
|
|
|
481
551
|
normalizedRoutes: projectNormalizedRoutes,
|
|
482
552
|
effectiveContextFiles: [...defaultContextFiles, ...contextFiles, ...projectContextFiles],
|
|
483
553
|
commitScope: normalizeSlashes(path.relative(resolvedPath, projectResolvedPath)),
|
|
554
|
+
runtimePathSource,
|
|
555
|
+
runtimeResolvedPath: projectResolvedPath,
|
|
556
|
+
activeRuntimeId: activeRuntime.id,
|
|
484
557
|
});
|
|
485
558
|
}
|
|
486
559
|
}
|
|
@@ -512,6 +585,7 @@ async function validateConfigObject(cwd: string, configPath: string, parsed: unk
|
|
|
512
585
|
workspaces: workspaces.filter((workspace) => workspace.kind === "workspace"),
|
|
513
586
|
},
|
|
514
587
|
workspaces,
|
|
588
|
+
activeRuntime,
|
|
515
589
|
};
|
|
516
590
|
}
|
|
517
591
|
|
|
@@ -1171,11 +1245,13 @@ async function gitRoot(workspace: ResolvedWorkspace): Promise<string | undefined
|
|
|
1171
1245
|
|
|
1172
1246
|
async function buildFullWorkspaceManifestEntry(workspace: ResolvedWorkspace, suffix = ""): Promise<string> {
|
|
1173
1247
|
const git = await gitSummary(workspace).catch((error) => ({ isGit: false, status: `git status error: ${String(error)}` }));
|
|
1248
|
+
const runtimeSuffix = workspace.runtimePathSource === "overlay" ? " (overlay)" : " (base)";
|
|
1174
1249
|
return (
|
|
1175
1250
|
`- ${formatWorkspaceLabel(workspace)}${suffix}\n` +
|
|
1176
1251
|
` capabilities: ${workspace.capabilities.join(", ")}\n` +
|
|
1177
1252
|
` routes: ${Object.keys(workspace.normalizedRoutes).join(", ") || "none"}\n` +
|
|
1178
1253
|
` contextFiles: ${workspace.effectiveContextFiles.join(", ") || "none"}\n` +
|
|
1254
|
+
` runtimePath: ${normalizeSlashes(workspace.runtimeResolvedPath)}${runtimeSuffix}\n` +
|
|
1179
1255
|
` git: ${git.isGit ? git.status : "not a git repository"}`
|
|
1180
1256
|
);
|
|
1181
1257
|
}
|
|
@@ -1186,6 +1262,7 @@ async function buildManifest(
|
|
|
1186
1262
|
): Promise<string> {
|
|
1187
1263
|
await ensureActiveFocusReady(loaded);
|
|
1188
1264
|
const lines = ["Pi Monofold Manifest:"];
|
|
1265
|
+
lines.push(`Active runtime: ${loaded.activeRuntime.id}${loaded.activeRuntime.source === "env" ? " [env]" : " [detected]"}`);
|
|
1189
1266
|
const activePreset = getActiveFocusPreset(loaded);
|
|
1190
1267
|
const position = getActiveFocusPresetPosition(loaded.raw.focusPresets);
|
|
1191
1268
|
if (activePreset && position) {
|
package/package.json
CHANGED