@momenttttt/pi-zcode-memory 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +152 -2
- package/extensions/zcode-memory.ts +476 -0
- package/package.json +46 -4
- package/src/atomic.ts +69 -0
- package/src/bridge.ts +57 -0
- package/src/config.ts +88 -0
- package/src/errors.ts +21 -0
- package/src/frontmatter.ts +136 -0
- package/src/index-content.ts +102 -0
- package/src/index-file.ts +171 -0
- package/src/injection.ts +82 -0
- package/src/list.ts +100 -0
- package/src/paths.ts +95 -0
- package/src/read.ts +175 -0
- package/src/resolve.ts +130 -0
- package/src/status.ts +111 -0
- package/src/write.ts +162 -0
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 记忆目录定位的三级回退(设计文档 §5.1):
|
|
3
|
+
*
|
|
4
|
+
* 1. 配置里的 `dir`(最高优先级,任何异常场景的兜底)
|
|
5
|
+
* 2. 按路径算法算出的目录**存在** → 用它
|
|
6
|
+
* 3. 计算路径不存在时,`memories/projects/` 下若恰好一个 `<slug>-*` 目录 → 用它并警告
|
|
7
|
+
* 都没有 → 视为"该项目尚无记忆",不创建目录
|
|
8
|
+
* 4. 第 3 步失败但存在 `project-*` 目录 → 提示目录可能是按 workspace identity 建的
|
|
9
|
+
* (slug 固定为 `project`,hash 输入不是工作区路径)
|
|
10
|
+
*
|
|
11
|
+
* 只做字符串路径判定,不做 realpath / `~` 展开:hash 的输入就是 `resolve(cwd)` 这个字符串,
|
|
12
|
+
* realpath 会把符号链接、短路径折成另一种写法,算出来的目录反而对不上。
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
15
|
+
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
computeMemoryDir,
|
|
21
|
+
computeProjectDirParts,
|
|
22
|
+
projectsRootOf,
|
|
23
|
+
resolveZcodeStorageRoot,
|
|
24
|
+
type MemoryDirInput,
|
|
25
|
+
} from "./paths.ts";
|
|
26
|
+
|
|
27
|
+
export type MemoryDirSource = "config" | "computed" | "fallback" | "none";
|
|
28
|
+
|
|
29
|
+
export interface MemoryDirResolution {
|
|
30
|
+
/** 最终使用的记忆目录;`none` 时为 undefined。 */
|
|
31
|
+
dir: string | undefined;
|
|
32
|
+
source: MemoryDirSource;
|
|
33
|
+
/** 按算法算出的目录(`memory_write` 在 `none` 时按它创建)。 */
|
|
34
|
+
computedDir: string;
|
|
35
|
+
/** `<storageRoot>/memories/projects`,回退扫描与提示用。 */
|
|
36
|
+
projectsRoot: string;
|
|
37
|
+
/** 计算路径与账户目录不一致时为什么。 */
|
|
38
|
+
warnings: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ResolveMemoryDirOptions {
|
|
42
|
+
cwd: string;
|
|
43
|
+
/** 显式配置的目录。 */
|
|
44
|
+
configDir?: string;
|
|
45
|
+
/** 覆盖 home(测试用)。默认 `homedir()`。 */
|
|
46
|
+
homeDir?: string;
|
|
47
|
+
/** 覆盖存储根(测试用);给了就不再由 homeDir 推导。 */
|
|
48
|
+
storageRoot?: string;
|
|
49
|
+
workspaceIdentity?: string;
|
|
50
|
+
platform?: NodeJS.Platform;
|
|
51
|
+
/** 注入文件系统判定(测试用)。默认真实 fs。 */
|
|
52
|
+
pathExists?: (path: string) => boolean;
|
|
53
|
+
listDirNames?: (path: string) => string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveMemoryDir(options: ResolveMemoryDirOptions): MemoryDirResolution {
|
|
57
|
+
const storageRoot =
|
|
58
|
+
options.storageRoot ?? resolveZcodeStorageRoot(options.homeDir ?? homedir(), options.platform);
|
|
59
|
+
const dirInput: MemoryDirInput = {
|
|
60
|
+
cwd: options.cwd,
|
|
61
|
+
storageRoot,
|
|
62
|
+
...(options.workspaceIdentity === undefined ? {} : { workspaceIdentity: options.workspaceIdentity }),
|
|
63
|
+
...(options.platform === undefined ? {} : { platform: options.platform }),
|
|
64
|
+
};
|
|
65
|
+
const parts = computeProjectDirParts(dirInput);
|
|
66
|
+
const projectsRoot = projectsRootOf(storageRoot, options.platform);
|
|
67
|
+
const computedDir = computeMemoryDir(dirInput);
|
|
68
|
+
const exists = options.pathExists ?? isDirectory;
|
|
69
|
+
const listNames = options.listDirNames ?? listDirectoryNames;
|
|
70
|
+
|
|
71
|
+
if (options.configDir !== undefined) {
|
|
72
|
+
// 配置是显式覆盖:不要求目录已存在(memory_write 会创建它)。
|
|
73
|
+
return { dir: options.configDir, source: "config", computedDir, projectsRoot, warnings: [] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (exists(computedDir)) {
|
|
77
|
+
return { dir: computedDir, source: "computed", computedDir, projectsRoot, warnings: [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const candidates = listNames(projectsRoot).filter((name) => name.startsWith(`${parts.slug}-`));
|
|
81
|
+
if (candidates.length === 1) {
|
|
82
|
+
const dir = join(projectsRoot, candidates[0]!, "memory");
|
|
83
|
+
return {
|
|
84
|
+
dir,
|
|
85
|
+
source: "fallback",
|
|
86
|
+
computedDir,
|
|
87
|
+
projectsRoot,
|
|
88
|
+
warnings: [
|
|
89
|
+
`The computed memory directory does not exist: ${computedDir}; using the only candidate with the same slug instead: ${dir}. ` +
|
|
90
|
+
`Different hashes on the two sides usually mean the workspace path is written differently from the one the memory directory was computed from; set dir explicitly in the config.`,
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const warnings: string[] = [];
|
|
96
|
+
if (candidates.length > 1) {
|
|
97
|
+
warnings.push(
|
|
98
|
+
`The computed memory directory does not exist, and ${projectsRoot} holds ${candidates.length} directories starting with ${parts.slug}-, so there is no way to tell which one to use.`,
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
warnings.push(`This project has no ZCode memory yet (the computed directory does not exist: ${computedDir}).`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (listNames(projectsRoot).some((name) => name.startsWith("project-"))) {
|
|
105
|
+
warnings.push(
|
|
106
|
+
"Found memory directories with the project- prefix: a workspace identity may be in use (slug fixed to project, hash input not the workspace path). " +
|
|
107
|
+
"Set dir explicitly in this package's config.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { dir: undefined, source: "none", computedDir, projectsRoot, warnings };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isDirectory(path: string): boolean {
|
|
115
|
+
try {
|
|
116
|
+
return statSync(path).isDirectory();
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function listDirectoryNames(path: string): string[] {
|
|
123
|
+
try {
|
|
124
|
+
return readdirSync(path, { withFileTypes: true })
|
|
125
|
+
.filter((entry) => entry.isDirectory())
|
|
126
|
+
.map((entry) => entry.name);
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/memory` 的状态与索引体检。
|
|
3
|
+
*
|
|
4
|
+
* 索引体检只做两件确定的事:索引行指向的文件不存在(孤儿行)、磁盘上的事实文件没出现在
|
|
5
|
+
* 索引里(未入索引)。索引可能被模型写坏(重复、失效链接),`memory_list` 走文件系统兜底。
|
|
6
|
+
*/
|
|
7
|
+
import { INDEX_CHARACTER_LIMIT, INDEX_LINE_LIMIT } from "./index-content.ts";
|
|
8
|
+
import { INDEX_FILENAME } from "./index-file.ts";
|
|
9
|
+
import type { MemoryListEntry } from "./list.ts";
|
|
10
|
+
import type { MemoryDirResolution } from "./resolve.ts";
|
|
11
|
+
|
|
12
|
+
export interface IndexHealth {
|
|
13
|
+
indexLineCount: number;
|
|
14
|
+
/** 索引里的链接目标(相对记忆目录,`/` 分隔)。 */
|
|
15
|
+
linkTargets: string[];
|
|
16
|
+
/** 指向不存在文件的索引行。 */
|
|
17
|
+
orphanIndexLines: string[];
|
|
18
|
+
/** 磁盘上有、但索引里没有链接的事实文件。 */
|
|
19
|
+
unindexedFiles: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LINK_TARGET_PATTERN = /\]\(\s*([^)\s]+)\s*\)/gu;
|
|
23
|
+
|
|
24
|
+
/** 抽出索引里的相对 `.md` 链接目标。 */
|
|
25
|
+
export function extractIndexTargets(indexContent: string): string[] {
|
|
26
|
+
const targets: string[] = [];
|
|
27
|
+
for (const match of indexContent.matchAll(LINK_TARGET_PATTERN)) {
|
|
28
|
+
const target = match[1]!.replace(/\\/gu, "/").replace(/^(?:\.\/)+/u, "");
|
|
29
|
+
if (target.startsWith("http://") || target.startsWith("https://")) continue;
|
|
30
|
+
if (!target.endsWith(".md")) continue;
|
|
31
|
+
if (target.split("/").pop() === INDEX_FILENAME) continue;
|
|
32
|
+
targets.push(target);
|
|
33
|
+
}
|
|
34
|
+
return targets;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 文件内容的"实际行数":末尾换行不算一行。 */
|
|
38
|
+
function countLines(content: string): number {
|
|
39
|
+
if (content.length === 0) return 0;
|
|
40
|
+
const lines = content.replace(/\r\n/gu, "\n").split("\n");
|
|
41
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
42
|
+
return lines.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function inspectMemoryIndex(input: {
|
|
46
|
+
indexContent: string | undefined;
|
|
47
|
+
files: readonly MemoryListEntry[];
|
|
48
|
+
}): IndexHealth {
|
|
49
|
+
const indexContent = input.indexContent ?? "";
|
|
50
|
+
const linkTargets = extractIndexTargets(indexContent);
|
|
51
|
+
const fileNames = new Set(input.files.map((entry) => entry.filename));
|
|
52
|
+
const targetNames = new Set(linkTargets);
|
|
53
|
+
return {
|
|
54
|
+
indexLineCount: countLines(indexContent),
|
|
55
|
+
linkTargets,
|
|
56
|
+
orphanIndexLines: [...new Set(linkTargets)].filter((target) => !fileNames.has(target)),
|
|
57
|
+
unindexedFiles: input.files.map((entry) => entry.filename).filter((filename) => !targetNames.has(filename)),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MemoryStatusInput {
|
|
62
|
+
resolution: MemoryDirResolution;
|
|
63
|
+
/** `MEMORY.md` 的内容;不存在/读不了时 undefined。 */
|
|
64
|
+
indexContent: string | undefined;
|
|
65
|
+
files: readonly MemoryListEntry[];
|
|
66
|
+
/** 由扩展算出的注入状态描述。 */
|
|
67
|
+
injectionLabel: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sourceLabel(source: MemoryDirResolution["source"]): string {
|
|
71
|
+
switch (source) {
|
|
72
|
+
case "config":
|
|
73
|
+
return "配置指定";
|
|
74
|
+
case "computed":
|
|
75
|
+
return "按算法计算(路径存在)";
|
|
76
|
+
case "fallback":
|
|
77
|
+
return "同 slug 唯一候选回退";
|
|
78
|
+
case "none":
|
|
79
|
+
return "尚无记忆";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function preview(values: readonly string[], limit = 10): string {
|
|
84
|
+
if (values.length === 0) return "无";
|
|
85
|
+
const shown = values.slice(0, limit).join("、");
|
|
86
|
+
return values.length > limit ? `${shown} 等 ${values.length} 项` : shown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatMemoryStatus(input: MemoryStatusInput): string {
|
|
90
|
+
const { resolution, files } = input;
|
|
91
|
+
const health = inspectMemoryIndex({ indexContent: input.indexContent, files });
|
|
92
|
+
const lines: string[] = [];
|
|
93
|
+
lines.push(`记忆目录:${resolution.dir ?? "未解析到"}`);
|
|
94
|
+
lines.push(`来源:${sourceLabel(resolution.source)}`);
|
|
95
|
+
if (resolution.dir === undefined || resolution.source !== "computed") {
|
|
96
|
+
lines.push(`计算路径:${resolution.computedDir}`);
|
|
97
|
+
}
|
|
98
|
+
lines.push(
|
|
99
|
+
input.indexContent === undefined
|
|
100
|
+
? `索引 ${INDEX_FILENAME}:不存在(注入会跳过;memory_write 会创建它)`
|
|
101
|
+
: `索引 ${INDEX_FILENAME}:${health.indexLineCount} 行(注入上限 ${INDEX_LINE_LIMIT} 行 / ${INDEX_CHARACTER_LIMIT} 字符)`,
|
|
102
|
+
);
|
|
103
|
+
lines.push(`事实文件:${files.length} 个`);
|
|
104
|
+
lines.push(
|
|
105
|
+
`索引体检:孤儿索引行 ${health.orphanIndexLines.length} 条(${preview(health.orphanIndexLines)});` +
|
|
106
|
+
`未入索引的文件 ${health.unindexedFiles.length} 个(${preview(health.unindexedFiles)})`,
|
|
107
|
+
);
|
|
108
|
+
lines.push(`索引注入:${input.injectionLabel}`);
|
|
109
|
+
for (const warning of resolution.warnings) lines.push(`注意:${warning}`);
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
package/src/write.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `memory_write` 的落盘实现。
|
|
3
|
+
*
|
|
4
|
+
* 顺序:校验 → 原子写事实文件 → 维护索引行。索引更新失败时不回滚事实文件——事实文件本身
|
|
5
|
+
* 是完整可读的,索引行下一次写入会补上;反过来(索引指了不存在的文件)更糟。
|
|
6
|
+
*
|
|
7
|
+
* frontmatter 只写设计 §2.2 规定的键,**不写** `originSessionId`:那是读文件的一方在缺失时
|
|
8
|
+
* 自己补的惰性字段,我们不伪造不属于自己的会话 id;也不加自定义键(未知键行为未核实)。
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, lstat } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { atomicWriteFile, SymlinkWriteRefusedError } from "./atomic.ts";
|
|
14
|
+
import { MemoryWriteError } from "./errors.ts";
|
|
15
|
+
import { renderMemoryFile } from "./frontmatter.ts";
|
|
16
|
+
import { updateMemoryIndex } from "./index-file.ts";
|
|
17
|
+
import type { MemoryType } from "./paths.ts";
|
|
18
|
+
|
|
19
|
+
export type MemoryWriteMode = "create" | "upsert";
|
|
20
|
+
|
|
21
|
+
/** 文件名只允许安全字符;首字符必须是字母或数字(挡掉 `.`/`..`/隐藏文件与盘符写法)。 */
|
|
22
|
+
const MEMORY_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
23
|
+
|
|
24
|
+
/** `name` → 事实文件名。`.` 与路径分隔符一律拒绝。 */
|
|
25
|
+
export function memoryFilenameFor(name: string): { filename: string; slug: string } {
|
|
26
|
+
const trimmed = name.trim();
|
|
27
|
+
const slug = trimmed.toLowerCase().endsWith(".md") ? trimmed.slice(0, -3) : trimmed;
|
|
28
|
+
if (slug.length === 0) {
|
|
29
|
+
throw new MemoryWriteError(
|
|
30
|
+
"invalid_name",
|
|
31
|
+
"Memory name must not be empty.",
|
|
32
|
+
"Use a short kebab-case name, for example user-api-preference.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!MEMORY_NAME_PATTERN.test(slug)) {
|
|
36
|
+
throw new MemoryWriteError(
|
|
37
|
+
"invalid_name",
|
|
38
|
+
`name "${name}" is not a valid memory filename: only letters, digits, dots, underscores and hyphens are allowed; it cannot start with a dot or a hyphen, and it cannot contain path separators.`,
|
|
39
|
+
"Use a short kebab-case name, for example user-api-preference.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return { filename: `${slug}.md`, slug };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface WriteMemoryInput {
|
|
46
|
+
/** 记忆目录。 */
|
|
47
|
+
root: string;
|
|
48
|
+
name: string;
|
|
49
|
+
description: string;
|
|
50
|
+
type: MemoryType;
|
|
51
|
+
body: string;
|
|
52
|
+
mode: MemoryWriteMode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WriteMemoryOutcome {
|
|
56
|
+
filename: string;
|
|
57
|
+
absolutePath: string;
|
|
58
|
+
indexPath: string;
|
|
59
|
+
fileAction: "created" | "overwritten";
|
|
60
|
+
indexAction: "inserted" | "updated" | "unchanged";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function writeMemory(input: WriteMemoryInput): Promise<WriteMemoryOutcome> {
|
|
64
|
+
const { filename, slug } = memoryFilenameFor(input.name);
|
|
65
|
+
const description = input.description.replace(/\s+/gu, " ").trim();
|
|
66
|
+
if (description.length === 0) {
|
|
67
|
+
throw new MemoryWriteError(
|
|
68
|
+
"invalid_input",
|
|
69
|
+
"description must not be empty: it is the one-line hook in the index line, and what ZCode uses to judge relevance when recalling a memory.",
|
|
70
|
+
"Write one sentence saying what this memory is.",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (!input.body.trim()) {
|
|
74
|
+
throw new MemoryWriteError(
|
|
75
|
+
"invalid_input",
|
|
76
|
+
"body must not be empty: a memory file has to carry the concrete fact (the body may link other memories with [[other-memory-name]]).",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 目录按计算结果 mkdir -p(三级回退第 3 步:尚无记忆时写入才创建)
|
|
81
|
+
await mkdir(input.root, { recursive: true }).catch((error: unknown) => {
|
|
82
|
+
throw new MemoryWriteError(
|
|
83
|
+
"io",
|
|
84
|
+
`Failed to create the memory directory: ${input.root} (${describe(error)})`,
|
|
85
|
+
"Check the directory permissions and retry.",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const absolutePath = join(input.root, filename);
|
|
90
|
+
const existing = await lstatOrUndefined(absolutePath);
|
|
91
|
+
if (existing?.isSymbolicLink()) {
|
|
92
|
+
throw new MemoryWriteError(
|
|
93
|
+
"symlink",
|
|
94
|
+
`Refusing to write through a symbolic link: ${absolutePath}.`,
|
|
95
|
+
"Pass the real file path instead.",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (input.mode === "create" && existing !== undefined) {
|
|
99
|
+
throw new MemoryWriteError(
|
|
100
|
+
"exists",
|
|
101
|
+
`Memory ${filename} already exists; create mode does not overwrite an existing memory.`,
|
|
102
|
+
'Use mode: "upsert", or choose a different name.',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const content = renderMemoryFile({ name: slug, description, type: input.type, body: input.body });
|
|
107
|
+
try {
|
|
108
|
+
await atomicWriteFile(absolutePath, content);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error instanceof SymlinkWriteRefusedError) {
|
|
111
|
+
throw new MemoryWriteError("symlink", error.message, "Pass the real file path instead.");
|
|
112
|
+
}
|
|
113
|
+
throw new MemoryWriteError(
|
|
114
|
+
"io",
|
|
115
|
+
`Failed to write the memory file: ${absolutePath} (${describe(error)})`,
|
|
116
|
+
"Retry later.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let indexAction: WriteMemoryOutcome["indexAction"];
|
|
121
|
+
try {
|
|
122
|
+
const index = await updateMemoryIndex(input.root, { filename, title: slug, description });
|
|
123
|
+
indexAction = index.action;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof MemoryWriteError && error.code === "index_conflict") {
|
|
126
|
+
throw new MemoryWriteError(
|
|
127
|
+
"index_conflict",
|
|
128
|
+
`The memory file was written (${filename}), but MEMORY.md was changed by another process at the same time, so its index line was not updated.`,
|
|
129
|
+
"Re-read the memory and retry this write so the index line gets added.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
filename,
|
|
137
|
+
absolutePath,
|
|
138
|
+
indexPath: join(input.root, "MEMORY.md"),
|
|
139
|
+
fileAction: existing === undefined ? "created" : "overwritten",
|
|
140
|
+
indexAction,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function lstatOrUndefined(path: string) {
|
|
145
|
+
try {
|
|
146
|
+
return await lstat(path);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (
|
|
149
|
+
typeof error === "object" &&
|
|
150
|
+
error !== null &&
|
|
151
|
+
"code" in error &&
|
|
152
|
+
(error as { code?: unknown }).code === "ENOENT"
|
|
153
|
+
) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
throw new MemoryWriteError("io", `Failed to check the target file: ${path} (${describe(error)})`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function describe(error: unknown): string {
|
|
161
|
+
return error instanceof Error ? error.message : String(error);
|
|
162
|
+
}
|