@akira-tl/forgerelay 0.1.1 → 0.2.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/CHANGELOG.md +37 -0
- package/README.md +51 -6
- package/dist/apply-patch.js +21 -6
- package/dist/artifact-tools.js +35 -16
- package/dist/cli.js +46 -3
- package/dist/config.js +10 -6
- package/dist/db/migrations.js +8 -0
- package/dist/db/schema.js +1 -0
- package/dist/hook-cli.js +100 -0
- package/dist/hooks.js +545 -0
- package/dist/local-agent-store.js +14 -1
- package/dist/logger.js +157 -15
- package/dist/mcp/server-instructions.js +6 -5
- package/dist/pi-tools.js +14 -13
- package/dist/process-platform.js +1 -0
- package/dist/roots.js +30 -1
- package/dist/server.js +586 -446
- package/dist/user-config.js +33 -1
- package/dist/workspaces.js +64 -7
- package/docs/configuration.md +138 -5
- package/docs/debugging.md +127 -0
- package/docs/roadmap.md +16 -21
- package/docs/security.md +32 -7
- package/package.json +5 -3
- package/scripts/debug/accept.mjs +649 -0
- package/scripts/debug/config.json +40 -0
- package/scripts/debug/hook-recorder.mjs +35 -0
- package/scripts/debug/runtime.mjs +47 -0
- package/scripts/debug/serve.mjs +37 -0
- package/scripts/dev-server.mjs +1 -1
package/dist/logger.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { styleText } from "node:util";
|
|
1
3
|
const LEVEL_WEIGHT = {
|
|
2
4
|
silent: 0,
|
|
3
5
|
error: 1,
|
|
@@ -5,6 +7,12 @@ const LEVEL_WEIGHT = {
|
|
|
5
7
|
info: 3,
|
|
6
8
|
debug: 4,
|
|
7
9
|
};
|
|
10
|
+
const LEVEL_STYLE = {
|
|
11
|
+
error: "red",
|
|
12
|
+
warn: "yellow",
|
|
13
|
+
info: "green",
|
|
14
|
+
debug: "gray",
|
|
15
|
+
};
|
|
8
16
|
export function shouldLog(config, level) {
|
|
9
17
|
return LEVEL_WEIGHT[config.level] >= LEVEL_WEIGHT[level];
|
|
10
18
|
}
|
|
@@ -17,7 +25,10 @@ export function logEvent(config, level, event, fields = {}) {
|
|
|
17
25
|
event,
|
|
18
26
|
...fields,
|
|
19
27
|
};
|
|
20
|
-
const
|
|
28
|
+
const stream = level === "error" || level === "warn" ? process.stderr : process.stdout;
|
|
29
|
+
const line = config.format === "pretty"
|
|
30
|
+
? formatPrettyLogEntry(entry, { colorize: true, stream })
|
|
31
|
+
: JSON.stringify(entry);
|
|
21
32
|
if (level === "error") {
|
|
22
33
|
console.error(line);
|
|
23
34
|
}
|
|
@@ -45,25 +56,156 @@ export function requestPath(req) {
|
|
|
45
56
|
export function sessionIdPrefix(sessionId) {
|
|
46
57
|
return sessionId ? sessionId.slice(0, 8) : undefined;
|
|
47
58
|
}
|
|
59
|
+
export function workspaceLogLabel(root, workspaceId) {
|
|
60
|
+
const shortWorkspaceId = workspaceId.startsWith("ws_")
|
|
61
|
+
? `ws_${workspaceId.slice(3, 11)}`
|
|
62
|
+
: workspaceId.slice(0, 8);
|
|
63
|
+
return `${basename(root)}/${shortWorkspaceId}`;
|
|
64
|
+
}
|
|
48
65
|
export function commandPreview(command) {
|
|
49
66
|
const normalized = command.replace(/\s+/g, " ").trim();
|
|
50
67
|
return normalized.length > 120 ? `${normalized.slice(0, 117)}...` : normalized;
|
|
51
68
|
}
|
|
69
|
+
export function formatPrettyLogEntry(entry, options = {}) {
|
|
70
|
+
const level = logLevel(entry.level);
|
|
71
|
+
const time = formatTimestamp(entry.ts);
|
|
72
|
+
const source = stringField(entry.workspace) ?? stringField(entry.workspaceId) ?? "forgerelay";
|
|
73
|
+
const session = stringField(entry.session) ?? stringField(entry.sessionIdPrefix);
|
|
74
|
+
const prefix = [
|
|
75
|
+
style("gray", time, options),
|
|
76
|
+
`[${style(LEVEL_STYLE[level], level.toUpperCase(), options)}]`,
|
|
77
|
+
style(["cyan", "underline"], source, options),
|
|
78
|
+
session ? style("gray", `session:${session}`, options) : undefined,
|
|
79
|
+
style("gray", "|", options),
|
|
80
|
+
].filter((value) => Boolean(value)).join(" ");
|
|
81
|
+
return `${prefix} ${formatPrettyMessage(entry, options)}`;
|
|
82
|
+
}
|
|
52
83
|
function firstHeaderValue(value) {
|
|
53
84
|
return value?.split(",")[0]?.trim() || undefined;
|
|
54
85
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
function formatPrettyMessage(entry, options) {
|
|
87
|
+
switch (String(entry.event)) {
|
|
88
|
+
case "tool_call":
|
|
89
|
+
case "artifact_tool_call":
|
|
90
|
+
return formatToolMessage(entry, options);
|
|
91
|
+
case "hook_call":
|
|
92
|
+
return formatHookMessage(entry, options);
|
|
93
|
+
case "http_request":
|
|
94
|
+
return formatHttpMessage(entry, options);
|
|
95
|
+
case "mcp_session_created":
|
|
96
|
+
return `session ${stringField(entry.sessionIdPrefix) ?? "unknown"} created`;
|
|
97
|
+
case "mcp_session_closed":
|
|
98
|
+
return `session ${stringField(entry.sessionIdPrefix) ?? "unknown"} closed`;
|
|
99
|
+
case "mcp_session_close_failed":
|
|
100
|
+
return `session ${stringField(entry.sessionIdPrefix) ?? "unknown"} close -> ${style("red", "error", options)}`;
|
|
101
|
+
case "auth_denied":
|
|
102
|
+
return `auth denied${entry.reason ? `: ${String(entry.reason)}` : ""}`;
|
|
103
|
+
case "mcp_request_error":
|
|
104
|
+
return `mcp request -> ${style("red", `error${entry.error ? `: ${String(entry.error)}` : ""}`, options)}`;
|
|
105
|
+
default:
|
|
106
|
+
return formatGenericMessage(entry);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function formatToolMessage(entry, options) {
|
|
110
|
+
const tool = stringField(entry.tool) ?? String(entry.event ?? "tool");
|
|
111
|
+
const target = toolTarget(entry, tool);
|
|
112
|
+
const operation = target ? `${tool} ${target}` : tool;
|
|
113
|
+
const result = toolResult(entry, tool, options);
|
|
114
|
+
return `${operation} -> ${result}`;
|
|
115
|
+
}
|
|
116
|
+
function toolTarget(entry, tool) {
|
|
117
|
+
if (isShellTool(tool)) {
|
|
118
|
+
return stringField(entry.commandPreview) ?? stringField(entry.workingDirectory);
|
|
119
|
+
}
|
|
120
|
+
return stringField(entry.path) ?? stringField(entry.workingDirectory);
|
|
121
|
+
}
|
|
122
|
+
function toolResult(entry, tool, options) {
|
|
123
|
+
if (entry.running === true) {
|
|
124
|
+
const processSessionId = entry.processSessionId;
|
|
125
|
+
return style("yellow", processSessionId === undefined ? "running" : `running process:${String(processSessionId)}`, options);
|
|
126
|
+
}
|
|
127
|
+
const exitCode = numberField(entry.exitCode) ?? exitCodeFromError(entry.error);
|
|
128
|
+
if (isShellTool(tool)) {
|
|
129
|
+
if (exitCode !== undefined) {
|
|
130
|
+
return style(exitCode === 0 ? "green" : "red", `exit=${exitCode}`, options);
|
|
131
|
+
}
|
|
132
|
+
if (entry.success === true)
|
|
133
|
+
return style("green", "exit=0", options);
|
|
134
|
+
}
|
|
135
|
+
if (entry.success === false) {
|
|
136
|
+
const error = stringField(entry.error);
|
|
137
|
+
return style("red", error ? `error: ${error}` : "error", options);
|
|
138
|
+
}
|
|
139
|
+
if (entry.success === true)
|
|
140
|
+
return style("green", "ok", options);
|
|
141
|
+
return "done";
|
|
142
|
+
}
|
|
143
|
+
function formatHookMessage(entry, options) {
|
|
144
|
+
const name = stringField(entry.hookName) ?? "hook";
|
|
145
|
+
const event = stringField(entry.hookEvent);
|
|
146
|
+
const operation = event ? `hook ${name} ${event}` : `hook ${name}`;
|
|
147
|
+
const exitCode = exitCodeFromHookError(entry.error);
|
|
148
|
+
if (entry.success === false) {
|
|
149
|
+
if (exitCode !== undefined)
|
|
150
|
+
return `${operation} -> ${style("red", `exit=${exitCode}`, options)}`;
|
|
151
|
+
const error = stringField(entry.error);
|
|
152
|
+
return `${operation} -> ${style("red", error ? `error: ${error}` : "error", options)}`;
|
|
153
|
+
}
|
|
154
|
+
return `${operation} -> ${style("green", "exit=0", options)}`;
|
|
155
|
+
}
|
|
156
|
+
function formatHttpMessage(entry, options) {
|
|
157
|
+
const method = stringField(entry.method) ?? "HTTP";
|
|
158
|
+
const path = stringField(entry.path) ?? "/";
|
|
159
|
+
const status = numberField(entry.status);
|
|
160
|
+
const statusText = status === undefined ? "done" : String(status);
|
|
161
|
+
const statusStyle = status !== undefined && status >= 400 ? "red" : "green";
|
|
162
|
+
return `http ${method} ${path} -> ${style(statusStyle, statusText, options)}`;
|
|
163
|
+
}
|
|
164
|
+
function formatGenericMessage(entry) {
|
|
165
|
+
const event = String(entry.event ?? "log");
|
|
166
|
+
const detail = [entry.reason, entry.error]
|
|
167
|
+
.map((value) => stringField(value))
|
|
168
|
+
.find((value) => value !== undefined);
|
|
169
|
+
return detail ? `${event}: ${detail}` : event;
|
|
170
|
+
}
|
|
171
|
+
function formatTimestamp(value) {
|
|
172
|
+
const date = new Date(String(value));
|
|
173
|
+
if (Number.isNaN(date.getTime()))
|
|
174
|
+
return String(value ?? "");
|
|
175
|
+
const two = (part) => String(part).padStart(2, "0");
|
|
176
|
+
return `${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}:${two(date.getSeconds())}`;
|
|
177
|
+
}
|
|
178
|
+
function logLevel(value) {
|
|
179
|
+
return value === "error" || value === "warn" || value === "debug" ? value : "info";
|
|
180
|
+
}
|
|
181
|
+
function stringField(value) {
|
|
182
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
183
|
+
}
|
|
184
|
+
function numberField(value) {
|
|
185
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
186
|
+
}
|
|
187
|
+
function exitCodeFromError(value) {
|
|
188
|
+
const error = stringField(value);
|
|
189
|
+
if (!error)
|
|
190
|
+
return undefined;
|
|
191
|
+
const match = error.match(/Command exited with code (-?\d+)/i);
|
|
192
|
+
return match ? Number(match[1]) : undefined;
|
|
193
|
+
}
|
|
194
|
+
function exitCodeFromHookError(value) {
|
|
195
|
+
const error = stringField(value);
|
|
196
|
+
if (!error)
|
|
197
|
+
return undefined;
|
|
198
|
+
const match = error.match(/exited with code (-?\d+)/i);
|
|
199
|
+
return match ? Number(match[1]) : undefined;
|
|
200
|
+
}
|
|
201
|
+
function isShellTool(tool) {
|
|
202
|
+
return tool === "bash" || tool === "exec_command" || tool === "write_stdin";
|
|
203
|
+
}
|
|
204
|
+
function style(format, text, options) {
|
|
205
|
+
if (options.colorize !== true)
|
|
206
|
+
return text;
|
|
207
|
+
return styleText(format, text, {
|
|
208
|
+
validateStream: options.validateStream ?? true,
|
|
209
|
+
stream: options.stream ?? process.stdout,
|
|
210
|
+
});
|
|
69
211
|
}
|
|
@@ -20,10 +20,10 @@ export function buildToolDescriptions(config) {
|
|
|
20
20
|
? ` In minimal tool mode, ${toolNames.grep}, ${toolNames.glob}, and ${toolNames.ls} are disabled, so shell commands may be used for equivalent search and directory inspection.`
|
|
21
21
|
: "";
|
|
22
22
|
return {
|
|
23
|
-
read: `Read a file inside an open workspace. Instruction files returned by ${toolNames.openWorkspace} and advertised skill files are also readable when applicable.${skillCapability} Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
24
|
-
write: `Create or completely overwrite a file inside an open workspace.
|
|
25
|
-
edit: `Edit one file inside an open workspace by replacing exact text blocks. Each oldText must match a unique, non-overlapping region of the original file. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
26
|
-
applyPatch: `Apply one Codex-style patch inside an open workspace. Supports adding, overwriting, updating, deleting, and moving files.
|
|
23
|
+
read: `Read a file inside an open workspace or the OS temp directory. Instruction files returned by ${toolNames.openWorkspace} and advertised skill files are also readable when applicable.${skillCapability} Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
24
|
+
write: `Create or completely overwrite a file inside an open workspace or the OS temp directory. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
25
|
+
edit: `Edit one file inside an open workspace or the OS temp directory by replacing exact text blocks. Each oldText must match a unique, non-overlapping region of the original file. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
26
|
+
applyPatch: `Apply one Codex-style patch inside an open workspace or the OS temp directory. Supports adding, overwriting, updating, deleting, and moving files. Workspace paths must remain relative; absolute paths are accepted only inside the OS temp directory. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
27
27
|
shell: `Run a shell command inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. Do not use ${toolNames.shell} to create or modify project files; use ${toolNames.edit} or ${toolNames.write} for file changes. Call ${toolNames.openWorkspace} first and pass workspaceId. This capability should only be exposed behind strong authentication.`,
|
|
28
28
|
shellCommand: "Shell command to run with the local user's authority.",
|
|
29
29
|
};
|
|
@@ -37,13 +37,14 @@ function capabilityContractInstructions(config, context) {
|
|
|
37
37
|
? `When ${toolNames.openWorkspace} returns available skills and a task matches a skill, use ${toolNames.read} to read that skill's path before proceeding. Skill paths may be outside the workspace, but ${toolNames.read} only permits advertised SKILL.md files and files under already-loaded skill directories.`
|
|
38
38
|
: "";
|
|
39
39
|
const toolSurface = toolSurfaceInstructions(config);
|
|
40
|
+
const hooks = "When a ForgeRelay tool result reports Hook results, tell the user which meaningful hooks ran and whether they passed or blocked the operation. Do not claim the requested operation succeeded when a blocking hook prevented it.";
|
|
40
41
|
const artifact = config.artifactsEnabled && context.artifactDownloadSupported
|
|
41
42
|
? "When the user supplies or generates a file that is not present on the ForgeRelay host, use download_artifact with its native file value, the existing workspace ID, and a suitable relative destination path chosen from the user's request and project structure. The tool refuses to overwrite an existing destination and returns the normalized workspace-relative path. Use normal workspace tools when explicit inspection, replacement, movement, renaming, or deletion is needed. Do not recreate binary files with write/edit calls or place signed URLs, native file objects, base64 content, or invented host paths in shell commands or logs."
|
|
42
43
|
: "";
|
|
43
44
|
const showChanges = config.widgets === "changes"
|
|
44
45
|
? "If the turn successfully modifies files by creating, editing, overwriting, deleting, moving, or applying patches, call show_changes exactly once for that workspace after the final related file change and before your final response so the user can inspect the aggregate diff for that turn. Do not call it after every individual file change; do not skip it because individual file-change tools already returned diffs."
|
|
45
46
|
: "";
|
|
46
|
-
return joinInstructions(workspaceLifecycle, agents, skills, toolSurface, artifact, showChanges);
|
|
47
|
+
return joinInstructions(workspaceLifecycle, agents, skills, toolSurface, hooks, artifact, showChanges);
|
|
47
48
|
}
|
|
48
49
|
function toolSurfaceInstructions(config) {
|
|
49
50
|
if (config.toolMode === "codex") {
|
package/dist/pi-tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveCanonicalAllowedPath } from "./roots.js";
|
|
3
3
|
function toMcpContent(result) {
|
|
4
4
|
return result.content.map((content) => {
|
|
5
5
|
if (content.type === "text") {
|
|
@@ -29,7 +29,7 @@ async function runTool(execute, input, context) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
export async function readFileTool(input, context) {
|
|
32
|
-
const path =
|
|
32
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.readRoots ?? [context.root]);
|
|
33
33
|
const tool = createReadTool(context.cwd);
|
|
34
34
|
return runTool((params) => tool.execute("read_file", params), {
|
|
35
35
|
path,
|
|
@@ -38,7 +38,7 @@ export async function readFileTool(input, context) {
|
|
|
38
38
|
}, context);
|
|
39
39
|
}
|
|
40
40
|
export async function writeFileTool(input, context) {
|
|
41
|
-
const path =
|
|
41
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.fileRoots ?? [context.root]);
|
|
42
42
|
const tool = createWriteTool(context.cwd);
|
|
43
43
|
return runTool((params) => tool.execute("write_file", params), {
|
|
44
44
|
path,
|
|
@@ -46,7 +46,7 @@ export async function writeFileTool(input, context) {
|
|
|
46
46
|
}, context);
|
|
47
47
|
}
|
|
48
48
|
export async function editFileTool(input, context) {
|
|
49
|
-
const path =
|
|
49
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.fileRoots ?? [context.root]);
|
|
50
50
|
const tool = createEditTool(context.cwd);
|
|
51
51
|
return runTool((params) => tool.execute("edit_file", params), {
|
|
52
52
|
path,
|
|
@@ -54,22 +54,23 @@ export async function editFileTool(input, context) {
|
|
|
54
54
|
}, context);
|
|
55
55
|
}
|
|
56
56
|
export async function grepFilesTool(input, context) {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const path = input.path
|
|
58
|
+
? await resolveCanonicalAllowedPath(input.path, context.cwd, context.fileRoots ?? [context.root])
|
|
59
|
+
: undefined;
|
|
59
60
|
const tool = createGrepTool(context.cwd);
|
|
60
|
-
return runTool((params) => tool.execute("grep_files", params), input, context);
|
|
61
|
+
return runTool((params) => tool.execute("grep_files", params), { ...input, path }, context);
|
|
61
62
|
}
|
|
62
63
|
export async function findFilesTool(input, context) {
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
const path = input.path
|
|
65
|
+
? await resolveCanonicalAllowedPath(input.path, context.cwd, context.fileRoots ?? [context.root])
|
|
66
|
+
: undefined;
|
|
65
67
|
const tool = createFindTool(context.cwd);
|
|
66
|
-
return runTool((params) => tool.execute("find_files", params), input, context);
|
|
68
|
+
return runTool((params) => tool.execute("find_files", params), { ...input, path }, context);
|
|
67
69
|
}
|
|
68
70
|
export async function listDirectoryTool(input, context) {
|
|
69
|
-
|
|
70
|
-
resolveAllowedPath(input.path, context.cwd, [context.root]);
|
|
71
|
+
const path = await resolveCanonicalAllowedPath(input.path ?? ".", context.cwd, context.fileRoots ?? [context.root]);
|
|
71
72
|
const tool = createLsTool(context.cwd);
|
|
72
|
-
return runTool((params) => tool.execute("list_directory", params), input, context);
|
|
73
|
+
return runTool((params) => tool.execute("list_directory", params), { ...input, path }, context);
|
|
73
74
|
}
|
|
74
75
|
export async function runShellTool(input, context) {
|
|
75
76
|
const tool = createBashTool(context.cwd);
|
package/dist/process-platform.js
CHANGED
|
@@ -18,6 +18,7 @@ export function resolveShellCommand(command, platform = process.platform, enviro
|
|
|
18
18
|
return {
|
|
19
19
|
executable: environment.ComSpec ?? environment.COMSPEC ?? "cmd.exe",
|
|
20
20
|
args: ["/d", "/s", "/c", command],
|
|
21
|
+
windowsVerbatimArguments: true,
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
const configuredShell = environment.SHELL;
|
package/dist/roots.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
4
|
export class AccessDeniedError extends Error {
|
|
4
5
|
constructor(message) {
|
|
5
6
|
super(message);
|
|
@@ -35,3 +36,31 @@ export function resolveAllowedPath(inputPath, cwd, allowedRoots) {
|
|
|
35
36
|
const absolutePath = resolve(cwd, inputPath);
|
|
36
37
|
return assertAllowedPath(absolutePath, allowedRoots);
|
|
37
38
|
}
|
|
39
|
+
export async function resolveCanonicalAllowedPath(inputPath, cwd, allowedRoots) {
|
|
40
|
+
const absolutePath = resolveAllowedPath(inputPath, cwd, allowedRoots);
|
|
41
|
+
const canonicalPath = await canonicalizePath(absolutePath);
|
|
42
|
+
const canonicalRoots = await Promise.all(allowedRoots.map((root) => canonicalizePath(resolve(expandHomePath(root)))));
|
|
43
|
+
if (canonicalRoots.some((root) => isPathInsideRoot(canonicalPath, root))) {
|
|
44
|
+
return absolutePath;
|
|
45
|
+
}
|
|
46
|
+
throw new AccessDeniedError(`Path is outside allowed roots: ${inputPath}`);
|
|
47
|
+
}
|
|
48
|
+
async function canonicalizePath(path) {
|
|
49
|
+
const missingSegments = [];
|
|
50
|
+
let candidate = path;
|
|
51
|
+
while (true) {
|
|
52
|
+
try {
|
|
53
|
+
return resolve(await realpath(candidate), ...missingSegments.slice().reverse());
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const code = error.code;
|
|
57
|
+
if (code !== "ENOENT" && code !== "ENOTDIR")
|
|
58
|
+
throw error;
|
|
59
|
+
const parent = dirname(candidate);
|
|
60
|
+
if (parent === candidate)
|
|
61
|
+
return resolve(path);
|
|
62
|
+
missingSegments.push(basename(candidate));
|
|
63
|
+
candidate = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|