@aigne/afs-fs 1.11.0-beta.10
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.md +26 -0
- package/README.md +337 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/index.cjs +1101 -0
- package/dist/index.d.cts +223 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +223 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1098 -0
- package/dist/index.mjs.map +1 -0
- package/dist/utils/ripgrep.cjs +85 -0
- package/dist/utils/ripgrep.mjs +85 -0
- package/dist/utils/ripgrep.mjs.map +1 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
|
|
3
|
+
const require_ripgrep = require('./utils/ripgrep.cjs');
|
|
4
|
+
const require_decorate = require('./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs');
|
|
5
|
+
let node_crypto = require("node:crypto");
|
|
6
|
+
let node_fs = require("node:fs");
|
|
7
|
+
let node_fs_promises = require("node:fs/promises");
|
|
8
|
+
let node_os = require("node:os");
|
|
9
|
+
let node_path = require("node:path");
|
|
10
|
+
let _aigne_afs = require("@aigne/afs");
|
|
11
|
+
let _aigne_afs_utils_zod = require("@aigne/afs/utils/zod");
|
|
12
|
+
let ignore = require("ignore");
|
|
13
|
+
ignore = require_rolldown_runtime.__toESM(ignore);
|
|
14
|
+
let js_yaml = require("js-yaml");
|
|
15
|
+
let minimatch = require("minimatch");
|
|
16
|
+
let ufo = require("ufo");
|
|
17
|
+
let zod = require("zod");
|
|
18
|
+
|
|
19
|
+
//#region src/index.ts
|
|
20
|
+
const LIST_MAX_LIMIT = 1e3;
|
|
21
|
+
/**
|
|
22
|
+
* Wrap fs ENOENT errors with AFSNotFoundError for consistent error handling.
|
|
23
|
+
* Re-throws other errors unchanged.
|
|
24
|
+
*/
|
|
25
|
+
function wrapNotFoundError(error, path) {
|
|
26
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") throw new _aigne_afs.AFSNotFoundError(path);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
/** Hidden directory name for storing meta data */
|
|
30
|
+
const AFS_META_DIR = ".afs";
|
|
31
|
+
/** Subdirectory for storing file-level meta (to avoid conflicts with directory resources) */
|
|
32
|
+
const AFS_NODES_DIR = ".nodes";
|
|
33
|
+
/** Meta file name */
|
|
34
|
+
const META_FILE = "meta.yaml";
|
|
35
|
+
const afsFSOptionsSchema = (0, _aigne_afs_utils_zod.camelize)(zod.z.object({
|
|
36
|
+
name: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string()),
|
|
37
|
+
localPath: zod.z.string().describe("The path to the local directory to mount"),
|
|
38
|
+
description: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string().describe("A description of the mounted directory")),
|
|
39
|
+
ignore: (0, _aigne_afs_utils_zod.optionalize)(zod.z.array(zod.z.string())),
|
|
40
|
+
useGitignore: (0, _aigne_afs_utils_zod.optionalize)(zod.z.boolean().describe("Whether to apply .gitignore rules")),
|
|
41
|
+
useAfsignore: (0, _aigne_afs_utils_zod.optionalize)(zod.z.boolean().describe("Whether to apply .afsignore rules")),
|
|
42
|
+
accessMode: (0, _aigne_afs_utils_zod.optionalize)(zod.z.enum(["readonly", "readwrite"]).describe("Access mode for this module")),
|
|
43
|
+
agentSkills: (0, _aigne_afs_utils_zod.optionalize)(zod.z.boolean().describe("Enable automatic agent skill scanning for this module"))
|
|
44
|
+
}));
|
|
45
|
+
var AFSFS = class AFSFS extends _aigne_afs.AFSBaseProvider {
|
|
46
|
+
static schema() {
|
|
47
|
+
return afsFSOptionsSchema;
|
|
48
|
+
}
|
|
49
|
+
static manifest() {
|
|
50
|
+
return {
|
|
51
|
+
name: "fs",
|
|
52
|
+
description: "Local filesystem directory access.\n- Browse directories, read/write files, search content by pattern\n- Exec actions: `archive` (create tar.gz/zip), `checksum` (MD5/SHA)\n- Path structure: direct filesystem paths (e.g., `/docs/guide.md`)",
|
|
53
|
+
uriTemplate: "fs://{localPath+}",
|
|
54
|
+
category: "storage",
|
|
55
|
+
schema: zod.z.object({ localPath: zod.z.string() }),
|
|
56
|
+
tags: ["local", "filesystem"]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
static async load({ basePath, config } = {}) {
|
|
60
|
+
return new AFSFS({
|
|
61
|
+
...await AFSFS.schema().parseAsync(config),
|
|
62
|
+
cwd: basePath
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
name;
|
|
66
|
+
description;
|
|
67
|
+
accessMode;
|
|
68
|
+
constructor(options) {
|
|
69
|
+
super();
|
|
70
|
+
this.options = options;
|
|
71
|
+
(0, _aigne_afs_utils_zod.zodParse)(afsFSOptionsSchema, options);
|
|
72
|
+
let localPath;
|
|
73
|
+
if (options.localPath === ".") localPath = process.cwd();
|
|
74
|
+
else {
|
|
75
|
+
localPath = options.localPath.replaceAll("${CWD}", process.cwd());
|
|
76
|
+
if (localPath.startsWith("~/")) localPath = (0, node_path.join)(process.env.HOME || "", localPath.slice(2));
|
|
77
|
+
if (!(0, node_path.isAbsolute)(localPath)) localPath = (0, node_path.join)(options.cwd || process.cwd(), localPath);
|
|
78
|
+
}
|
|
79
|
+
if (!(0, node_fs.existsSync)(localPath)) (0, node_fs.mkdirSync)(localPath, { recursive: true });
|
|
80
|
+
this.name = options.name || (0, node_path.basename)(localPath) || "fs";
|
|
81
|
+
this.description = options.description;
|
|
82
|
+
this.agentSkills = options.agentSkills;
|
|
83
|
+
this.accessMode = options.accessMode ?? (options.agentSkills ? "readonly" : "readwrite");
|
|
84
|
+
this.options.localPath = localPath;
|
|
85
|
+
this.useGitignore = options.useGitignore ?? false;
|
|
86
|
+
this.useAfsignore = options.useAfsignore ?? true;
|
|
87
|
+
}
|
|
88
|
+
agentSkills;
|
|
89
|
+
/** Whether to apply .gitignore rules (default: false) */
|
|
90
|
+
useGitignore;
|
|
91
|
+
/** Whether to apply .afsignore rules (default: true) */
|
|
92
|
+
useAfsignore;
|
|
93
|
+
get localPathExists() {
|
|
94
|
+
return (0, node_fs_promises.stat)(this.options.localPath).then(() => true).catch(() => false);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Detect MIME type based on file extension
|
|
98
|
+
*/
|
|
99
|
+
getMimeType(filePath) {
|
|
100
|
+
return {
|
|
101
|
+
png: "image/png",
|
|
102
|
+
jpg: "image/jpeg",
|
|
103
|
+
jpeg: "image/jpeg",
|
|
104
|
+
gif: "image/gif",
|
|
105
|
+
bmp: "image/bmp",
|
|
106
|
+
webp: "image/webp",
|
|
107
|
+
svg: "image/svg+xml",
|
|
108
|
+
ico: "image/x-icon",
|
|
109
|
+
pdf: "application/pdf",
|
|
110
|
+
txt: "text/plain",
|
|
111
|
+
md: "text/markdown",
|
|
112
|
+
js: "text/javascript",
|
|
113
|
+
ts: "text/typescript",
|
|
114
|
+
json: "application/json",
|
|
115
|
+
html: "text/html",
|
|
116
|
+
css: "text/css",
|
|
117
|
+
xml: "text/xml"
|
|
118
|
+
}[(0, node_path.basename)(filePath).split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check if file is likely binary based on extension
|
|
122
|
+
*/
|
|
123
|
+
isBinaryFile(filePath) {
|
|
124
|
+
const ext = (0, node_path.basename)(filePath).split(".").pop()?.toLowerCase();
|
|
125
|
+
return [
|
|
126
|
+
"png",
|
|
127
|
+
"jpg",
|
|
128
|
+
"jpeg",
|
|
129
|
+
"gif",
|
|
130
|
+
"bmp",
|
|
131
|
+
"webp",
|
|
132
|
+
"ico",
|
|
133
|
+
"pdf",
|
|
134
|
+
"zip",
|
|
135
|
+
"tar",
|
|
136
|
+
"gz",
|
|
137
|
+
"exe",
|
|
138
|
+
"dll",
|
|
139
|
+
"so",
|
|
140
|
+
"dylib",
|
|
141
|
+
"wasm"
|
|
142
|
+
].includes(ext || "");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if a segment is the hidden .afs directory
|
|
146
|
+
*/
|
|
147
|
+
isHiddenAfsDir(segment) {
|
|
148
|
+
return segment === AFS_META_DIR;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get the physical storage path for a meta operation.
|
|
152
|
+
*
|
|
153
|
+
* Virtual path mapping:
|
|
154
|
+
* - /dir/.meta → /dir/.afs/meta.yaml
|
|
155
|
+
* - /dir/file.txt/.meta → /dir/.afs/.nodes/file.txt/meta.yaml
|
|
156
|
+
*/
|
|
157
|
+
getMetaStoragePath(nodePath, isDirectory) {
|
|
158
|
+
const mountRoot = this.options.localPath;
|
|
159
|
+
const normalizedNodePath = nodePath === "/" ? "" : nodePath;
|
|
160
|
+
const nodeFullPath = (0, node_path.join)(mountRoot, normalizedNodePath);
|
|
161
|
+
const parentDir = (0, node_path.dirname)(nodeFullPath);
|
|
162
|
+
const nodeName = (0, node_path.basename)(nodeFullPath);
|
|
163
|
+
if (isDirectory || normalizedNodePath === "") return (0, node_path.join)(nodeFullPath, AFS_META_DIR, META_FILE);
|
|
164
|
+
return (0, node_path.join)(parentDir, AFS_META_DIR, AFS_NODES_DIR, nodeName, META_FILE);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if a node is a directory (exists and is directory)
|
|
168
|
+
*/
|
|
169
|
+
async isNodeDirectory(nodePath) {
|
|
170
|
+
const mountRoot = this.options.localPath;
|
|
171
|
+
const fullPath = (0, node_path.join)(mountRoot, nodePath === "/" ? "" : nodePath);
|
|
172
|
+
try {
|
|
173
|
+
return (await (0, node_fs_promises.stat)(fullPath)).isDirectory();
|
|
174
|
+
} catch {
|
|
175
|
+
return nodePath === "/";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Load meta object from storage (returns null if not found)
|
|
180
|
+
*/
|
|
181
|
+
async loadMeta(nodePath) {
|
|
182
|
+
try {
|
|
183
|
+
const isDir = await this.isNodeDirectory(nodePath);
|
|
184
|
+
return (0, js_yaml.load)(await (0, node_fs_promises.readFile)(this.getMetaStoragePath(nodePath, isDir), "utf8"));
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Save metadata to meta.yaml (merges with existing)
|
|
191
|
+
* Used internally by writeHandler to persist user metadata
|
|
192
|
+
*/
|
|
193
|
+
async saveMeta(nodePath, meta) {
|
|
194
|
+
const isDir = await this.isNodeDirectory(nodePath);
|
|
195
|
+
const storagePath = this.getMetaStoragePath(nodePath, isDir);
|
|
196
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(storagePath), { recursive: true });
|
|
197
|
+
let existingMeta = {};
|
|
198
|
+
const existing = await this.loadMeta(nodePath);
|
|
199
|
+
if (existing) existingMeta = existing;
|
|
200
|
+
await (0, node_fs_promises.writeFile)(storagePath, (0, js_yaml.dump)({
|
|
201
|
+
...existingMeta,
|
|
202
|
+
...meta
|
|
203
|
+
}), "utf8");
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Read meta for a node silently (returns undefined if no meta exists).
|
|
207
|
+
* Used internally by list/search/read methods.
|
|
208
|
+
*/
|
|
209
|
+
async getNodeMeta(nodePath) {
|
|
210
|
+
const meta = await this.loadMeta(nodePath);
|
|
211
|
+
if (!meta) return;
|
|
212
|
+
return {
|
|
213
|
+
meta,
|
|
214
|
+
kind: typeof meta.kind === "string" ? meta.kind : void 0
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async symlinkToPhysical(path) {
|
|
218
|
+
if (await this.localPathExists) await (0, node_fs_promises.symlink)(this.options.localPath, path);
|
|
219
|
+
}
|
|
220
|
+
async readMetaHandler(ctx) {
|
|
221
|
+
const nodePath = (0, ufo.joinURL)("/", ctx.params.path ?? "");
|
|
222
|
+
const metaPath = (0, ufo.joinURL)(nodePath, ".meta");
|
|
223
|
+
const nodeFullPath = (0, node_path.join)(this.options.localPath, nodePath);
|
|
224
|
+
let nodeStats;
|
|
225
|
+
try {
|
|
226
|
+
nodeStats = await (0, node_fs_promises.stat)(nodeFullPath);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
wrapNotFoundError(error, metaPath);
|
|
229
|
+
}
|
|
230
|
+
const isDir = nodeStats.isDirectory();
|
|
231
|
+
const storagePath = this.getMetaStoragePath(nodePath, isDir);
|
|
232
|
+
let meta = {};
|
|
233
|
+
let metaStats = null;
|
|
234
|
+
try {
|
|
235
|
+
metaStats = await (0, node_fs_promises.stat)(storagePath);
|
|
236
|
+
meta = (0, js_yaml.load)(await (0, node_fs_promises.readFile)(storagePath, "utf8")) || {};
|
|
237
|
+
} catch {}
|
|
238
|
+
return {
|
|
239
|
+
id: nodePath,
|
|
240
|
+
path: metaPath,
|
|
241
|
+
createdAt: metaStats?.birthtime ?? nodeStats.birthtime,
|
|
242
|
+
updatedAt: metaStats?.mtime ?? nodeStats.mtime,
|
|
243
|
+
meta
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async listHandler(ctx) {
|
|
247
|
+
const options = ctx.options;
|
|
248
|
+
const path = (0, node_path.join)("/", ctx.path);
|
|
249
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
250
|
+
const maxChildren = typeof options?.maxChildren === "number" ? options.maxChildren : Number.MAX_SAFE_INTEGER;
|
|
251
|
+
const pattern = options?.pattern;
|
|
252
|
+
const mountRoot = this.options.localPath;
|
|
253
|
+
if (!(await (0, node_fs_promises.stat)(mountRoot)).isDirectory()) return { data: [] };
|
|
254
|
+
const fullPath = (0, node_path.join)(mountRoot, path.slice(1));
|
|
255
|
+
if (typeof maxChildren === "number" && maxChildren <= 0) throw new Error(`Invalid maxChildren: ${maxChildren}. Must be positive.`);
|
|
256
|
+
const entries = [];
|
|
257
|
+
const noExpandPaths = [];
|
|
258
|
+
let stats;
|
|
259
|
+
try {
|
|
260
|
+
stats = await (0, node_fs_promises.stat)(fullPath);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
wrapNotFoundError(error, path);
|
|
263
|
+
}
|
|
264
|
+
if (!stats.isDirectory()) return { data: [] };
|
|
265
|
+
const items = (await (0, node_fs_promises.readdir)(fullPath)).sort().filter((name) => !this.isHiddenAfsDir(name));
|
|
266
|
+
const ignoreResult = await this.loadIgnoreRules(fullPath, mountRoot);
|
|
267
|
+
const ig = ignoreResult?.ig || null;
|
|
268
|
+
const ignoreBase = ignoreResult?.ignoreBase || mountRoot;
|
|
269
|
+
const mountIgnorePatterns = ignoreResult?.mountIgnorePatterns || [];
|
|
270
|
+
const negationPatterns = ignoreResult?.negationPatterns || [];
|
|
271
|
+
const itemsToProcess = items.length > maxChildren ? items.slice(0, maxChildren) : items;
|
|
272
|
+
for (const childName of itemsToProcess) {
|
|
273
|
+
if (entries.length >= limit) break;
|
|
274
|
+
const childFullPath = (0, node_path.join)(fullPath, childName);
|
|
275
|
+
const childRelativePath = (0, node_path.join)(path, childName);
|
|
276
|
+
const itemRelativePath = (0, node_path.relative)(ignoreBase, childFullPath);
|
|
277
|
+
let isIgnored = false;
|
|
278
|
+
if (this.isIgnoredByMountPatterns(itemRelativePath, mountIgnorePatterns)) isIgnored = true;
|
|
279
|
+
else if (this.isNegatedByPatterns(itemRelativePath, negationPatterns)) isIgnored = false;
|
|
280
|
+
else if (ig) isIgnored = ig.ignores(itemRelativePath) || ig.ignores(`${itemRelativePath}/`);
|
|
281
|
+
let childStats;
|
|
282
|
+
try {
|
|
283
|
+
childStats = await (0, node_fs_promises.stat)(childFullPath);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (err.code === "ENOENT") continue;
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
const childIsDirectory = childStats.isDirectory();
|
|
289
|
+
if (isIgnored && !childIsDirectory) continue;
|
|
290
|
+
let childrenCount;
|
|
291
|
+
if (childIsDirectory) {
|
|
292
|
+
try {
|
|
293
|
+
childrenCount = (await (0, node_fs_promises.readdir)(childFullPath)).filter((n) => !this.isHiddenAfsDir(n)).length;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
if (err.code === "ENOENT") continue;
|
|
296
|
+
throw err;
|
|
297
|
+
}
|
|
298
|
+
if (isIgnored) {
|
|
299
|
+
if (!this.hasNegatedDescendants(itemRelativePath, negationPatterns)) noExpandPaths.push(childRelativePath);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const nodeMeta = await this.getNodeMeta(childRelativePath);
|
|
303
|
+
const meta = {
|
|
304
|
+
...nodeMeta?.meta,
|
|
305
|
+
childrenCount,
|
|
306
|
+
size: childStats.size,
|
|
307
|
+
kind: nodeMeta?.kind ?? (childIsDirectory ? "fs:directory" : "fs:file")
|
|
308
|
+
};
|
|
309
|
+
if (!childIsDirectory) meta.mimeType = this.getMimeType(childFullPath);
|
|
310
|
+
const entry = {
|
|
311
|
+
id: childRelativePath,
|
|
312
|
+
path: childRelativePath,
|
|
313
|
+
createdAt: childStats.birthtime,
|
|
314
|
+
updatedAt: childStats.mtime,
|
|
315
|
+
meta
|
|
316
|
+
};
|
|
317
|
+
if (!pattern || (0, minimatch.minimatch)(childRelativePath, pattern, { matchBase: true })) entries.push(entry);
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
data: entries,
|
|
321
|
+
noExpand: noExpandPaths.length > 0 ? noExpandPaths : void 0
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async readHandler(ctx) {
|
|
325
|
+
const path = ctx.path;
|
|
326
|
+
const normalizedPath = (0, node_path.join)("/", path);
|
|
327
|
+
if (path.split("/").filter(Boolean).some((seg) => this.isHiddenAfsDir(seg))) return;
|
|
328
|
+
const mountRoot = this.options.localPath;
|
|
329
|
+
const mountStats = await (0, node_fs_promises.stat)(mountRoot);
|
|
330
|
+
let fullPath;
|
|
331
|
+
let stats;
|
|
332
|
+
try {
|
|
333
|
+
if (!mountStats.isDirectory()) {
|
|
334
|
+
if (normalizedPath !== "/") return;
|
|
335
|
+
fullPath = mountRoot;
|
|
336
|
+
stats = mountStats;
|
|
337
|
+
} else {
|
|
338
|
+
fullPath = (0, node_path.join)(mountRoot, path);
|
|
339
|
+
stats = await (0, node_fs_promises.stat)(fullPath);
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
wrapNotFoundError(error, normalizedPath);
|
|
343
|
+
}
|
|
344
|
+
let content;
|
|
345
|
+
const nodeMeta = await this.getNodeMeta(normalizedPath);
|
|
346
|
+
const meta = {
|
|
347
|
+
size: stats.size,
|
|
348
|
+
...nodeMeta?.meta,
|
|
349
|
+
kind: nodeMeta?.kind
|
|
350
|
+
};
|
|
351
|
+
if (stats.isDirectory()) meta.childrenCount = (await (0, node_fs_promises.readdir)(fullPath)).filter((c) => !this.isHiddenAfsDir(c)).length;
|
|
352
|
+
else if (stats.isFile()) {
|
|
353
|
+
const mimeType = this.getMimeType(fullPath);
|
|
354
|
+
const isBinary = this.isBinaryFile(fullPath);
|
|
355
|
+
meta.mimeType = mimeType;
|
|
356
|
+
if (isBinary) {
|
|
357
|
+
content = (await (0, node_fs_promises.readFile)(fullPath)).toString("base64");
|
|
358
|
+
meta.contentType = "base64";
|
|
359
|
+
} else content = await (0, node_fs_promises.readFile)(fullPath, "utf8");
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
id: normalizedPath,
|
|
363
|
+
path: normalizedPath,
|
|
364
|
+
createdAt: stats.birthtime,
|
|
365
|
+
updatedAt: stats.mtime,
|
|
366
|
+
content,
|
|
367
|
+
meta
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async writeHandler(ctx, entry) {
|
|
371
|
+
const path = ctx.path;
|
|
372
|
+
const normalizedPath = (0, node_path.join)("/", path);
|
|
373
|
+
const options = ctx.options;
|
|
374
|
+
const fullPath = (0, node_path.join)(this.options.localPath, path);
|
|
375
|
+
const append = options?.append ?? false;
|
|
376
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
377
|
+
if (entry.content !== void 0) {
|
|
378
|
+
let contentToWrite;
|
|
379
|
+
if (typeof entry.content === "string") contentToWrite = entry.content;
|
|
380
|
+
else contentToWrite = JSON.stringify(entry.content, null, 2);
|
|
381
|
+
await (0, node_fs_promises.writeFile)(fullPath, contentToWrite, {
|
|
382
|
+
encoding: "utf8",
|
|
383
|
+
flag: append ? "a" : "w"
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
if (entry.meta && Object.keys(entry.meta).length > 0) {
|
|
387
|
+
const { size: _size, mimeType: _mime, childrenCount: _cc, ...metaToPersist } = entry.meta;
|
|
388
|
+
if (Object.keys(metaToPersist).length > 0) await this.saveMeta(path, metaToPersist);
|
|
389
|
+
}
|
|
390
|
+
const stats = await (0, node_fs_promises.stat)(fullPath);
|
|
391
|
+
let finalMetadata = { size: stats.size };
|
|
392
|
+
const meta = await this.loadMeta(path);
|
|
393
|
+
if (meta) finalMetadata = {
|
|
394
|
+
...meta,
|
|
395
|
+
size: stats.size
|
|
396
|
+
};
|
|
397
|
+
return { data: {
|
|
398
|
+
id: normalizedPath,
|
|
399
|
+
path: normalizedPath,
|
|
400
|
+
createdAt: stats.birthtime,
|
|
401
|
+
updatedAt: stats.mtime,
|
|
402
|
+
content: entry.content,
|
|
403
|
+
summary: entry.summary,
|
|
404
|
+
meta: finalMetadata,
|
|
405
|
+
userId: entry.userId,
|
|
406
|
+
sessionId: entry.sessionId,
|
|
407
|
+
linkTo: entry.linkTo
|
|
408
|
+
} };
|
|
409
|
+
}
|
|
410
|
+
async deleteHandler(ctx) {
|
|
411
|
+
const path = ctx.path;
|
|
412
|
+
const displayPath = path === "/" ? "/" : path.slice(1);
|
|
413
|
+
const options = ctx.options;
|
|
414
|
+
const fullPath = (0, node_path.join)(this.options.localPath, path);
|
|
415
|
+
const recursive = options?.recursive ?? false;
|
|
416
|
+
let stats;
|
|
417
|
+
try {
|
|
418
|
+
stats = await (0, node_fs_promises.stat)(fullPath);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
wrapNotFoundError(error, path);
|
|
421
|
+
}
|
|
422
|
+
if (stats.isDirectory() && !recursive) throw new Error(`Cannot delete directory '${displayPath}' without recursive option. Set recursive: true to delete directories.`);
|
|
423
|
+
await (0, node_fs_promises.rm)(fullPath, {
|
|
424
|
+
recursive,
|
|
425
|
+
force: true
|
|
426
|
+
});
|
|
427
|
+
return { message: `Successfully deleted: ${displayPath}` };
|
|
428
|
+
}
|
|
429
|
+
async renameHandler(ctx, newPath) {
|
|
430
|
+
const oldPath = ctx.path;
|
|
431
|
+
const displayOldPath = oldPath === "/" ? "/" : oldPath.slice(1);
|
|
432
|
+
const overwrite = ctx.options?.overwrite ?? false;
|
|
433
|
+
const oldFullPath = (0, node_path.join)(this.options.localPath, oldPath);
|
|
434
|
+
const newFullPath = (0, node_path.join)(this.options.localPath, newPath);
|
|
435
|
+
try {
|
|
436
|
+
await (0, node_fs_promises.stat)(oldFullPath);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
wrapNotFoundError(error, oldPath);
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
await (0, node_fs_promises.stat)(newFullPath);
|
|
442
|
+
if (!overwrite) throw new Error(`Destination '${newPath}' already exists. Set overwrite: true to replace it.`);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
if (error.code !== "ENOENT") throw error;
|
|
445
|
+
}
|
|
446
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(newFullPath), { recursive: true });
|
|
447
|
+
await (0, node_fs_promises.rename)(oldFullPath, newFullPath);
|
|
448
|
+
return { message: `Successfully renamed '${displayOldPath}' to '${newPath}'` };
|
|
449
|
+
}
|
|
450
|
+
async searchHandler(ctx, query, options) {
|
|
451
|
+
const path = ctx.path;
|
|
452
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
453
|
+
const mountRoot = this.options.localPath;
|
|
454
|
+
const basePath = (0, node_path.join)(mountRoot, path);
|
|
455
|
+
try {
|
|
456
|
+
await (0, node_fs_promises.stat)(basePath);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
wrapNotFoundError(error, path);
|
|
459
|
+
}
|
|
460
|
+
const matches = await require_ripgrep.searchWithRipgrep(basePath, query, options);
|
|
461
|
+
const ignoreResult = await this.loadIgnoreRules(basePath, mountRoot);
|
|
462
|
+
const ig = ignoreResult?.ig || null;
|
|
463
|
+
const mountIgnorePatterns = ignoreResult?.mountIgnorePatterns || [];
|
|
464
|
+
const entries = [];
|
|
465
|
+
const processedFiles = /* @__PURE__ */ new Set();
|
|
466
|
+
let hasMoreFiles = false;
|
|
467
|
+
for (const match of matches) if (match.type === "match" && match.data.path) {
|
|
468
|
+
const absolutePath = match.data.path.text;
|
|
469
|
+
const itemRelativePath = (0, node_path.join)(path, (0, node_path.relative)(basePath, absolutePath));
|
|
470
|
+
const pathFromRoot = (0, node_path.relative)(mountRoot, absolutePath);
|
|
471
|
+
if (this.isIgnoredByMountPatterns(pathFromRoot, mountIgnorePatterns)) continue;
|
|
472
|
+
if (ig && (ig.ignores(pathFromRoot) || ig.ignores(`${pathFromRoot}/`))) continue;
|
|
473
|
+
if (processedFiles.has(itemRelativePath)) continue;
|
|
474
|
+
processedFiles.add(itemRelativePath);
|
|
475
|
+
const normalizedEntryPath = (0, node_path.join)("/", itemRelativePath);
|
|
476
|
+
const stats = await (0, node_fs_promises.stat)(absolutePath);
|
|
477
|
+
const nodeMeta = await this.getNodeMeta(itemRelativePath);
|
|
478
|
+
const entry = {
|
|
479
|
+
id: normalizedEntryPath,
|
|
480
|
+
path: normalizedEntryPath,
|
|
481
|
+
createdAt: stats.birthtime,
|
|
482
|
+
updatedAt: stats.mtime,
|
|
483
|
+
summary: match.data.lines?.text,
|
|
484
|
+
meta: {
|
|
485
|
+
size: stats.size,
|
|
486
|
+
...nodeMeta?.meta,
|
|
487
|
+
kind: nodeMeta?.kind
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
entries.push(entry);
|
|
491
|
+
if (entries.length >= limit) {
|
|
492
|
+
hasMoreFiles = true;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
data: entries,
|
|
498
|
+
message: hasMoreFiles ? `Results truncated to limit ${limit}` : void 0
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async statHandler(ctx) {
|
|
502
|
+
const normalizedPath = ctx.path;
|
|
503
|
+
if (normalizedPath.split("/").filter(Boolean).some((seg) => this.isHiddenAfsDir(seg))) throw new Error("Access denied: .afs is a hidden directory");
|
|
504
|
+
const fullPath = (0, node_path.join)(this.options.localPath, normalizedPath);
|
|
505
|
+
let stats;
|
|
506
|
+
try {
|
|
507
|
+
stats = await (0, node_fs_promises.stat)(fullPath);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
wrapNotFoundError(error, normalizedPath);
|
|
510
|
+
}
|
|
511
|
+
const meta = { ...await this.loadMeta(normalizedPath) };
|
|
512
|
+
if (stats.isFile()) meta.size = stats.size;
|
|
513
|
+
else if (stats.isDirectory()) meta.childrenCount = (await (0, node_fs_promises.readdir)(fullPath)).filter((c) => !this.isHiddenAfsDir(c)).length;
|
|
514
|
+
return { data: {
|
|
515
|
+
id: (0, node_path.basename)(normalizedPath) || "/",
|
|
516
|
+
path: normalizedPath,
|
|
517
|
+
updatedAt: stats.mtime,
|
|
518
|
+
createdAt: stats.birthtime,
|
|
519
|
+
meta
|
|
520
|
+
} };
|
|
521
|
+
}
|
|
522
|
+
async explainHandler(ctx) {
|
|
523
|
+
const normalizedPath = ctx.path;
|
|
524
|
+
const format = ctx.options?.format || "markdown";
|
|
525
|
+
if (normalizedPath.split("/").filter(Boolean).some((seg) => this.isHiddenAfsDir(seg))) throw new Error("Access denied: .afs is a hidden directory");
|
|
526
|
+
const fullPath = (0, node_path.join)(this.options.localPath, normalizedPath);
|
|
527
|
+
const stats = await (0, node_fs_promises.stat)(fullPath);
|
|
528
|
+
const isDir = stats.isDirectory();
|
|
529
|
+
const nodeName = (0, node_path.basename)(normalizedPath) || "/";
|
|
530
|
+
const meta = await this.loadMeta(normalizedPath);
|
|
531
|
+
let kindSchema;
|
|
532
|
+
if (meta?.kind && typeof meta.kind === "string") kindSchema = (0, _aigne_afs.resolveKindSchema)(meta.kind);
|
|
533
|
+
let children = [];
|
|
534
|
+
if (isDir) children = (await (0, node_fs_promises.readdir)(fullPath)).filter((item) => !this.isHiddenAfsDir(item)).sort();
|
|
535
|
+
const lines = [];
|
|
536
|
+
if (format === "markdown") {
|
|
537
|
+
lines.push(`# ${nodeName}`);
|
|
538
|
+
lines.push("");
|
|
539
|
+
lines.push(`**Path:** \`${normalizedPath}\``);
|
|
540
|
+
lines.push(`**Type:** ${isDir ? "directory" : "file"}`);
|
|
541
|
+
if (!isDir) lines.push(`**Size:** ${stats.size} bytes`);
|
|
542
|
+
if (meta) {
|
|
543
|
+
lines.push("");
|
|
544
|
+
lines.push("## Metadata");
|
|
545
|
+
if (meta.kind) lines.push(`**Kind:** \`${meta.kind}\``);
|
|
546
|
+
if (meta.name) lines.push(`**Name:** ${meta.name}`);
|
|
547
|
+
if (meta.description) lines.push(`**Description:** ${meta.description}`);
|
|
548
|
+
const otherProps = Object.entries(meta).filter(([key]) => ![
|
|
549
|
+
"kind",
|
|
550
|
+
"name",
|
|
551
|
+
"description"
|
|
552
|
+
].includes(key));
|
|
553
|
+
if (otherProps.length > 0) {
|
|
554
|
+
lines.push("");
|
|
555
|
+
for (const [key, value] of otherProps) lines.push(`- **${key}:** ${JSON.stringify(value)}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (kindSchema) {
|
|
559
|
+
lines.push("");
|
|
560
|
+
lines.push("## Kind Schema");
|
|
561
|
+
lines.push(`This node is of kind \`${kindSchema.name}\`.`);
|
|
562
|
+
if (kindSchema.description) lines.push(`> ${kindSchema.description}`);
|
|
563
|
+
if (kindSchema.extends) lines.push(`Extends: \`${kindSchema.extends}\``);
|
|
564
|
+
}
|
|
565
|
+
if (isDir && children.length > 0) {
|
|
566
|
+
lines.push("");
|
|
567
|
+
lines.push("## Contents");
|
|
568
|
+
lines.push("");
|
|
569
|
+
for (const child of children.slice(0, 20)) lines.push(`- ${child}`);
|
|
570
|
+
if (children.length > 20) lines.push(`- ... and ${children.length - 20} more`);
|
|
571
|
+
} else if (isDir) {
|
|
572
|
+
lines.push("");
|
|
573
|
+
lines.push("*This directory is empty.*");
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
lines.push(`${nodeName} (${isDir ? "directory" : "file"})`);
|
|
577
|
+
lines.push(`Path: ${normalizedPath}`);
|
|
578
|
+
if (!isDir) lines.push(`Size: ${stats.size} bytes`);
|
|
579
|
+
if (meta) {
|
|
580
|
+
if (meta.kind) lines.push(`Kind: ${meta.kind}`);
|
|
581
|
+
if (meta.name) lines.push(`Name: ${meta.name}`);
|
|
582
|
+
if (meta.description) lines.push(`Description: ${meta.description}`);
|
|
583
|
+
}
|
|
584
|
+
if (isDir && children.length > 0) {
|
|
585
|
+
lines.push("");
|
|
586
|
+
lines.push("Contents:");
|
|
587
|
+
for (const child of children.slice(0, 20)) lines.push(` - ${child}`);
|
|
588
|
+
if (children.length > 20) lines.push(` ... and ${children.length - 20} more`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
content: lines.join("\n"),
|
|
593
|
+
format
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async readCapabilitiesHandler(_ctx) {
|
|
597
|
+
const operations = [
|
|
598
|
+
"list",
|
|
599
|
+
"read",
|
|
600
|
+
"stat",
|
|
601
|
+
"explain",
|
|
602
|
+
"search"
|
|
603
|
+
];
|
|
604
|
+
if (this.accessMode === "readwrite") operations.push("write", "delete", "rename");
|
|
605
|
+
return {
|
|
606
|
+
id: "/.meta/.capabilities",
|
|
607
|
+
path: "/.meta/.capabilities",
|
|
608
|
+
content: {
|
|
609
|
+
schemaVersion: 1,
|
|
610
|
+
provider: this.name,
|
|
611
|
+
description: this.description || "Local filesystem provider",
|
|
612
|
+
tools: [],
|
|
613
|
+
operations: this.getOperationsDeclaration(),
|
|
614
|
+
actions: [{
|
|
615
|
+
description: "Directory-level actions",
|
|
616
|
+
catalog: [{
|
|
617
|
+
name: "archive",
|
|
618
|
+
description: "Create a compressed archive (tar.gz or zip) of the directory contents",
|
|
619
|
+
inputSchema: {
|
|
620
|
+
type: "object",
|
|
621
|
+
properties: {
|
|
622
|
+
format: {
|
|
623
|
+
type: "string",
|
|
624
|
+
enum: ["tar.gz", "zip"],
|
|
625
|
+
description: "Archive format"
|
|
626
|
+
},
|
|
627
|
+
pattern: {
|
|
628
|
+
type: "string",
|
|
629
|
+
description: "Glob pattern to filter files (e.g., '**/*.ts')"
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
required: ["format"]
|
|
633
|
+
}
|
|
634
|
+
}],
|
|
635
|
+
discovery: {
|
|
636
|
+
pathTemplate: "/:path*/.actions",
|
|
637
|
+
note: "Archive action available on directories"
|
|
638
|
+
}
|
|
639
|
+
}, {
|
|
640
|
+
description: "File-level actions",
|
|
641
|
+
catalog: [{
|
|
642
|
+
name: "checksum",
|
|
643
|
+
description: "Compute a cryptographic hash of the file content",
|
|
644
|
+
inputSchema: {
|
|
645
|
+
type: "object",
|
|
646
|
+
properties: { algorithm: {
|
|
647
|
+
type: "string",
|
|
648
|
+
enum: [
|
|
649
|
+
"md5",
|
|
650
|
+
"sha1",
|
|
651
|
+
"sha256",
|
|
652
|
+
"sha512"
|
|
653
|
+
],
|
|
654
|
+
description: "Hash algorithm to use"
|
|
655
|
+
} },
|
|
656
|
+
required: ["algorithm"]
|
|
657
|
+
}
|
|
658
|
+
}],
|
|
659
|
+
discovery: {
|
|
660
|
+
pathTemplate: "/:path*/.actions",
|
|
661
|
+
note: "Checksum action available on files"
|
|
662
|
+
}
|
|
663
|
+
}]
|
|
664
|
+
},
|
|
665
|
+
meta: {
|
|
666
|
+
kind: "afs:capabilities",
|
|
667
|
+
operations
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
async archiveAction(ctx, args) {
|
|
672
|
+
const normalizedPath = (0, ufo.joinURL)("/", ctx.params.path ?? "");
|
|
673
|
+
const format = args.format;
|
|
674
|
+
const pattern = args.pattern;
|
|
675
|
+
const mountRoot = this.options.localPath;
|
|
676
|
+
if (!format || !["tar.gz", "zip"].includes(format)) return {
|
|
677
|
+
success: false,
|
|
678
|
+
error: {
|
|
679
|
+
code: "INVALID_FORMAT",
|
|
680
|
+
message: `Unsupported archive format: ${format}. Supported formats: tar.gz, zip`
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
const fullPath = (0, node_path.join)(mountRoot, normalizedPath);
|
|
684
|
+
if (!fullPath.startsWith(mountRoot)) return {
|
|
685
|
+
success: false,
|
|
686
|
+
error: {
|
|
687
|
+
code: "PATH_TRAVERSAL",
|
|
688
|
+
message: "Path traversal is not allowed"
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
let stats;
|
|
692
|
+
try {
|
|
693
|
+
stats = await (0, node_fs_promises.stat)(fullPath);
|
|
694
|
+
} catch {
|
|
695
|
+
return {
|
|
696
|
+
success: false,
|
|
697
|
+
error: {
|
|
698
|
+
code: "NOT_FOUND",
|
|
699
|
+
message: `Path not found: ${normalizedPath}`
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (!stats.isDirectory()) return {
|
|
704
|
+
success: false,
|
|
705
|
+
error: {
|
|
706
|
+
code: "NOT_DIRECTORY",
|
|
707
|
+
message: `Archive action requires a directory, but ${normalizedPath} is a file`
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
const files = [];
|
|
711
|
+
await this.collectFilesForArchive(fullPath, fullPath, pattern, files);
|
|
712
|
+
const ext = format === "tar.gz" ? ".tar.gz" : ".zip";
|
|
713
|
+
const archiveName = `afs-archive-${Date.now()}${ext}`;
|
|
714
|
+
const outputPath = (0, node_path.join)((0, node_os.tmpdir)(), archiveName);
|
|
715
|
+
try {
|
|
716
|
+
if (format === "tar.gz") await this.createTarGz(fullPath, files, outputPath);
|
|
717
|
+
else await this.createZip(fullPath, files, outputPath);
|
|
718
|
+
return {
|
|
719
|
+
success: true,
|
|
720
|
+
data: {
|
|
721
|
+
outputPath,
|
|
722
|
+
size: (await (0, node_fs_promises.stat)(outputPath)).size,
|
|
723
|
+
fileCount: files.length,
|
|
724
|
+
format
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
} catch (error) {
|
|
728
|
+
try {
|
|
729
|
+
await (0, node_fs_promises.rm)(outputPath, { force: true });
|
|
730
|
+
} catch {}
|
|
731
|
+
return {
|
|
732
|
+
success: false,
|
|
733
|
+
error: {
|
|
734
|
+
code: "ARCHIVE_ERROR",
|
|
735
|
+
message: `Failed to create archive: ${error instanceof Error ? error.message : "unknown error"}`
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async checksumAction(ctx, args) {
|
|
741
|
+
const normalizedPath = (0, ufo.joinURL)("/", ctx.params.path ?? "");
|
|
742
|
+
const algorithm = args.algorithm;
|
|
743
|
+
const mountRoot = this.options.localPath;
|
|
744
|
+
const supportedAlgorithms = [
|
|
745
|
+
"md5",
|
|
746
|
+
"sha1",
|
|
747
|
+
"sha256",
|
|
748
|
+
"sha512"
|
|
749
|
+
];
|
|
750
|
+
if (!algorithm || !supportedAlgorithms.includes(algorithm)) return {
|
|
751
|
+
success: false,
|
|
752
|
+
error: {
|
|
753
|
+
code: "INVALID_ALGORITHM",
|
|
754
|
+
message: `Unsupported algorithm: ${algorithm}. Supported: ${supportedAlgorithms.join(", ")}`
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
const fullPath = (0, node_path.join)(mountRoot, normalizedPath);
|
|
758
|
+
if (!fullPath.startsWith(mountRoot)) return {
|
|
759
|
+
success: false,
|
|
760
|
+
error: {
|
|
761
|
+
code: "PATH_TRAVERSAL",
|
|
762
|
+
message: "Path traversal is not allowed"
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
let stats;
|
|
766
|
+
try {
|
|
767
|
+
stats = await (0, node_fs_promises.stat)(fullPath);
|
|
768
|
+
} catch {
|
|
769
|
+
return {
|
|
770
|
+
success: false,
|
|
771
|
+
error: {
|
|
772
|
+
code: "NOT_FOUND",
|
|
773
|
+
message: `Path not found: ${normalizedPath}`
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
if (stats.isDirectory()) return {
|
|
778
|
+
success: false,
|
|
779
|
+
error: {
|
|
780
|
+
code: "NOT_FILE",
|
|
781
|
+
message: `Checksum action requires a file, but ${normalizedPath} is a directory`
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
try {
|
|
785
|
+
return {
|
|
786
|
+
success: true,
|
|
787
|
+
data: {
|
|
788
|
+
hash: await new Promise((resolve, reject) => {
|
|
789
|
+
const hasher = (0, node_crypto.createHash)(algorithm);
|
|
790
|
+
const stream = (0, node_fs.createReadStream)(fullPath);
|
|
791
|
+
stream.on("data", (chunk) => hasher.update(chunk));
|
|
792
|
+
stream.on("end", () => resolve(hasher.digest("hex")));
|
|
793
|
+
stream.on("error", reject);
|
|
794
|
+
}),
|
|
795
|
+
algorithm,
|
|
796
|
+
size: stats.size,
|
|
797
|
+
path: normalizedPath
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
} catch (error) {
|
|
801
|
+
return {
|
|
802
|
+
success: false,
|
|
803
|
+
error: {
|
|
804
|
+
code: "CHECKSUM_ERROR",
|
|
805
|
+
message: `Failed to compute checksum: ${error instanceof Error ? error.message : "unknown error"}`
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Recursively collect files for archiving.
|
|
812
|
+
* Excludes .afs directories and respects optional glob pattern.
|
|
813
|
+
*/
|
|
814
|
+
async collectFilesForArchive(baseDir, currentDir, pattern, files) {
|
|
815
|
+
let items;
|
|
816
|
+
try {
|
|
817
|
+
items = await (0, node_fs_promises.readdir)(currentDir);
|
|
818
|
+
} catch {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
for (const item of items) {
|
|
822
|
+
if (this.isHiddenAfsDir(item)) continue;
|
|
823
|
+
const fullPath = (0, node_path.join)(currentDir, item);
|
|
824
|
+
let itemStats;
|
|
825
|
+
try {
|
|
826
|
+
itemStats = await (0, node_fs_promises.stat)(fullPath);
|
|
827
|
+
} catch {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if (itemStats.isDirectory()) await this.collectFilesForArchive(baseDir, fullPath, pattern, files);
|
|
831
|
+
else {
|
|
832
|
+
const relativePath = (0, node_path.relative)(baseDir, fullPath);
|
|
833
|
+
if (pattern) {
|
|
834
|
+
if ((0, minimatch.minimatch)(relativePath, pattern, { matchBase: true })) files.push(relativePath);
|
|
835
|
+
} else files.push(relativePath);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Create a tar.gz archive from a list of files.
|
|
841
|
+
*/
|
|
842
|
+
async createTarGz(baseDir, files, outputPath) {
|
|
843
|
+
const { execSync } = await import("node:child_process");
|
|
844
|
+
if (files.length === 0) {
|
|
845
|
+
execSync(`tar czf "${outputPath}" -T /dev/null`, { cwd: baseDir });
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const fileListPath = (0, node_path.join)((0, node_os.tmpdir)(), `afs-tar-list-${Date.now()}.txt`);
|
|
849
|
+
await (0, node_fs_promises.writeFile)(fileListPath, files.join("\n"), "utf8");
|
|
850
|
+
try {
|
|
851
|
+
execSync(`tar czf "${outputPath}" -T "${fileListPath}"`, { cwd: baseDir });
|
|
852
|
+
} finally {
|
|
853
|
+
await (0, node_fs_promises.rm)(fileListPath, { force: true });
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Create a zip archive from a list of files.
|
|
858
|
+
*/
|
|
859
|
+
async createZip(baseDir, files, outputPath) {
|
|
860
|
+
const { execSync } = await import("node:child_process");
|
|
861
|
+
if (files.length === 0) {
|
|
862
|
+
execSync(`zip -q "${outputPath}" -T 2>/dev/null || true`, { cwd: baseDir });
|
|
863
|
+
try {
|
|
864
|
+
await (0, node_fs_promises.stat)(outputPath);
|
|
865
|
+
} catch {
|
|
866
|
+
await (0, node_fs_promises.writeFile)(outputPath, Buffer.from([
|
|
867
|
+
80,
|
|
868
|
+
75,
|
|
869
|
+
5,
|
|
870
|
+
6,
|
|
871
|
+
0,
|
|
872
|
+
0,
|
|
873
|
+
0,
|
|
874
|
+
0,
|
|
875
|
+
0,
|
|
876
|
+
0,
|
|
877
|
+
0,
|
|
878
|
+
0,
|
|
879
|
+
0,
|
|
880
|
+
0,
|
|
881
|
+
0,
|
|
882
|
+
0,
|
|
883
|
+
0,
|
|
884
|
+
0,
|
|
885
|
+
0,
|
|
886
|
+
0,
|
|
887
|
+
0,
|
|
888
|
+
0
|
|
889
|
+
]));
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const fileListPath = (0, node_path.join)((0, node_os.tmpdir)(), `afs-zip-list-${Date.now()}.txt`);
|
|
894
|
+
await (0, node_fs_promises.writeFile)(fileListPath, files.join("\n"), "utf8");
|
|
895
|
+
try {
|
|
896
|
+
execSync(`cat "${fileListPath}" | zip -q "${outputPath}" -@`, { cwd: baseDir });
|
|
897
|
+
} finally {
|
|
898
|
+
await (0, node_fs_promises.rm)(fileListPath, { force: true });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Read .gitignore content safely from a directory.
|
|
903
|
+
* Returns empty string if file doesn't exist or is unreadable.
|
|
904
|
+
*/
|
|
905
|
+
async readGitignoreContent(dirPath) {
|
|
906
|
+
try {
|
|
907
|
+
return await (0, node_fs_promises.readFile)((0, node_path.join)(dirPath, ".gitignore"), "utf8");
|
|
908
|
+
} catch {
|
|
909
|
+
return "";
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Read .afsignore content safely from a directory.
|
|
914
|
+
* Returns empty string if file doesn't exist or is unreadable.
|
|
915
|
+
*/
|
|
916
|
+
async readAfsignoreContent(dirPath) {
|
|
917
|
+
try {
|
|
918
|
+
return await (0, node_fs_promises.readFile)((0, node_path.join)(dirPath, ".afsignore"), "utf8");
|
|
919
|
+
} catch {
|
|
920
|
+
return "";
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Parse .afsignore content and expand @inherit directives.
|
|
925
|
+
* Returns an array of patterns with @inherit .gitignore replaced by actual gitignore rules.
|
|
926
|
+
* @param content - The .afsignore file content
|
|
927
|
+
* @param dirPath - The directory containing the .afsignore file
|
|
928
|
+
* @param mountRoot - The mount root path for security checks
|
|
929
|
+
* @param visitedPaths - Set of already visited paths to detect circular references
|
|
930
|
+
*/
|
|
931
|
+
async parseAfsignoreContent(content, dirPath, mountRoot, visitedPaths = /* @__PURE__ */ new Set()) {
|
|
932
|
+
const patterns = [];
|
|
933
|
+
const lines = content.split("\n");
|
|
934
|
+
for (const line of lines) {
|
|
935
|
+
const trimmed = line.trim();
|
|
936
|
+
if (trimmed.startsWith("@inherit ")) {
|
|
937
|
+
const target = trimmed.slice(9).trim();
|
|
938
|
+
if (target === ".gitignore") {
|
|
939
|
+
const gitignoreContent = await this.readGitignoreContent(dirPath);
|
|
940
|
+
if (gitignoreContent) patterns.push(...gitignoreContent.split("\n"));
|
|
941
|
+
} else {
|
|
942
|
+
const normalizedTarget = (0, node_path.join)(dirPath, target);
|
|
943
|
+
if (!normalizedTarget.startsWith(mountRoot) || (0, node_path.isAbsolute)(target) || visitedPaths.has(normalizedTarget)) continue;
|
|
944
|
+
visitedPaths.add(normalizedTarget);
|
|
945
|
+
try {
|
|
946
|
+
const inheritedContent = await (0, node_fs_promises.readFile)(normalizedTarget, "utf8");
|
|
947
|
+
if (target.endsWith(".afsignore")) {
|
|
948
|
+
const inheritedPatterns = await this.parseAfsignoreContent(inheritedContent, (0, node_path.dirname)(normalizedTarget), mountRoot, visitedPaths);
|
|
949
|
+
patterns.push(...inheritedPatterns);
|
|
950
|
+
} else patterns.push(...inheritedContent.split("\n"));
|
|
951
|
+
} catch {}
|
|
952
|
+
}
|
|
953
|
+
} else patterns.push(line);
|
|
954
|
+
}
|
|
955
|
+
return patterns;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Add patterns to an ignore instance, handling prefixing for subdirectories.
|
|
959
|
+
* @param ig - The ignore instance to add patterns to
|
|
960
|
+
* @param patterns - Array of pattern lines
|
|
961
|
+
* @param dirPath - The directory these patterns come from
|
|
962
|
+
* @param baseDir - The base directory for relative path calculation
|
|
963
|
+
*/
|
|
964
|
+
addPatternsToIgnore(ig, patterns, dirPath, baseDir) {
|
|
965
|
+
const normalizedDirPath = dirPath.endsWith("/") ? dirPath.slice(0, -1) : dirPath;
|
|
966
|
+
const normalizedBaseDir = baseDir.endsWith("/") ? baseDir.slice(0, -1) : baseDir;
|
|
967
|
+
const needsPrefix = normalizedDirPath !== normalizedBaseDir;
|
|
968
|
+
const prefix = needsPrefix ? (0, node_path.relative)(normalizedBaseDir, normalizedDirPath) : "";
|
|
969
|
+
for (const line of patterns) {
|
|
970
|
+
const trimmed = line.trim();
|
|
971
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
972
|
+
const isNegation = trimmed.startsWith("!");
|
|
973
|
+
const pattern = isNegation ? trimmed.slice(1) : trimmed;
|
|
974
|
+
if (needsPrefix) {
|
|
975
|
+
let prefixedPattern;
|
|
976
|
+
if (pattern.startsWith("/")) prefixedPattern = `/${prefix}${pattern}`;
|
|
977
|
+
else if (pattern.includes("/") && !pattern.startsWith("**/")) prefixedPattern = `${prefix}/${pattern}`;
|
|
978
|
+
else prefixedPattern = `${prefix}/**/${pattern}`;
|
|
979
|
+
ig.add(isNegation ? `!${prefixedPattern}` : prefixedPattern);
|
|
980
|
+
} else ig.add(trimmed);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Load combined ignore rules from mountRoot down to checkPath.
|
|
985
|
+
* Combines .gitignore (if useGitignore), .afsignore (if useAfsignore), and mount ignore options.
|
|
986
|
+
*
|
|
987
|
+
* Priority order (later rules override earlier):
|
|
988
|
+
* 1. Mount config `ignore` option (highest priority - always applied first as base)
|
|
989
|
+
* 2. .gitignore rules (if useGitignore=true, or via @inherit in .afsignore)
|
|
990
|
+
* 3. .afsignore rules (if useAfsignore=true)
|
|
991
|
+
*
|
|
992
|
+
* @param checkPath - The directory whose files we're checking
|
|
993
|
+
* @param mountRoot - The mounted local filesystem root
|
|
994
|
+
* @returns An object with ignore instance and the base path for matching
|
|
995
|
+
*/
|
|
996
|
+
async loadIgnoreRules(checkPath, mountRoot) {
|
|
997
|
+
const ig = (0, ignore.default)();
|
|
998
|
+
const dirsToCheck = [];
|
|
999
|
+
let currentPath = checkPath;
|
|
1000
|
+
while (true) {
|
|
1001
|
+
dirsToCheck.unshift(currentPath);
|
|
1002
|
+
if (currentPath === mountRoot) break;
|
|
1003
|
+
const parentPath = (0, node_path.dirname)(currentPath);
|
|
1004
|
+
if (!currentPath.startsWith(mountRoot) || parentPath === currentPath) break;
|
|
1005
|
+
currentPath = parentPath;
|
|
1006
|
+
}
|
|
1007
|
+
const mountIgnorePatterns = this.options.ignore || [];
|
|
1008
|
+
for (const dirPath of dirsToCheck) {
|
|
1009
|
+
if (this.useGitignore) {
|
|
1010
|
+
const gitignoreContent = await this.readGitignoreContent(dirPath);
|
|
1011
|
+
if (gitignoreContent) {
|
|
1012
|
+
const gitignorePatterns = gitignoreContent.split("\n");
|
|
1013
|
+
this.addPatternsToIgnore(ig, gitignorePatterns, dirPath, mountRoot);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (this.useAfsignore) {
|
|
1017
|
+
const afsignoreContent = await this.readAfsignoreContent(dirPath);
|
|
1018
|
+
if (afsignoreContent) {
|
|
1019
|
+
const afsignorePatterns = await this.parseAfsignoreContent(afsignoreContent, dirPath, mountRoot);
|
|
1020
|
+
this.addPatternsToIgnore(ig, afsignorePatterns, dirPath, mountRoot);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
const negationPatterns = [];
|
|
1025
|
+
const collectNegationPatterns = (content) => {
|
|
1026
|
+
for (const line of content.split("\n")) {
|
|
1027
|
+
const trimmed = line.trim();
|
|
1028
|
+
if (trimmed.startsWith("!") && !trimmed.startsWith("!#")) negationPatterns.push(trimmed.slice(1));
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
for (const dirPath of dirsToCheck) if (this.useAfsignore) {
|
|
1032
|
+
const afsignoreContent = await this.readAfsignoreContent(dirPath);
|
|
1033
|
+
if (afsignoreContent) collectNegationPatterns(afsignoreContent);
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
ig,
|
|
1037
|
+
ignoreBase: mountRoot,
|
|
1038
|
+
mountIgnorePatterns,
|
|
1039
|
+
negationPatterns
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Check if an ignored directory might have un-ignored children based on negation patterns.
|
|
1044
|
+
* This allows recursing into ignored directories when negation patterns exist for their contents.
|
|
1045
|
+
*/
|
|
1046
|
+
hasNegatedDescendants(relativePath, negationPatterns) {
|
|
1047
|
+
const normalizedPath = relativePath.replace(/\/$/, "");
|
|
1048
|
+
for (const pattern of negationPatterns) {
|
|
1049
|
+
const normalizedPattern = pattern.replace(/\/$/, "");
|
|
1050
|
+
if (normalizedPattern.startsWith(`${normalizedPath}/`) || normalizedPattern === normalizedPath) return true;
|
|
1051
|
+
const firstSegment = normalizedPath.split("/")[0] || "";
|
|
1052
|
+
if (pattern.includes("**") && firstSegment && pattern.startsWith(firstSegment)) return true;
|
|
1053
|
+
}
|
|
1054
|
+
return false;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Check if a path is explicitly negated by negation patterns.
|
|
1058
|
+
* This is used to override the ignore library's decision for paths that match negation patterns.
|
|
1059
|
+
*/
|
|
1060
|
+
isNegatedByPatterns(relativePath, negationPatterns) {
|
|
1061
|
+
const normalizedPath = relativePath.replace(/\/$/, "");
|
|
1062
|
+
for (const pattern of negationPatterns) {
|
|
1063
|
+
const normalizedPattern = pattern.replace(/\/$/, "");
|
|
1064
|
+
if (normalizedPattern === normalizedPath) return true;
|
|
1065
|
+
if (normalizedPattern.endsWith("/**") || normalizedPattern.endsWith("/*")) {
|
|
1066
|
+
const basePath = normalizedPattern.replace(/\/\*+$/, "");
|
|
1067
|
+
if (normalizedPath.startsWith(`${basePath}/`) || normalizedPath === basePath) return true;
|
|
1068
|
+
}
|
|
1069
|
+
if (normalizedPath.startsWith(`${normalizedPattern}/`)) return true;
|
|
1070
|
+
}
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Check if a file path should be ignored based on mount-level ignore patterns.
|
|
1075
|
+
* Mount-level ignores have highest priority and cannot be overridden.
|
|
1076
|
+
*/
|
|
1077
|
+
isIgnoredByMountPatterns(relativePath, mountIgnorePatterns) {
|
|
1078
|
+
if (mountIgnorePatterns.length === 0) return false;
|
|
1079
|
+
const mountIg = (0, ignore.default)();
|
|
1080
|
+
mountIg.add(mountIgnorePatterns);
|
|
1081
|
+
const pathWithoutSlash = relativePath.startsWith("/") ? relativePath.slice(1) : relativePath;
|
|
1082
|
+
return mountIg.ignores(pathWithoutSlash) || mountIg.ignores(`${pathWithoutSlash}/`);
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
require_decorate.__decorate([(0, _aigne_afs.Meta)("/:path*")], AFSFS.prototype, "readMetaHandler", null);
|
|
1086
|
+
require_decorate.__decorate([(0, _aigne_afs.List)("/:path*")], AFSFS.prototype, "listHandler", null);
|
|
1087
|
+
require_decorate.__decorate([(0, _aigne_afs.Read)("/:path*")], AFSFS.prototype, "readHandler", null);
|
|
1088
|
+
require_decorate.__decorate([(0, _aigne_afs.Write)("/:path*")], AFSFS.prototype, "writeHandler", null);
|
|
1089
|
+
require_decorate.__decorate([(0, _aigne_afs.Delete)("/:path*")], AFSFS.prototype, "deleteHandler", null);
|
|
1090
|
+
require_decorate.__decorate([(0, _aigne_afs.Rename)("/:path*")], AFSFS.prototype, "renameHandler", null);
|
|
1091
|
+
require_decorate.__decorate([(0, _aigne_afs.Search)("/:path*")], AFSFS.prototype, "searchHandler", null);
|
|
1092
|
+
require_decorate.__decorate([(0, _aigne_afs.Stat)("/:path*")], AFSFS.prototype, "statHandler", null);
|
|
1093
|
+
require_decorate.__decorate([(0, _aigne_afs.Explain)("/:path*")], AFSFS.prototype, "explainHandler", null);
|
|
1094
|
+
require_decorate.__decorate([(0, _aigne_afs.Read)("/.meta/.capabilities")], AFSFS.prototype, "readCapabilitiesHandler", null);
|
|
1095
|
+
require_decorate.__decorate([_aigne_afs.Actions.Exec("/:path*", "archive")], AFSFS.prototype, "archiveAction", null);
|
|
1096
|
+
require_decorate.__decorate([_aigne_afs.Actions.Exec("/:path*", "checksum")], AFSFS.prototype, "checksumAction", null);
|
|
1097
|
+
var src_default = AFSFS;
|
|
1098
|
+
|
|
1099
|
+
//#endregion
|
|
1100
|
+
exports.AFSFS = AFSFS;
|
|
1101
|
+
exports.default = src_default;
|