@aigne/afs 1.11.0-beta.5 → 1.11.0-beta.7
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/dist/afs.cjs +463 -110
- package/dist/afs.d.cts +140 -53
- package/dist/afs.d.cts.map +1 -1
- package/dist/afs.d.mts +140 -53
- package/dist/afs.d.mts.map +1 -1
- package/dist/afs.mjs +464 -111
- package/dist/afs.mjs.map +1 -1
- package/dist/capabilities/index.d.mts +2 -0
- package/dist/capabilities/types.d.cts +100 -0
- package/dist/capabilities/types.d.cts.map +1 -0
- package/dist/capabilities/types.d.mts +100 -0
- package/dist/capabilities/types.d.mts.map +1 -0
- package/dist/capabilities/world-mapping.cjs +20 -0
- package/dist/capabilities/world-mapping.d.cts +139 -0
- package/dist/capabilities/world-mapping.d.cts.map +1 -0
- package/dist/capabilities/world-mapping.d.mts +139 -0
- package/dist/capabilities/world-mapping.d.mts.map +1 -0
- package/dist/capabilities/world-mapping.mjs +20 -0
- package/dist/capabilities/world-mapping.mjs.map +1 -0
- package/dist/error.cjs +38 -1
- package/dist/error.d.cts +23 -1
- package/dist/error.d.cts.map +1 -1
- package/dist/error.d.mts +23 -1
- package/dist/error.d.mts.map +1 -1
- package/dist/error.mjs +35 -1
- package/dist/error.mjs.map +1 -1
- package/dist/index.cjs +55 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.mts +17 -3
- package/dist/index.mjs +13 -3
- package/dist/loader/index.cjs +67 -0
- package/dist/loader/index.d.cts +48 -0
- package/dist/loader/index.d.cts.map +1 -0
- package/dist/loader/index.d.mts +48 -0
- package/dist/loader/index.d.mts.map +1 -0
- package/dist/loader/index.mjs +66 -0
- package/dist/loader/index.mjs.map +1 -0
- package/dist/meta/index.cjs +4 -0
- package/dist/meta/index.mjs +6 -0
- package/dist/meta/kind.cjs +161 -0
- package/dist/meta/kind.d.cts +134 -0
- package/dist/meta/kind.d.cts.map +1 -0
- package/dist/meta/kind.d.mts +134 -0
- package/dist/meta/kind.d.mts.map +1 -0
- package/dist/meta/kind.mjs +157 -0
- package/dist/meta/kind.mjs.map +1 -0
- package/dist/meta/path.cjs +116 -0
- package/dist/meta/path.d.cts +43 -0
- package/dist/meta/path.d.cts.map +1 -0
- package/dist/meta/path.d.mts +43 -0
- package/dist/meta/path.d.mts.map +1 -0
- package/dist/meta/path.mjs +112 -0
- package/dist/meta/path.mjs.map +1 -0
- package/dist/meta/type.d.cts +96 -0
- package/dist/meta/type.d.cts.map +1 -0
- package/dist/meta/type.d.mts +96 -0
- package/dist/meta/type.d.mts.map +1 -0
- package/dist/meta/validation.cjs +77 -0
- package/dist/meta/validation.d.cts +19 -0
- package/dist/meta/validation.d.cts.map +1 -0
- package/dist/meta/validation.d.mts +19 -0
- package/dist/meta/validation.d.mts.map +1 -0
- package/dist/meta/validation.mjs +77 -0
- package/dist/meta/validation.mjs.map +1 -0
- package/dist/meta/well-known-kinds.cjs +228 -0
- package/dist/meta/well-known-kinds.d.cts +52 -0
- package/dist/meta/well-known-kinds.d.cts.map +1 -0
- package/dist/meta/well-known-kinds.d.mts +52 -0
- package/dist/meta/well-known-kinds.d.mts.map +1 -0
- package/dist/meta/well-known-kinds.mjs +219 -0
- package/dist/meta/well-known-kinds.mjs.map +1 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts +141 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts.map +1 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts +141 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts.map +1 -0
- package/dist/path.d.cts.map +1 -1
- package/dist/path.d.mts.map +1 -1
- package/dist/provider/base.cjs +425 -0
- package/dist/provider/base.d.cts +175 -0
- package/dist/provider/base.d.cts.map +1 -0
- package/dist/provider/base.d.mts +175 -0
- package/dist/provider/base.d.mts.map +1 -0
- package/dist/provider/base.mjs +426 -0
- package/dist/provider/base.mjs.map +1 -0
- package/dist/provider/decorators.cjs +268 -0
- package/dist/provider/decorators.d.cts +244 -0
- package/dist/provider/decorators.d.cts.map +1 -0
- package/dist/provider/decorators.d.mts +244 -0
- package/dist/provider/decorators.d.mts.map +1 -0
- package/dist/provider/decorators.mjs +256 -0
- package/dist/provider/decorators.mjs.map +1 -0
- package/dist/provider/index.cjs +19 -0
- package/dist/provider/index.d.cts +5 -0
- package/dist/provider/index.d.mts +5 -0
- package/dist/provider/index.mjs +5 -0
- package/dist/provider/router.cjs +185 -0
- package/dist/provider/router.d.cts +50 -0
- package/dist/provider/router.d.cts.map +1 -0
- package/dist/provider/router.d.mts +50 -0
- package/dist/provider/router.d.mts.map +1 -0
- package/dist/provider/router.mjs +185 -0
- package/dist/provider/router.mjs.map +1 -0
- package/dist/provider/types.d.cts +113 -0
- package/dist/provider/types.d.cts.map +1 -0
- package/dist/provider/types.d.mts +113 -0
- package/dist/provider/types.d.mts.map +1 -0
- package/dist/type.cjs +12 -3
- package/dist/type.d.cts +183 -100
- package/dist/type.d.cts.map +1 -1
- package/dist/type.d.mts +183 -100
- package/dist/type.d.mts.map +1 -1
- package/dist/type.mjs +12 -4
- package/dist/type.mjs.map +1 -1
- package/dist/utils/camelize.d.cts.map +1 -1
- package/dist/utils/camelize.d.mts.map +1 -1
- package/dist/utils/type-utils.d.cts.map +1 -1
- package/dist/utils/type-utils.d.mts.map +1 -1
- package/dist/utils/zod.d.cts.map +1 -1
- package/dist/utils/zod.d.mts.map +1 -1
- package/package.json +12 -1
package/dist/afs.cjs
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
const require_error = require('./error.cjs');
|
|
2
2
|
const require_path = require('./path.cjs');
|
|
3
|
-
const require_type = require('./type.cjs');
|
|
4
3
|
let _aigne_uuid = require("@aigne/uuid");
|
|
5
|
-
let strict_event_emitter = require("strict-event-emitter");
|
|
6
4
|
let ufo = require("ufo");
|
|
7
|
-
let zod = require("zod");
|
|
8
5
|
|
|
9
6
|
//#region src/afs.ts
|
|
10
7
|
const DEFAULT_MAX_DEPTH = 1;
|
|
11
8
|
const MODULES_ROOT_DIR = "/modules";
|
|
12
9
|
/**
|
|
10
|
+
* Default timeout for mount check operations (10 seconds)
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_MOUNT_TIMEOUT = 1e4;
|
|
13
|
+
/**
|
|
14
|
+
* Execute a promise with a timeout.
|
|
15
|
+
* Throws an error if the promise does not resolve within the timeout.
|
|
16
|
+
*/
|
|
17
|
+
async function withTimeout(promise, ms) {
|
|
18
|
+
let timeoutId;
|
|
19
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
20
|
+
timeoutId = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timeout after ${ms}ms`)), ms);
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
24
|
+
} finally {
|
|
25
|
+
clearTimeout(timeoutId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get the timeout value for a provider.
|
|
30
|
+
* Returns provider.timeout if set, otherwise DEFAULT_MOUNT_TIMEOUT.
|
|
31
|
+
*/
|
|
32
|
+
function getTimeout(provider) {
|
|
33
|
+
return provider.timeout ?? DEFAULT_MOUNT_TIMEOUT;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get error message from an error, handling timeout specially.
|
|
37
|
+
*/
|
|
38
|
+
function getMountErrorMessage(err, timeout) {
|
|
39
|
+
if (err instanceof Error) {
|
|
40
|
+
if (err.message.includes("Timeout")) return `Timeout after ${timeout}ms`;
|
|
41
|
+
return err.message;
|
|
42
|
+
}
|
|
43
|
+
return String(err);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
13
46
|
* Characters forbidden in namespace names (security-sensitive)
|
|
14
47
|
*/
|
|
15
48
|
const NAMESPACE_FORBIDDEN_CHARS = [
|
|
@@ -38,10 +71,19 @@ function validateNamespaceName(namespace) {
|
|
|
38
71
|
if (namespace.trim() === "") throw new Error("Namespace cannot be empty or whitespace-only");
|
|
39
72
|
for (const char of NAMESPACE_FORBIDDEN_CHARS) if (namespace.includes(char)) throw new Error(`Namespace contains forbidden character: '${char}'`);
|
|
40
73
|
}
|
|
41
|
-
var AFS = class
|
|
74
|
+
var AFS = class {
|
|
42
75
|
name = "AFSRoot";
|
|
76
|
+
/**
|
|
77
|
+
* Injectable method for loading and mounting a provider from a URI.
|
|
78
|
+
* Injected by CLI layer (afs-loader.ts) with full pipeline:
|
|
79
|
+
* 1. ProviderRegistry.createProvider({ uri, path }) → provider instance
|
|
80
|
+
* 2. afs.mount(provider, path)
|
|
81
|
+
* 3. Persist to config.toml
|
|
82
|
+
*
|
|
83
|
+
* Used by root-level /.actions/mount action.
|
|
84
|
+
*/
|
|
85
|
+
loadProvider;
|
|
43
86
|
constructor(options = {}) {
|
|
44
|
-
super();
|
|
45
87
|
this.options = options;
|
|
46
88
|
for (const module of options?.modules ?? []) this.mount(module, (0, ufo.joinURL)(MODULES_ROOT_DIR, module.name));
|
|
47
89
|
}
|
|
@@ -69,17 +111,62 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
69
111
|
* Check if write operations are allowed for the given module.
|
|
70
112
|
* Throws AFSReadonlyError if not allowed.
|
|
71
113
|
*/
|
|
114
|
+
/** Fire-and-forget change notification */
|
|
115
|
+
notifyChange(record) {
|
|
116
|
+
try {
|
|
117
|
+
this.options.onChange?.(record);
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
72
120
|
checkWritePermission(module, operation, path) {
|
|
73
121
|
if (module.accessMode !== "readwrite") throw new require_error.AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
|
|
74
122
|
}
|
|
75
123
|
/**
|
|
124
|
+
* Check provider availability on mount.
|
|
125
|
+
* Validates that the provider can successfully respond to stat/read
|
|
126
|
+
* and list (if childrenCount indicates children exist).
|
|
127
|
+
*
|
|
128
|
+
* @throws AFSMountError if validation fails
|
|
129
|
+
*/
|
|
130
|
+
async checkProviderOnMount(provider) {
|
|
131
|
+
const timeout = getTimeout(provider);
|
|
132
|
+
const name = provider.name;
|
|
133
|
+
let rootData;
|
|
134
|
+
if (provider.stat) try {
|
|
135
|
+
rootData = (await withTimeout(provider.stat("/"), timeout)).data;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new require_error.AFSMountError(name, "stat", getMountErrorMessage(err, timeout));
|
|
138
|
+
}
|
|
139
|
+
else if (provider.read) try {
|
|
140
|
+
const result = await withTimeout(provider.read("/"), timeout);
|
|
141
|
+
if (result.data) rootData = {
|
|
142
|
+
path: result.data.path,
|
|
143
|
+
meta: result.data.meta ?? void 0
|
|
144
|
+
};
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw new require_error.AFSMountError(name, "read", getMountErrorMessage(err, timeout));
|
|
147
|
+
}
|
|
148
|
+
else throw new require_error.AFSMountError(name, "read", "Provider has no stat or read method");
|
|
149
|
+
if (!rootData) throw new require_error.AFSMountError(name, provider.stat ? "stat" : "read", "Root path returned undefined data");
|
|
150
|
+
const childrenCount = rootData.meta?.childrenCount;
|
|
151
|
+
if (childrenCount === -1 || typeof childrenCount === "number" && childrenCount > 0) {
|
|
152
|
+
if (!provider.list) throw new require_error.AFSMountError(name, "list", "Provider has childrenCount but no list method");
|
|
153
|
+
try {
|
|
154
|
+
const listResult = await withTimeout(provider.list("/"), timeout);
|
|
155
|
+
if (!listResult.data || listResult.data.length === 0) throw new require_error.AFSMountError(name, "list", "childrenCount indicates children but list returned empty");
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err instanceof require_error.AFSMountError) throw err;
|
|
158
|
+
throw new require_error.AFSMountError(name, "list", getMountErrorMessage(err, timeout));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
76
163
|
* Mount a module at a path in a namespace
|
|
77
164
|
*
|
|
78
165
|
* @param module - The module to mount
|
|
79
166
|
* @param path - The path to mount at (optional, defaults to /modules/{module.name} for backward compatibility)
|
|
80
167
|
* @param options - Mount options (namespace, replace)
|
|
81
168
|
*/
|
|
82
|
-
mount(module, path, options) {
|
|
169
|
+
async mount(module, path, options) {
|
|
83
170
|
require_path.validateModuleName(module.name);
|
|
84
171
|
const normalizedPath = require_path.validatePath(path ?? (0, ufo.joinURL)(MODULES_ROOT_DIR, module.name));
|
|
85
172
|
const namespace = options?.namespace === void 0 ? null : options.namespace;
|
|
@@ -97,12 +184,20 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
97
184
|
if (existingPath.startsWith(normalizedPath) && (existingPath === normalizedPath || existingPath.length === normalizedPath.length || existingPath[normalizedPath.length] === "/")) throw new Error(`Mount conflict: path '${normalizedPath}' conflicts with existing mount '${existingPath}' in namespace '${namespace ?? "default"}'`);
|
|
98
185
|
if (normalizedPath.startsWith(existingPath) && (normalizedPath === existingPath || normalizedPath.length === existingPath.length || normalizedPath[existingPath.length] === "/")) throw new Error(`Mount conflict: path '${normalizedPath}' conflicts with existing mount '${existingPath}' in namespace '${namespace ?? "default"}'`);
|
|
99
186
|
}
|
|
187
|
+
await this.checkProviderOnMount(module);
|
|
100
188
|
this.mounts.set(key, {
|
|
101
189
|
namespace,
|
|
102
190
|
path: normalizedPath,
|
|
103
191
|
module
|
|
104
192
|
});
|
|
105
193
|
module.onMount?.(this);
|
|
194
|
+
this.notifyChange({
|
|
195
|
+
kind: "mount",
|
|
196
|
+
path: normalizedPath,
|
|
197
|
+
moduleName: module.name,
|
|
198
|
+
namespace,
|
|
199
|
+
timestamp: Date.now()
|
|
200
|
+
});
|
|
106
201
|
return this;
|
|
107
202
|
}
|
|
108
203
|
/**
|
|
@@ -138,8 +233,16 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
138
233
|
const normalizedPath = require_path.validatePath(path);
|
|
139
234
|
const ns = namespace === void 0 ? null : namespace;
|
|
140
235
|
const key = this.makeKey(ns, normalizedPath);
|
|
141
|
-
|
|
236
|
+
const entry = this.mounts.get(key);
|
|
237
|
+
if (entry) {
|
|
142
238
|
this.mounts.delete(key);
|
|
239
|
+
this.notifyChange({
|
|
240
|
+
kind: "unmount",
|
|
241
|
+
path: normalizedPath,
|
|
242
|
+
moduleName: entry.module.name,
|
|
243
|
+
namespace: entry.namespace,
|
|
244
|
+
timestamp: Date.now()
|
|
245
|
+
});
|
|
143
246
|
return true;
|
|
144
247
|
}
|
|
145
248
|
return false;
|
|
@@ -197,11 +300,13 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
197
300
|
let newMaxDepth;
|
|
198
301
|
let subpath;
|
|
199
302
|
let remainedModulePath;
|
|
200
|
-
|
|
303
|
+
const moduleUnderPath = !options?.exactMatch && modulePath.startsWith(path) && (modulePath === path || path === "/" || modulePath[path.length] === "/");
|
|
304
|
+
const pathUnderModule = path.startsWith(modulePath) && (path === modulePath || modulePath === "/" || path[modulePath.length] === "/");
|
|
305
|
+
if (moduleUnderPath) {
|
|
201
306
|
newMaxDepth = Math.max(0, maxDepth - (modulePathSegments.length - pathSegments.length));
|
|
202
307
|
subpath = "/";
|
|
203
308
|
remainedModulePath = (0, ufo.joinURL)("/", ...modulePathSegments.slice(pathSegments.length).slice(0, maxDepth));
|
|
204
|
-
} else if (
|
|
309
|
+
} else if (pathUnderModule) {
|
|
205
310
|
newMaxDepth = maxDepth;
|
|
206
311
|
subpath = (0, ufo.joinURL)("/", ...pathSegments.slice(modulePathSegments.length));
|
|
207
312
|
remainedModulePath = "/";
|
|
@@ -219,27 +324,22 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
219
324
|
}
|
|
220
325
|
async list(path, options = {}) {
|
|
221
326
|
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
222
|
-
|
|
223
|
-
if (options.preset) {
|
|
224
|
-
preset = this.options?.context?.list?.presets?.[options.preset];
|
|
225
|
-
if (!preset) throw new Error(`Preset not found: ${options.preset}`);
|
|
226
|
-
}
|
|
227
|
-
return await this.processWithPreset(normalizedPath, void 0, preset, {
|
|
228
|
-
...options,
|
|
229
|
-
defaultSelect: () => this._list(normalizedPath, namespace, options)
|
|
230
|
-
});
|
|
327
|
+
return await this._list(normalizedPath, namespace, options);
|
|
231
328
|
}
|
|
232
329
|
async _list(path, namespace, options = {}) {
|
|
330
|
+
if (options?.maxDepth === 0) return { data: [] };
|
|
233
331
|
const results = [];
|
|
234
332
|
const hasModulesMounts = namespace === null && [...this.mounts.values()].some((m) => m.namespace === null && m.path.startsWith(`${MODULES_ROOT_DIR}/`));
|
|
235
333
|
if (path === "/" && hasModulesMounts) {
|
|
236
334
|
const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
335
|
+
let moduleCount = 0;
|
|
336
|
+
for (const entry of this.mounts.values()) if (entry.namespace === namespace) moduleCount++;
|
|
237
337
|
results.push({
|
|
238
338
|
id: "modules",
|
|
239
339
|
path: MODULES_ROOT_DIR,
|
|
240
340
|
summary: "All mounted modules",
|
|
241
|
-
|
|
242
|
-
|
|
341
|
+
meta: {
|
|
342
|
+
childrenCount: moduleCount,
|
|
243
343
|
description: "All mounted modules"
|
|
244
344
|
}
|
|
245
345
|
});
|
|
@@ -259,48 +359,191 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
259
359
|
id: matched.module.name,
|
|
260
360
|
path: matched.modulePath,
|
|
261
361
|
summary: matched.module.description,
|
|
262
|
-
|
|
263
|
-
|
|
362
|
+
meta: {
|
|
363
|
+
childrenCount: -1,
|
|
264
364
|
description: matched.module.description
|
|
265
365
|
}
|
|
266
366
|
};
|
|
267
367
|
results.push(moduleEntry);
|
|
268
368
|
continue;
|
|
269
369
|
}
|
|
270
|
-
if (!matched.module.list)
|
|
370
|
+
if (!matched.module.list) {
|
|
371
|
+
if (matched.module.read) try {
|
|
372
|
+
const childrenCount = (await matched.module.read(matched.subpath)).data?.meta?.childrenCount;
|
|
373
|
+
if (childrenCount === void 0 || childrenCount === 0) continue;
|
|
374
|
+
throw new Error(`Provider '${matched.module.name}' has childrenCount=${childrenCount} but does not implement list(). Providers with children must implement the list() method.`);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error instanceof Error && error.message.includes("does not implement list")) throw error;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
271
381
|
try {
|
|
272
|
-
const
|
|
382
|
+
const result = await matched.module.list(matched.subpath, {
|
|
273
383
|
...options,
|
|
274
384
|
maxDepth: matched.maxDepth
|
|
275
385
|
});
|
|
276
|
-
const children = data.map((entry) => ({
|
|
386
|
+
const children = result.data.map((entry) => ({
|
|
277
387
|
...entry,
|
|
278
388
|
path: (0, ufo.joinURL)(matched.modulePath, entry.path)
|
|
279
389
|
}));
|
|
280
390
|
results.push(...children);
|
|
391
|
+
if (result.message && children.length === 0) return {
|
|
392
|
+
data: results,
|
|
393
|
+
message: result.message
|
|
394
|
+
};
|
|
281
395
|
} catch (error) {
|
|
282
396
|
throw new Error(`Error listing from module at ${matched.modulePath}: ${error.message}`);
|
|
283
397
|
}
|
|
284
398
|
}
|
|
285
399
|
return { data: results };
|
|
286
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Check if a path should skip auto-enrichment.
|
|
403
|
+
* Paths ending with /.meta or /.actions should not be enriched
|
|
404
|
+
* to avoid recursive fetches, but their children (e.g., /.meta/kinds)
|
|
405
|
+
* can still be enriched.
|
|
406
|
+
*/
|
|
407
|
+
shouldSkipEnrich(path) {
|
|
408
|
+
return path.endsWith("/.meta") || path.endsWith("/.actions");
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Fetch actions for a path by listing path/.actions.
|
|
412
|
+
* Returns ActionSummary[] on success, [] on failure.
|
|
413
|
+
*/
|
|
414
|
+
async fetchActions(module, subpath) {
|
|
415
|
+
try {
|
|
416
|
+
const actionsPath = (0, ufo.joinURL)(subpath, ".actions");
|
|
417
|
+
const result = await module.list?.(actionsPath);
|
|
418
|
+
if (!result?.data) return [];
|
|
419
|
+
return result.data.filter((entry) => entry.meta?.kind === "afs:executable").map((entry) => ({
|
|
420
|
+
name: entry.id,
|
|
421
|
+
description: entry.meta?.description,
|
|
422
|
+
inputSchema: entry.meta?.inputSchema
|
|
423
|
+
}));
|
|
424
|
+
} catch {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Fetch meta for a path by reading path/.meta.
|
|
430
|
+
* Returns the meta content on success, null on failure.
|
|
431
|
+
*/
|
|
432
|
+
async fetchMeta(module, subpath) {
|
|
433
|
+
try {
|
|
434
|
+
const metaPath = (0, ufo.joinURL)(subpath, ".meta");
|
|
435
|
+
const result = await module.read?.(metaPath);
|
|
436
|
+
if (!result?.data?.content) return null;
|
|
437
|
+
const content = result.data.content;
|
|
438
|
+
if (typeof content === "object" && content !== null && !Array.isArray(content)) return content;
|
|
439
|
+
return null;
|
|
440
|
+
} catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Type for data that can be enriched (has path, optional actions, optional meta)
|
|
446
|
+
*/
|
|
447
|
+
async enrichData(data, module, subpath) {
|
|
448
|
+
if (this.shouldSkipEnrich(subpath)) return data;
|
|
449
|
+
const result = { ...data };
|
|
450
|
+
const enrichPromises = [];
|
|
451
|
+
if (result.actions === void 0) enrichPromises.push(this.fetchActions(module, subpath).then((actions) => {
|
|
452
|
+
result.actions = actions;
|
|
453
|
+
}));
|
|
454
|
+
if (result.meta?.kind === void 0) enrichPromises.push(this.fetchMeta(module, subpath).then((meta) => {
|
|
455
|
+
if (meta) result.meta = {
|
|
456
|
+
...result.meta,
|
|
457
|
+
...meta
|
|
458
|
+
};
|
|
459
|
+
}));
|
|
460
|
+
await Promise.all(enrichPromises);
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
287
463
|
async read(path, _options) {
|
|
288
464
|
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
465
|
+
if (normalizedPath === "/.meta/.capabilities") return { data: {
|
|
466
|
+
id: ".capabilities",
|
|
467
|
+
path: "/.meta/.capabilities",
|
|
468
|
+
content: await this.aggregateCapabilities(namespace),
|
|
469
|
+
meta: { kind: "afs:capabilities" }
|
|
470
|
+
} };
|
|
289
471
|
const modules = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true });
|
|
290
472
|
for (const { module, modulePath, subpath } of modules) {
|
|
291
473
|
const res = await module.read?.(subpath);
|
|
292
|
-
if (res?.data)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
...res
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
474
|
+
if (res?.data) {
|
|
475
|
+
const enrichedData = await this.enrichData(res.data, module, subpath);
|
|
476
|
+
return {
|
|
477
|
+
...res,
|
|
478
|
+
data: {
|
|
479
|
+
...enrichedData,
|
|
480
|
+
path: (0, ufo.joinURL)(modulePath, res.data.path)
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
299
484
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
485
|
+
throw new require_error.AFSNotFoundError(path);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Aggregate capabilities from all mounted providers.
|
|
489
|
+
*
|
|
490
|
+
* For each provider:
|
|
491
|
+
* - Read /.meta/.capabilities
|
|
492
|
+
* - Merge tools with provider prefix and mount path prefix
|
|
493
|
+
* - Merge actions with mount path prefix on discovery.pathTemplate
|
|
494
|
+
* - Silently skip providers that fail or don't implement capabilities
|
|
495
|
+
*/
|
|
496
|
+
async aggregateCapabilities(namespace) {
|
|
497
|
+
const allTools = [];
|
|
498
|
+
const allActions = [];
|
|
499
|
+
const skipped = [];
|
|
500
|
+
const allOperations = [];
|
|
501
|
+
const mounts = this.getMounts(namespace);
|
|
502
|
+
for (const mount of mounts) {
|
|
503
|
+
const { path: mountPath, module: provider } = mount;
|
|
504
|
+
try {
|
|
505
|
+
const content = (await provider.read?.("/.meta/.capabilities"))?.data?.content;
|
|
506
|
+
if (!content) continue;
|
|
507
|
+
const manifest = content;
|
|
508
|
+
for (const tool of manifest.tools ?? []) allTools.push({
|
|
509
|
+
...tool,
|
|
510
|
+
name: `${manifest.provider}.${tool.name}`,
|
|
511
|
+
path: (0, ufo.joinURL)(mountPath, tool.path)
|
|
512
|
+
});
|
|
513
|
+
for (const actionCatalog of manifest.actions ?? []) allActions.push({
|
|
514
|
+
...actionCatalog,
|
|
515
|
+
discovery: {
|
|
516
|
+
...actionCatalog.discovery,
|
|
517
|
+
pathTemplate: (0, ufo.joinURL)(mountPath, actionCatalog.discovery.pathTemplate)
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
if (manifest.operations) allOperations.push(manifest.operations);
|
|
521
|
+
} catch {
|
|
522
|
+
skipped.push(mountPath);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const result = {
|
|
526
|
+
schemaVersion: 1,
|
|
527
|
+
provider: "afs",
|
|
528
|
+
description: "AFS aggregated capabilities",
|
|
529
|
+
tools: allTools,
|
|
530
|
+
actions: allActions
|
|
531
|
+
};
|
|
532
|
+
if (allOperations.length > 0) result.operations = {
|
|
533
|
+
read: allOperations.some((o) => o.read),
|
|
534
|
+
list: allOperations.some((o) => o.list),
|
|
535
|
+
write: allOperations.some((o) => o.write),
|
|
536
|
+
delete: allOperations.some((o) => o.delete),
|
|
537
|
+
search: allOperations.some((o) => o.search),
|
|
538
|
+
exec: allOperations.some((o) => o.exec),
|
|
539
|
+
stat: allOperations.some((o) => o.stat),
|
|
540
|
+
explain: allOperations.some((o) => o.explain)
|
|
303
541
|
};
|
|
542
|
+
if (skipped.length > 0) {
|
|
543
|
+
result.partial = true;
|
|
544
|
+
result.skipped = skipped;
|
|
545
|
+
}
|
|
546
|
+
return result;
|
|
304
547
|
}
|
|
305
548
|
async write(path, content, options) {
|
|
306
549
|
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
@@ -308,20 +551,34 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
308
551
|
if (!module?.module.write) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
|
|
309
552
|
this.checkWritePermission(module.module, "write", path);
|
|
310
553
|
const res = await module.module.write(module.subpath, content, options);
|
|
311
|
-
|
|
554
|
+
const result = {
|
|
312
555
|
...res,
|
|
313
556
|
data: {
|
|
314
557
|
...res.data,
|
|
315
558
|
path: (0, ufo.joinURL)(module.modulePath, res.data.path)
|
|
316
559
|
}
|
|
317
560
|
};
|
|
561
|
+
this.notifyChange({
|
|
562
|
+
kind: "write",
|
|
563
|
+
path: result.data.path,
|
|
564
|
+
moduleName: module.module.name,
|
|
565
|
+
timestamp: Date.now()
|
|
566
|
+
});
|
|
567
|
+
return result;
|
|
318
568
|
}
|
|
319
569
|
async delete(path, options) {
|
|
320
570
|
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
321
571
|
const module = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true })[0];
|
|
322
572
|
if (!module?.module.delete) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
|
|
323
573
|
this.checkWritePermission(module.module, "delete", path);
|
|
324
|
-
|
|
574
|
+
const result = await module.module.delete(module.subpath, options);
|
|
575
|
+
this.notifyChange({
|
|
576
|
+
kind: "delete",
|
|
577
|
+
path: (0, ufo.joinURL)(module.modulePath, module.subpath),
|
|
578
|
+
moduleName: module.module.name,
|
|
579
|
+
timestamp: Date.now()
|
|
580
|
+
});
|
|
581
|
+
return result;
|
|
325
582
|
}
|
|
326
583
|
async rename(oldPath, newPath, options) {
|
|
327
584
|
const { namespace: oldNamespace, path: normalizedOldPath } = this.parsePathWithNamespace(oldPath);
|
|
@@ -332,43 +589,20 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
332
589
|
if (!oldModule || !newModule || oldModule.modulePath !== newModule.modulePath) throw new Error(`Cannot rename across different modules. Both paths must be in the same module.`);
|
|
333
590
|
if (!oldModule.module.rename) throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
|
|
334
591
|
this.checkWritePermission(oldModule.module, "rename", oldPath);
|
|
335
|
-
|
|
592
|
+
const result = await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
|
|
593
|
+
this.notifyChange({
|
|
594
|
+
kind: "rename",
|
|
595
|
+
path: (0, ufo.joinURL)(oldModule.modulePath, oldModule.subpath),
|
|
596
|
+
moduleName: oldModule.module.name,
|
|
597
|
+
namespace: oldNamespace,
|
|
598
|
+
meta: { newPath: (0, ufo.joinURL)(newModule.modulePath, newModule.subpath) },
|
|
599
|
+
timestamp: Date.now()
|
|
600
|
+
});
|
|
601
|
+
return result;
|
|
336
602
|
}
|
|
337
603
|
async search(path, query, options = {}) {
|
|
338
604
|
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
339
|
-
|
|
340
|
-
if (options.preset) {
|
|
341
|
-
preset = this.options?.context?.search?.presets?.[options.preset];
|
|
342
|
-
if (!preset) throw new Error(`Preset not found: ${options.preset}`);
|
|
343
|
-
}
|
|
344
|
-
return await this.processWithPreset(normalizedPath, query, preset, {
|
|
345
|
-
...options,
|
|
346
|
-
defaultSelect: () => this._search(normalizedPath, namespace, query, options)
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
async processWithPreset(path, query, preset, options) {
|
|
350
|
-
const select = options.select || preset?.select;
|
|
351
|
-
const per = options.per || preset?.per;
|
|
352
|
-
const dedupe = options.dedupe || preset?.dedupe;
|
|
353
|
-
const format = options.format || preset?.format;
|
|
354
|
-
const entries = select ? (await this._select(path, query, select, options)).data : (await options.defaultSelect()).data;
|
|
355
|
-
const mapped = per ? await Promise.all(entries.map((data) => per.invoke({ data }, options).then((res) => res.data))) : entries;
|
|
356
|
-
const deduped = dedupe ? await dedupe.invoke({ data: mapped }, options).then((res) => res.data) : mapped;
|
|
357
|
-
let formatted = deduped;
|
|
358
|
-
if (format === "simple-list" || format === "tree") {
|
|
359
|
-
const valid = zod.z.array(require_type.afsEntrySchema).safeParse(deduped);
|
|
360
|
-
if (!valid.data) throw new Error("Tree format requires entries to be AFSEntry objects");
|
|
361
|
-
if (format === "tree") formatted = this.buildTreeView(valid.data);
|
|
362
|
-
else if (format === "simple-list") formatted = this.buildSimpleListView(valid.data);
|
|
363
|
-
} else if (typeof format === "object" && typeof format.invoke === "function") formatted = await format.invoke({ data: deduped }, options).then((res) => res.data);
|
|
364
|
-
return { data: formatted };
|
|
365
|
-
}
|
|
366
|
-
async _select(path, query, select, options) {
|
|
367
|
-
const { data } = await select.invoke({
|
|
368
|
-
path,
|
|
369
|
-
query
|
|
370
|
-
}, options);
|
|
371
|
-
return { data: (await Promise.all(data.map((p) => this.read(p).then((res) => res.data)))).filter((i) => !!i) };
|
|
605
|
+
return await this._search(normalizedPath, namespace, query, options);
|
|
372
606
|
}
|
|
373
607
|
async _search(path, namespace, query, options) {
|
|
374
608
|
const results = [];
|
|
@@ -391,50 +625,169 @@ var AFS = class extends strict_event_emitter.Emitter {
|
|
|
391
625
|
message: messages.join("; ")
|
|
392
626
|
};
|
|
393
627
|
}
|
|
394
|
-
async exec(path, args, options) {
|
|
628
|
+
async exec(path, args, options = {}) {
|
|
395
629
|
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
630
|
+
if (normalizedPath.startsWith("/.actions/")) return await this.execRootAction(normalizedPath, args);
|
|
396
631
|
const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
|
|
397
632
|
if (!module?.module.exec) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
633
|
+
this.checkWritePermission(module.module, "exec", path);
|
|
634
|
+
await this.validateExecInput(module.module, module.subpath, args);
|
|
635
|
+
const enhancedOptions = {
|
|
636
|
+
...options,
|
|
637
|
+
context: {
|
|
638
|
+
...options?.context,
|
|
639
|
+
afs: this
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
return await module.module.exec(module.subpath, args, enhancedOptions);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Handle root-level action execution (/.actions/*).
|
|
646
|
+
* Currently supports:
|
|
647
|
+
* - /.actions/mount: Load and mount a provider via loadProvider
|
|
648
|
+
*/
|
|
649
|
+
async execRootAction(path, args) {
|
|
650
|
+
const actionName = path.slice(10);
|
|
651
|
+
if (actionName === "mount") return await this.execRootMountAction(args);
|
|
652
|
+
throw new require_error.AFSNotFoundError(path, `Root action not found: ${actionName}`);
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Execute root-level mount action.
|
|
656
|
+
* Validates input and delegates to loadProvider.
|
|
657
|
+
*/
|
|
658
|
+
async execRootMountAction(args) {
|
|
659
|
+
if (typeof args.uri !== "string" || args.uri === "") throw new require_error.AFSValidationError("Input validation failed: uri: must be a non-empty string");
|
|
660
|
+
if (typeof args.path !== "string" || args.path === "") throw new require_error.AFSValidationError("Input validation failed: path: must be a non-empty string");
|
|
661
|
+
if (!this.loadProvider) throw new Error("loadProvider not configured");
|
|
662
|
+
await this.loadProvider(args.uri, args.path);
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
data: {
|
|
666
|
+
uri: args.uri,
|
|
667
|
+
path: args.path
|
|
413
668
|
}
|
|
414
|
-
}
|
|
415
|
-
const renderTree = (node, prefix = "", currentPath = "") => {
|
|
416
|
-
let result = "";
|
|
417
|
-
const keys = Object.keys(node);
|
|
418
|
-
keys.forEach((key, index) => {
|
|
419
|
-
const isLast = index === keys.length - 1;
|
|
420
|
-
const fullPath = currentPath ? `${currentPath}/${key}` : `/${key}`;
|
|
421
|
-
const entry = entryMap.get(fullPath);
|
|
422
|
-
result += `${prefix}${isLast ? "└── " : "├── "}${key}${entry ? this.buildMetadataSuffix(entry) : ""}`;
|
|
423
|
-
result += `\n`;
|
|
424
|
-
result += renderTree(node[key], `${prefix}${isLast ? " " : "│ "}`, fullPath);
|
|
425
|
-
});
|
|
426
|
-
return result;
|
|
427
669
|
};
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
if (
|
|
437
|
-
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Validate exec input args against inputSchema.
|
|
673
|
+
* Throws AFSValidationError if validation fails.
|
|
674
|
+
* Uses zod-from-json-schema for full JSON Schema validation.
|
|
675
|
+
*/
|
|
676
|
+
async validateExecInput(module, subpath, args) {
|
|
677
|
+
let inputSchema;
|
|
678
|
+
if (module.read) try {
|
|
679
|
+
inputSchema = (await module.read(subpath)).data?.meta?.inputSchema;
|
|
680
|
+
} catch {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (!inputSchema) return;
|
|
684
|
+
try {
|
|
685
|
+
const { convertJsonSchemaToZod } = await import("zod-from-json-schema");
|
|
686
|
+
convertJsonSchemaToZod(inputSchema).parse(args);
|
|
687
|
+
} catch (error) {
|
|
688
|
+
if (error instanceof Error && error.name === "ZodError") throw new require_error.AFSValidationError(`Input validation failed: ${error.issues.map((issue) => {
|
|
689
|
+
const path = issue.path.join(".");
|
|
690
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
691
|
+
}).join("; ")}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Get stat information for a path
|
|
696
|
+
*
|
|
697
|
+
* Resolution order:
|
|
698
|
+
* 1. Provider's stat() method (if implemented)
|
|
699
|
+
* 2. Fallback to read() - extracts stat data from AFSEntry
|
|
700
|
+
*
|
|
701
|
+
* This allows providers to implement only read() while stat() still works.
|
|
702
|
+
*/
|
|
703
|
+
async stat(path, options) {
|
|
704
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
705
|
+
const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
|
|
706
|
+
if (!module) throw new require_error.AFSNotFoundError(path);
|
|
707
|
+
if (module.module.stat) try {
|
|
708
|
+
const result = await module.module.stat(module.subpath, options);
|
|
709
|
+
if (result.data) {
|
|
710
|
+
const enrichedData = await this.enrichData(result.data, module.module, module.subpath);
|
|
711
|
+
return {
|
|
712
|
+
...result,
|
|
713
|
+
data: enrichedData
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
throw new require_error.AFSNotFoundError(path);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
if (error instanceof require_error.AFSNotFoundError) throw error;
|
|
719
|
+
}
|
|
720
|
+
if (module.module.read) {
|
|
721
|
+
const readResult = await module.module.read(module.subpath, options);
|
|
722
|
+
if (readResult.data) {
|
|
723
|
+
const { content: _content, ...statData } = readResult.data;
|
|
724
|
+
return { data: await this.enrichData(statData, module.module, module.subpath) };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
throw new require_error.AFSNotFoundError(path);
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Get human-readable explanation for a path
|
|
731
|
+
*
|
|
732
|
+
* Resolution order:
|
|
733
|
+
* 1. Provider's explain() method (if implemented)
|
|
734
|
+
* 2. Fallback to stat() - builds explanation from metadata
|
|
735
|
+
*
|
|
736
|
+
* This allows providers to skip implementing explain() while it still works.
|
|
737
|
+
*/
|
|
738
|
+
async explain(path, options) {
|
|
739
|
+
const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
|
|
740
|
+
const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
|
|
741
|
+
if (!module) throw new require_error.AFSNotFoundError(path);
|
|
742
|
+
if (module.module.explain) try {
|
|
743
|
+
return await module.module.explain(module.subpath, options);
|
|
744
|
+
} catch (error) {
|
|
745
|
+
if (error instanceof require_error.AFSNotFoundError) throw error;
|
|
746
|
+
}
|
|
747
|
+
const data = (await this.stat(path, options)).data;
|
|
748
|
+
if (data) {
|
|
749
|
+
const lines = [];
|
|
750
|
+
lines.push(`# ${normalizedPath}`);
|
|
751
|
+
lines.push("");
|
|
752
|
+
const meta = data.meta || {};
|
|
753
|
+
if (meta.size !== void 0) lines.push(`- **Size**: ${this.formatBytes(meta.size)}`);
|
|
754
|
+
if (meta.childrenCount !== void 0) lines.push(`- **Children**: ${meta.childrenCount} items`);
|
|
755
|
+
if (data.updatedAt) lines.push(`- **Modified**: ${data.updatedAt.toISOString()}`);
|
|
756
|
+
if (meta.description) {
|
|
757
|
+
lines.push("");
|
|
758
|
+
lines.push("## Description");
|
|
759
|
+
lines.push(String(meta.description));
|
|
760
|
+
}
|
|
761
|
+
if (meta.provider) lines.push(`- **Provider**: ${meta.provider}`);
|
|
762
|
+
if (meta.kind) lines.push(`- **Kind**: ${meta.kind}`);
|
|
763
|
+
if (meta.kinds && Array.isArray(meta.kinds)) lines.push(`- **Kinds**: ${meta.kinds.join(", ")}`);
|
|
764
|
+
if (data.actions && data.actions.length > 0) {
|
|
765
|
+
lines.push("");
|
|
766
|
+
lines.push("## Actions");
|
|
767
|
+
for (const action of data.actions) lines.push(`- **${action.name}**${action.description ? `: ${action.description}` : ""}`);
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
format: "markdown",
|
|
771
|
+
content: lines.join("\n")
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
throw new Error(`No explain or stat handler for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Format bytes to human-readable string
|
|
778
|
+
*/
|
|
779
|
+
formatBytes(bytes) {
|
|
780
|
+
if (bytes === 0) return "0 B";
|
|
781
|
+
const k = 1024;
|
|
782
|
+
const sizes = [
|
|
783
|
+
"B",
|
|
784
|
+
"KB",
|
|
785
|
+
"MB",
|
|
786
|
+
"GB",
|
|
787
|
+
"TB"
|
|
788
|
+
];
|
|
789
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
790
|
+
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
438
791
|
}
|
|
439
792
|
physicalPath;
|
|
440
793
|
async initializePhysicalPath() {
|