@aigne/afs 1.11.0-beta.2 → 1.11.0-beta.4

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 CHANGED
@@ -1,4 +1,5 @@
1
1
  const require_error = require('./error.cjs');
2
+ const require_path = require('./path.cjs');
2
3
  const require_type = require('./type.cjs');
3
4
  let _aigne_uuid = require("@aigne/uuid");
4
5
  let strict_event_emitter = require("strict-event-emitter");
@@ -8,14 +9,62 @@ let zod = require("zod");
8
9
  //#region src/afs.ts
9
10
  const DEFAULT_MAX_DEPTH = 1;
10
11
  const MODULES_ROOT_DIR = "/modules";
12
+ /**
13
+ * Characters forbidden in namespace names (security-sensitive)
14
+ */
15
+ const NAMESPACE_FORBIDDEN_CHARS = [
16
+ "/",
17
+ "\\",
18
+ ":",
19
+ ";",
20
+ "|",
21
+ "&",
22
+ "`",
23
+ "$",
24
+ "(",
25
+ ")",
26
+ ">",
27
+ "<",
28
+ "\n",
29
+ "\r",
30
+ " ",
31
+ "\0"
32
+ ];
33
+ /**
34
+ * Validate a namespace name for mount operations
35
+ * @throws Error if namespace is invalid
36
+ */
37
+ function validateNamespaceName(namespace) {
38
+ if (namespace.trim() === "") throw new Error("Namespace cannot be empty or whitespace-only");
39
+ for (const char of NAMESPACE_FORBIDDEN_CHARS) if (namespace.includes(char)) throw new Error(`Namespace contains forbidden character: '${char}'`);
40
+ }
11
41
  var AFS = class extends strict_event_emitter.Emitter {
12
42
  name = "AFSRoot";
13
43
  constructor(options = {}) {
14
44
  super();
15
45
  this.options = options;
16
- for (const module of options?.modules ?? []) this.mount(module);
46
+ for (const module of options?.modules ?? []) this.mount(module, (0, ufo.joinURL)(MODULES_ROOT_DIR, module.name));
47
+ }
48
+ /**
49
+ * Internal storage: Map<compositeKey, MountEntry>
50
+ * compositeKey = `${namespace ?? ""}:${path}`
51
+ */
52
+ mounts = /* @__PURE__ */ new Map();
53
+ /**
54
+ * Legacy compatibility: Map<path, AFSModule> for modules mounted via old API
55
+ * This is used internally by findModules for backward compatibility
56
+ */
57
+ get modules() {
58
+ const map = /* @__PURE__ */ new Map();
59
+ for (const entry of this.mounts.values()) if (entry.namespace === null) map.set(entry.path, entry.module);
60
+ return map;
61
+ }
62
+ /**
63
+ * Create composite key for mount storage
64
+ */
65
+ makeKey(namespace, path) {
66
+ return `${namespace ?? ""}:${path}`;
17
67
  }
18
- modules = /* @__PURE__ */ new Map();
19
68
  /**
20
69
  * Check if write operations are allowed for the given module.
21
70
  * Throws AFSReadonlyError if not allowed.
@@ -23,57 +72,197 @@ var AFS = class extends strict_event_emitter.Emitter {
23
72
  checkWritePermission(module, operation, path) {
24
73
  if (module.accessMode !== "readwrite") throw new require_error.AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
25
74
  }
26
- mount(module) {
27
- if (module.name.includes("/")) throw new Error(`Invalid module name: ${module.name}. Module name must not contain '/'`);
28
- const path = (0, ufo.joinURL)(MODULES_ROOT_DIR, module.name);
29
- if (this.modules.has(path)) throw new Error(`Module already mounted at path: ${path}`);
30
- this.modules.set(path, module);
75
+ /**
76
+ * Mount a module at a path in a namespace
77
+ *
78
+ * @param module - The module to mount
79
+ * @param path - The path to mount at (optional, defaults to /modules/{module.name} for backward compatibility)
80
+ * @param options - Mount options (namespace, replace)
81
+ */
82
+ mount(module, path, options) {
83
+ require_path.validateModuleName(module.name);
84
+ const normalizedPath = require_path.validatePath(path ?? (0, ufo.joinURL)(MODULES_ROOT_DIR, module.name));
85
+ const namespace = options?.namespace === void 0 ? null : options.namespace;
86
+ if (namespace !== null) {
87
+ if (namespace === "") throw new Error("Namespace cannot be empty or whitespace-only");
88
+ validateNamespaceName(namespace);
89
+ }
90
+ const key = this.makeKey(namespace, normalizedPath);
91
+ if (this.mounts.has(key)) if (options?.replace) this.mounts.delete(key);
92
+ else throw new Error(`Mount conflict: path '${normalizedPath}' already mounted in namespace '${namespace ?? "default"}'`);
93
+ for (const entry of this.mounts.values()) {
94
+ if (entry.namespace !== namespace) continue;
95
+ const existingPath = entry.path;
96
+ if (normalizedPath === "/" || existingPath === "/") throw new Error(`Mount conflict: path '${normalizedPath}' conflicts with existing mount '${existingPath}' in namespace '${namespace ?? "default"}'`);
97
+ 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
+ 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
+ }
100
+ this.mounts.set(key, {
101
+ namespace,
102
+ path: normalizedPath,
103
+ module
104
+ });
31
105
  module.onMount?.(this);
32
106
  return this;
33
107
  }
108
+ /**
109
+ * Get all mounts, optionally filtered by namespace
110
+ *
111
+ * @param namespace - Filter by namespace (undefined = all, null = default only)
112
+ */
113
+ getMounts(namespace) {
114
+ const result = [];
115
+ for (const entry of this.mounts.values()) if (namespace === void 0 || entry.namespace === namespace) result.push({
116
+ namespace: entry.namespace,
117
+ path: entry.path,
118
+ module: entry.module
119
+ });
120
+ return result;
121
+ }
122
+ /**
123
+ * Get all unique namespaces that have mounts
124
+ */
125
+ getNamespaces() {
126
+ const namespaces = /* @__PURE__ */ new Set();
127
+ for (const entry of this.mounts.values()) namespaces.add(entry.namespace);
128
+ return Array.from(namespaces);
129
+ }
130
+ /**
131
+ * Unmount a module at a path in a namespace
132
+ *
133
+ * @param path - The path to unmount
134
+ * @param namespace - The namespace (undefined/null for default namespace)
135
+ * @returns true if unmounted, false if not found
136
+ */
137
+ unmount(path, namespace) {
138
+ const normalizedPath = require_path.validatePath(path);
139
+ const ns = namespace === void 0 ? null : namespace;
140
+ const key = this.makeKey(ns, normalizedPath);
141
+ if (this.mounts.has(key)) {
142
+ this.mounts.delete(key);
143
+ return true;
144
+ }
145
+ return false;
146
+ }
147
+ /**
148
+ * Check if a path is mounted in a namespace
149
+ *
150
+ * @param path - The path to check
151
+ * @param namespace - The namespace (undefined/null for default namespace)
152
+ */
153
+ isMounted(path, namespace) {
154
+ const normalizedPath = require_path.validatePath(path);
155
+ const ns = namespace === void 0 ? null : namespace;
156
+ const key = this.makeKey(ns, normalizedPath);
157
+ return this.mounts.has(key);
158
+ }
34
159
  async listModules() {
35
- return Array.from(this.modules.entries()).map(([path, module]) => ({
36
- path,
37
- name: module.name,
38
- description: module.description,
39
- module
160
+ return Array.from(this.mounts.values()).map((entry) => ({
161
+ path: entry.path,
162
+ namespace: entry.namespace,
163
+ name: entry.module.name,
164
+ description: entry.module.description,
165
+ module: entry.module
40
166
  }));
41
167
  }
168
+ /**
169
+ * Parse a path and extract namespace if it's a canonical path
170
+ * Returns the namespace and the path within the namespace
171
+ */
172
+ parsePathWithNamespace(inputPath) {
173
+ if (require_path.isCanonicalPath(inputPath)) {
174
+ const parsed = require_path.parseCanonicalPath(inputPath);
175
+ return {
176
+ namespace: parsed.namespace,
177
+ path: parsed.path
178
+ };
179
+ }
180
+ return {
181
+ namespace: null,
182
+ path: require_path.validatePath(inputPath)
183
+ };
184
+ }
185
+ /**
186
+ * Find modules that can handle a path in a specific namespace
187
+ */
188
+ findModulesInNamespace(path, namespace, options) {
189
+ const maxDepth = Math.max(options?.maxDepth ?? DEFAULT_MAX_DEPTH, 1);
190
+ const matched = [];
191
+ for (const entry of this.mounts.values()) {
192
+ if (entry.namespace !== namespace) continue;
193
+ const modulePath = entry.path;
194
+ const module = entry.module;
195
+ const pathSegments = path.split("/").filter(Boolean);
196
+ const modulePathSegments = modulePath.split("/").filter(Boolean);
197
+ let newMaxDepth;
198
+ let subpath;
199
+ let remainedModulePath;
200
+ if (!options?.exactMatch && modulePath.startsWith(path)) {
201
+ newMaxDepth = Math.max(0, maxDepth - (modulePathSegments.length - pathSegments.length));
202
+ subpath = "/";
203
+ remainedModulePath = (0, ufo.joinURL)("/", ...modulePathSegments.slice(pathSegments.length).slice(0, maxDepth));
204
+ } else if (path.startsWith(modulePath)) {
205
+ newMaxDepth = maxDepth;
206
+ subpath = (0, ufo.joinURL)("/", ...pathSegments.slice(modulePathSegments.length));
207
+ remainedModulePath = "/";
208
+ } else continue;
209
+ if (newMaxDepth < 0) continue;
210
+ matched.push({
211
+ module,
212
+ modulePath,
213
+ maxDepth: newMaxDepth,
214
+ subpath,
215
+ remainedModulePath
216
+ });
217
+ }
218
+ return matched;
219
+ }
42
220
  async list(path, options = {}) {
221
+ const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
43
222
  let preset;
44
223
  if (options.preset) {
45
224
  preset = this.options?.context?.list?.presets?.[options.preset];
46
225
  if (!preset) throw new Error(`Preset not found: ${options.preset}`);
47
226
  }
48
- return await this.processWithPreset(path, void 0, preset, {
227
+ return await this.processWithPreset(normalizedPath, void 0, preset, {
49
228
  ...options,
50
- defaultSelect: () => this._list(path, options)
229
+ defaultSelect: () => this._list(normalizedPath, namespace, options)
51
230
  });
52
231
  }
53
- async _list(path, options = {}) {
232
+ async _list(path, namespace, options = {}) {
54
233
  const results = [];
55
- if (path === "/" && this.modules.size > 0) {
234
+ const hasModulesMounts = namespace === null && [...this.mounts.values()].some((m) => m.namespace === null && m.path.startsWith(`${MODULES_ROOT_DIR}/`));
235
+ if (path === "/" && hasModulesMounts) {
56
236
  const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
57
237
  results.push({
58
238
  id: "modules",
59
239
  path: MODULES_ROOT_DIR,
60
- summary: "All mounted modules"
240
+ summary: "All mounted modules",
241
+ metadata: {
242
+ type: "directory",
243
+ description: "All mounted modules"
244
+ }
61
245
  });
62
246
  if (maxDepth === 1) return { data: results };
63
- const childrenResult = await this._list(MODULES_ROOT_DIR, {
247
+ const childrenResult = await this._list(MODULES_ROOT_DIR, namespace, {
64
248
  ...options,
65
249
  maxDepth: maxDepth - 1
66
250
  });
67
251
  results.push(...childrenResult.data);
68
252
  return { data: results };
69
253
  }
70
- const matches = this.findModules(path, options);
254
+ const matches = this.findModulesInNamespace(path, namespace, options);
255
+ if (matches.length === 0) return { data: results };
71
256
  for (const matched of matches) {
72
257
  if (matched.maxDepth === 0) {
73
258
  const moduleEntry = {
74
259
  id: matched.module.name,
75
260
  path: matched.modulePath,
76
- summary: matched.module.description
261
+ summary: matched.module.description,
262
+ metadata: {
263
+ type: "module",
264
+ description: matched.module.description
265
+ }
77
266
  };
78
267
  results.push(moduleEntry);
79
268
  continue;
@@ -96,7 +285,8 @@ var AFS = class extends strict_event_emitter.Emitter {
96
285
  return { data: results };
97
286
  }
98
287
  async read(path, _options) {
99
- const modules = this.findModules(path, { exactMatch: true });
288
+ const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
289
+ const modules = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true });
100
290
  for (const { module, modulePath, subpath } of modules) {
101
291
  const res = await module.read?.(subpath);
102
292
  if (res?.data) return {
@@ -113,8 +303,9 @@ var AFS = class extends strict_event_emitter.Emitter {
113
303
  };
114
304
  }
115
305
  async write(path, content, options) {
116
- const module = this.findModules(path, { exactMatch: true })[0];
117
- if (!module?.module.write) throw new Error(`No module found for path: ${path}`);
306
+ const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
307
+ const module = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true })[0];
308
+ if (!module?.module.write) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
118
309
  this.checkWritePermission(module.module, "write", path);
119
310
  const res = await module.module.write(module.subpath, content, options);
120
311
  return {
@@ -126,28 +317,33 @@ var AFS = class extends strict_event_emitter.Emitter {
126
317
  };
127
318
  }
128
319
  async delete(path, options) {
129
- const module = this.findModules(path, { exactMatch: true })[0];
130
- if (!module?.module.delete) throw new Error(`No module found for path: ${path}`);
320
+ const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
321
+ const module = this.findModulesInNamespace(normalizedPath, namespace, { exactMatch: true })[0];
322
+ if (!module?.module.delete) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
131
323
  this.checkWritePermission(module.module, "delete", path);
132
324
  return await module.module.delete(module.subpath, options);
133
325
  }
134
326
  async rename(oldPath, newPath, options) {
135
- const oldModule = this.findModules(oldPath, { exactMatch: true })[0];
136
- const newModule = this.findModules(newPath, { exactMatch: true })[0];
327
+ const { namespace: oldNamespace, path: normalizedOldPath } = this.parsePathWithNamespace(oldPath);
328
+ const { namespace: newNamespace, path: normalizedNewPath } = this.parsePathWithNamespace(newPath);
329
+ if (oldNamespace !== newNamespace) throw new Error(`Cannot rename across different namespaces.`);
330
+ const oldModule = this.findModulesInNamespace(normalizedOldPath, oldNamespace, { exactMatch: true })[0];
331
+ const newModule = this.findModulesInNamespace(normalizedNewPath, newNamespace, { exactMatch: true })[0];
137
332
  if (!oldModule || !newModule || oldModule.modulePath !== newModule.modulePath) throw new Error(`Cannot rename across different modules. Both paths must be in the same module.`);
138
333
  if (!oldModule.module.rename) throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
139
334
  this.checkWritePermission(oldModule.module, "rename", oldPath);
140
335
  return await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
141
336
  }
142
337
  async search(path, query, options = {}) {
338
+ const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
143
339
  let preset;
144
340
  if (options.preset) {
145
341
  preset = this.options?.context?.search?.presets?.[options.preset];
146
342
  if (!preset) throw new Error(`Preset not found: ${options.preset}`);
147
343
  }
148
- return await this.processWithPreset(path, query, preset, {
344
+ return await this.processWithPreset(normalizedPath, query, preset, {
149
345
  ...options,
150
- defaultSelect: () => this._search(path, query, options)
346
+ defaultSelect: () => this._search(normalizedPath, namespace, query, options)
151
347
  });
152
348
  }
153
349
  async processWithPreset(path, query, preset, options) {
@@ -174,10 +370,10 @@ var AFS = class extends strict_event_emitter.Emitter {
174
370
  }, options);
175
371
  return { data: (await Promise.all(data.map((p) => this.read(p).then((res) => res.data)))).filter((i) => !!i) };
176
372
  }
177
- async _search(path, query, options) {
373
+ async _search(path, namespace, query, options) {
178
374
  const results = [];
179
375
  const messages = [];
180
- for (const { module, modulePath, subpath } of this.findModules(path)) {
376
+ for (const { module, modulePath, subpath } of this.findModulesInNamespace(path, namespace)) {
181
377
  if (!module.search) continue;
182
378
  try {
183
379
  const { data, message } = await module.search(subpath, query, options);
@@ -195,38 +391,10 @@ var AFS = class extends strict_event_emitter.Emitter {
195
391
  message: messages.join("; ")
196
392
  };
197
393
  }
198
- findModules(path, options) {
199
- const maxDepth = Math.max(options?.maxDepth ?? DEFAULT_MAX_DEPTH, 1);
200
- const matched = [];
201
- for (const [modulePath, module] of this.modules) {
202
- const pathSegments = path.split("/").filter(Boolean);
203
- const modulePathSegments = modulePath.split("/").filter(Boolean);
204
- let newMaxDepth;
205
- let subpath;
206
- let remainedModulePath;
207
- if (!options?.exactMatch && modulePath.startsWith(path)) {
208
- newMaxDepth = Math.max(0, maxDepth - (modulePathSegments.length - pathSegments.length));
209
- subpath = "/";
210
- remainedModulePath = (0, ufo.joinURL)("/", ...modulePathSegments.slice(pathSegments.length).slice(0, maxDepth));
211
- } else if (path.startsWith(modulePath)) {
212
- newMaxDepth = maxDepth;
213
- subpath = (0, ufo.joinURL)("/", ...pathSegments.slice(modulePathSegments.length));
214
- remainedModulePath = "/";
215
- } else continue;
216
- if (newMaxDepth < 0) continue;
217
- matched.push({
218
- module,
219
- modulePath,
220
- maxDepth: newMaxDepth,
221
- subpath,
222
- remainedModulePath
223
- });
224
- }
225
- return matched;
226
- }
227
394
  async exec(path, args, options) {
228
- const module = this.findModules(path)[0];
229
- if (!module?.module.exec) throw new Error(`No module found for path: ${path}`);
395
+ const { namespace, path: normalizedPath } = this.parsePathWithNamespace(path);
396
+ const module = this.findModulesInNamespace(normalizedPath, namespace)[0];
397
+ if (!module?.module.exec) throw new Error(`No module found for path: ${normalizedPath} in namespace '${namespace ?? "default"}'`);
230
398
  return await module.module.exec(module.subpath, args, options);
231
399
  }
232
400
  buildSimpleListView(entries) {
@@ -276,10 +444,11 @@ var AFS = class extends strict_event_emitter.Emitter {
276
444
  const fs = await import("node:fs/promises");
277
445
  const rootDir = path.join(os.tmpdir(), (0, _aigne_uuid.v7)());
278
446
  await fs.mkdir(rootDir, { recursive: true });
279
- for (const [modulePath, module] of this.modules) {
280
- const physicalModulePath = path.join(rootDir, modulePath);
447
+ for (const entry of this.mounts.values()) {
448
+ const namespacePart = entry.namespace ?? "_default";
449
+ const physicalModulePath = path.join(rootDir, namespacePart, entry.path);
281
450
  await fs.mkdir(path.dirname(physicalModulePath), { recursive: true });
282
- await module.symlinkToPhysical?.(physicalModulePath);
451
+ await entry.module.symlinkToPhysical?.(physicalModulePath);
283
452
  }
284
453
  return rootDir;
285
454
  })();
package/dist/afs.d.cts CHANGED
@@ -2,6 +2,26 @@ import { AFSContext, AFSDeleteOptions, AFSDeleteResult, AFSExecOptions, AFSExecR
2
2
  import { Emitter } from "strict-event-emitter";
3
3
 
4
4
  //#region src/afs.d.ts
5
+ /**
6
+ * Information about a mounted module
7
+ */
8
+ interface MountInfo {
9
+ /** The namespace (null for default namespace) */
10
+ namespace: string | null;
11
+ /** The mount path within the namespace */
12
+ path: string;
13
+ /** The mounted module */
14
+ module: AFSModule;
15
+ }
16
+ /**
17
+ * Options for mounting a module
18
+ */
19
+ interface MountOptions {
20
+ /** Namespace to mount into (null/undefined for default namespace) */
21
+ namespace?: string | null;
22
+ /** Replace existing mount at the same path */
23
+ replace?: boolean;
24
+ }
5
25
  interface AFSOptions {
6
26
  modules?: AFSModule[];
7
27
  context?: AFSContext;
@@ -10,19 +30,74 @@ declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
10
30
  options: AFSOptions;
11
31
  name: string;
12
32
  constructor(options?: AFSOptions);
13
- private modules;
33
+ /**
34
+ * Internal storage: Map<compositeKey, MountEntry>
35
+ * compositeKey = `${namespace ?? ""}:${path}`
36
+ */
37
+ private mounts;
38
+ /**
39
+ * Legacy compatibility: Map<path, AFSModule> for modules mounted via old API
40
+ * This is used internally by findModules for backward compatibility
41
+ */
42
+ private get modules();
43
+ /**
44
+ * Create composite key for mount storage
45
+ */
46
+ private makeKey;
14
47
  /**
15
48
  * Check if write operations are allowed for the given module.
16
49
  * Throws AFSReadonlyError if not allowed.
17
50
  */
18
51
  private checkWritePermission;
19
- mount(module: AFSModule): this;
52
+ /**
53
+ * Mount a module at a path in a namespace
54
+ *
55
+ * @param module - The module to mount
56
+ * @param path - The path to mount at (optional, defaults to /modules/{module.name} for backward compatibility)
57
+ * @param options - Mount options (namespace, replace)
58
+ */
59
+ mount(module: AFSModule, path?: string, options?: MountOptions): this;
60
+ /**
61
+ * Get all mounts, optionally filtered by namespace
62
+ *
63
+ * @param namespace - Filter by namespace (undefined = all, null = default only)
64
+ */
65
+ getMounts(namespace?: string | null): MountInfo[];
66
+ /**
67
+ * Get all unique namespaces that have mounts
68
+ */
69
+ getNamespaces(): (string | null)[];
70
+ /**
71
+ * Unmount a module at a path in a namespace
72
+ *
73
+ * @param path - The path to unmount
74
+ * @param namespace - The namespace (undefined/null for default namespace)
75
+ * @returns true if unmounted, false if not found
76
+ */
77
+ unmount(path: string, namespace?: string | null): boolean;
78
+ /**
79
+ * Check if a path is mounted in a namespace
80
+ *
81
+ * @param path - The path to check
82
+ * @param namespace - The namespace (undefined/null for default namespace)
83
+ */
84
+ isMounted(path: string, namespace?: string | null): boolean;
20
85
  listModules(): Promise<{
21
86
  name: string;
22
87
  path: string;
88
+ namespace: string | null;
23
89
  description?: string;
24
90
  module: AFSModule;
25
91
  }[]>;
92
+ /**
93
+ * Parse a path and extract namespace if it's a canonical path
94
+ * Returns the namespace and the path within the namespace
95
+ */
96
+ private parsePathWithNamespace;
97
+ /**
98
+ * Find modules that can handle a path in a specific namespace
99
+ */
100
+ private findModulesInNamespace;
26
101
  list(path: string, options?: AFSRootListOptions): Promise<AFSRootListResult>;
27
102
  private _list;
28
103
  read(path: string, _options?: AFSReadOptions): Promise<AFSReadResult>;
@@ -33,7 +108,6 @@ declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
33
108
  private processWithPreset;
34
109
  private _select;
35
110
  private _search;
36
- private findModules;
37
111
  exec(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
38
112
  private buildSimpleListView;
39
113
  private buildTreeView;
@@ -43,5 +117,5 @@ declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
43
117
  cleanupPhysicalPath(): Promise<void>;
44
118
  }
45
119
  //#endregion
46
- export { AFS, AFSOptions };
120
+ export { AFS, AFSOptions, MountInfo, MountOptions };
47
121
  //# sourceMappingURL=afs.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"afs.d.cts","names":[],"sources":["../src/afs.ts"],"mappings":";;;;UAqCiB,UAAA;EAAA,OAAA,GACL,SAAA;EAAA,OAAA,GACA,UAAA;AAAA;AAAA,cAGC,GAAA,SAAY,OAAA,CAAQ,aAAA,aAA0B,OAAA;EAAA,OAAA,EAG7B,UAAA;EAAA,IAAA;EAAA,YAAA,OAAA,GAAA,UAAA;EAAA,QAAA,OAAA;EAAA;;;;EAAA,QAAA,oBAAA;EAAA,MAAA,MAAA,EAuBd,SAAA;EAAA,YAAA,GAiBO,OAAA;IAAA,IAAA;IAAA,IAAA;IAAA,WAAA;IAAA,MAAA,EACyC,SAAA;EAAA;EAAA,KAAA,IAAA,UAAA,OAAA,GAU5B,kBAAA,GAA0B,OAAA,CAAQ,iBAAA;EAAA,QAAA,KAAA;EAAA,KAAA,IAAA,UAAA,QAAA,GAiFhC,cAAA,GAAiB,OAAA,CAAQ,aAAA;EAAA,MAAA,IAAA,UAAA,OAAA,EAsBlD,oBAAA,EAAA,OAAA,GACC,eAAA,GACT,OAAA,CAAQ,cAAA;EAAA,OAAA,IAAA,UAAA,OAAA,GAiB0B,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAAA,OAAA,OAAA,UAAA,OAAA,UAAA,OAAA,GAYpD,gBAAA,GACT,OAAA,CAAQ,eAAA;EAAA,OAAA,IAAA,UAAA,KAAA,UAAA,OAAA,GAuBA,oBAAA,GACR,OAAA,CAAQ,mBAAA;EAAA,QAAA,iBAAA;EAAA,QAAA,OAAA;EAAA,QAAA,OAAA;EAAA,QAAA,WAAA;EAAA,KAAA,IAAA,UAAA,IAAA,EAoJH,MAAA,eAAA,OAAA,EACG,cAAA,GACR,OAAA,CAAQ,aAAA;EAAA,QAAA,mBAAA;EAAA,QAAA,aAAA;EAAA,QAAA,mBAAA;EAAA,QAAA,YAAA;EAAA,uBAAA,GA8EqB,OAAA;EAAA,oBAAA,GAqBH,OAAA;AAAA"}
1
+ {"version":3,"file":"afs.d.cts","names":[],"sources":["../src/afs.ts"],"mappings":";;;;;AA+EA;AAYA;UAZiB,SAAA;EAAA;EAAA,SAAA;EAAA;EAAA,IAAA;EAAA;EAAA,MAAA,EAMP,SAAA;AAAA;AAAA;AAMV;AAgBA;AAtBU,UAMO,YAAA;EAAA;EAAA,SAAA;EAAA;EAAA,OAAA;AAAA;AAAA,UAgBA,UAAA;EAAA,OAAA,GACL,SAAA;EAAA,OAAA,GACA,UAAA;AAAA;AAAA,cAGC,GAAA,SAAY,OAAA,CAAQ,aAAA,aAA0B,OAAA;EAAA,OAAA,EAG7B,UAAA;EAAA,IAAA;EAAA,YAAA,OAAA,GAAA,UAAA;EAAA;;;;EAAA,QAAA,MAAA;EAAA;;;;EAAA,YAAA,QAAA;EAAA;;;EAAA,QAAA,OAAA;EAAA;;;;EAAA,QAAA,oBAAA;EAAA;;;;;;;EAAA,MAAA,MAAA,EAwDd,SAAA,EAAA,IAAA,WAAA,OAAA,GAAoC,YAAA;EAAA;;;;;EAAA,UAAA,SAAA,mBAyFZ,SAAA;EAAA;;;EAAA,cAAA;EAAA;;;;;;;EAAA,QAAA,IAAA,UAAA,SAAA;EAAA;;;;;;EAAA,UAAA,IAAA,UAAA,SAAA;EAAA,YAAA,GA2DjB,OAAA;IAAA,IAAA;IAAA,IAAA;IAAA,SAAA;IAAA,WAAA;IAAA,MAAA,EAMT,SAAA;EAAA;EAAA;;;;EAAA,QAAA,sBAAA;EAAA;;;EAAA,QAAA,sBAAA;EAAA,KAAA,IAAA,UAAA,OAAA,GAqFsB,kBAAA,GAA0B,OAAA,CAAQ,iBAAA;EAAA,QAAA,KAAA;EAAA,KAAA,IAAA,UAAA,QAAA,GA6GhC,cAAA,GAAiB,OAAA,CAAQ,aAAA;EAAA,MAAA,IAAA,UAAA,OAAA,EAyBlD,oBAAA,EAAA,OAAA,GACC,eAAA,GACT,OAAA,CAAQ,cAAA;EAAA,OAAA,IAAA,UAAA,OAAA,GAuB0B,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAAA,OAAA,OAAA,UAAA,OAAA,UAAA,OAAA,GAkBpD,gBAAA,GACT,OAAA,CAAQ,eAAA;EAAA,OAAA,IAAA,UAAA,KAAA,UAAA,OAAA,GAsCA,oBAAA,GACR,OAAA,CAAQ,mBAAA;EAAA,QAAA,iBAAA;EAAA,QAAA,OAAA;EAAA,QAAA,OAAA;EAAA,KAAA,IAAA,UAAA,IAAA,EAsGH,MAAA,eAAA,OAAA,EACG,cAAA,GACR,OAAA,CAAQ,aAAA;EAAA,QAAA,mBAAA;EAAA,QAAA,aAAA;EAAA,QAAA,mBAAA;EAAA,QAAA,YAAA;EAAA,uBAAA,GAoFqB,OAAA;EAAA,oBAAA,GAuBH,OAAA;AAAA"}
package/dist/afs.d.mts CHANGED
@@ -2,6 +2,26 @@ import { AFSContext, AFSDeleteOptions, AFSDeleteResult, AFSExecOptions, AFSExecR
2
2
  import { Emitter } from "strict-event-emitter";
3
3
 
4
4
  //#region src/afs.d.ts
5
+ /**
6
+ * Information about a mounted module
7
+ */
8
+ interface MountInfo {
9
+ /** The namespace (null for default namespace) */
10
+ namespace: string | null;
11
+ /** The mount path within the namespace */
12
+ path: string;
13
+ /** The mounted module */
14
+ module: AFSModule;
15
+ }
16
+ /**
17
+ * Options for mounting a module
18
+ */
19
+ interface MountOptions {
20
+ /** Namespace to mount into (null/undefined for default namespace) */
21
+ namespace?: string | null;
22
+ /** Replace existing mount at the same path */
23
+ replace?: boolean;
24
+ }
5
25
  interface AFSOptions {
6
26
  modules?: AFSModule[];
7
27
  context?: AFSContext;
@@ -10,19 +30,74 @@ declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
10
30
  options: AFSOptions;
11
31
  name: string;
12
32
  constructor(options?: AFSOptions);
13
- private modules;
33
+ /**
34
+ * Internal storage: Map<compositeKey, MountEntry>
35
+ * compositeKey = `${namespace ?? ""}:${path}`
36
+ */
37
+ private mounts;
38
+ /**
39
+ * Legacy compatibility: Map<path, AFSModule> for modules mounted via old API
40
+ * This is used internally by findModules for backward compatibility
41
+ */
42
+ private get modules();
43
+ /**
44
+ * Create composite key for mount storage
45
+ */
46
+ private makeKey;
14
47
  /**
15
48
  * Check if write operations are allowed for the given module.
16
49
  * Throws AFSReadonlyError if not allowed.
17
50
  */
18
51
  private checkWritePermission;
19
- mount(module: AFSModule): this;
52
+ /**
53
+ * Mount a module at a path in a namespace
54
+ *
55
+ * @param module - The module to mount
56
+ * @param path - The path to mount at (optional, defaults to /modules/{module.name} for backward compatibility)
57
+ * @param options - Mount options (namespace, replace)
58
+ */
59
+ mount(module: AFSModule, path?: string, options?: MountOptions): this;
60
+ /**
61
+ * Get all mounts, optionally filtered by namespace
62
+ *
63
+ * @param namespace - Filter by namespace (undefined = all, null = default only)
64
+ */
65
+ getMounts(namespace?: string | null): MountInfo[];
66
+ /**
67
+ * Get all unique namespaces that have mounts
68
+ */
69
+ getNamespaces(): (string | null)[];
70
+ /**
71
+ * Unmount a module at a path in a namespace
72
+ *
73
+ * @param path - The path to unmount
74
+ * @param namespace - The namespace (undefined/null for default namespace)
75
+ * @returns true if unmounted, false if not found
76
+ */
77
+ unmount(path: string, namespace?: string | null): boolean;
78
+ /**
79
+ * Check if a path is mounted in a namespace
80
+ *
81
+ * @param path - The path to check
82
+ * @param namespace - The namespace (undefined/null for default namespace)
83
+ */
84
+ isMounted(path: string, namespace?: string | null): boolean;
20
85
  listModules(): Promise<{
21
86
  name: string;
22
87
  path: string;
88
+ namespace: string | null;
23
89
  description?: string;
24
90
  module: AFSModule;
25
91
  }[]>;
92
+ /**
93
+ * Parse a path and extract namespace if it's a canonical path
94
+ * Returns the namespace and the path within the namespace
95
+ */
96
+ private parsePathWithNamespace;
97
+ /**
98
+ * Find modules that can handle a path in a specific namespace
99
+ */
100
+ private findModulesInNamespace;
26
101
  list(path: string, options?: AFSRootListOptions): Promise<AFSRootListResult>;
27
102
  private _list;
28
103
  read(path: string, _options?: AFSReadOptions): Promise<AFSReadResult>;
@@ -33,7 +108,6 @@ declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
33
108
  private processWithPreset;
34
109
  private _select;
35
110
  private _search;
36
- private findModules;
37
111
  exec(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
38
112
  private buildSimpleListView;
39
113
  private buildTreeView;
@@ -43,5 +117,5 @@ declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
43
117
  cleanupPhysicalPath(): Promise<void>;
44
118
  }
45
119
  //#endregion
46
- export { AFS, AFSOptions };
120
+ export { AFS, AFSOptions, MountInfo, MountOptions };
47
121
  //# sourceMappingURL=afs.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"afs.d.mts","names":[],"sources":["../src/afs.ts"],"mappings":";;;;UAqCiB,UAAA;EAAA,OAAA,GACL,SAAA;EAAA,OAAA,GACA,UAAA;AAAA;AAAA,cAGC,GAAA,SAAY,OAAA,CAAQ,aAAA,aAA0B,OAAA;EAAA,OAAA,EAG7B,UAAA;EAAA,IAAA;EAAA,YAAA,OAAA,GAAA,UAAA;EAAA,QAAA,OAAA;EAAA;;;;EAAA,QAAA,oBAAA;EAAA,MAAA,MAAA,EAuBd,SAAA;EAAA,YAAA,GAiBO,OAAA;IAAA,IAAA;IAAA,IAAA;IAAA,WAAA;IAAA,MAAA,EACyC,SAAA;EAAA;EAAA,KAAA,IAAA,UAAA,OAAA,GAU5B,kBAAA,GAA0B,OAAA,CAAQ,iBAAA;EAAA,QAAA,KAAA;EAAA,KAAA,IAAA,UAAA,QAAA,GAiFhC,cAAA,GAAiB,OAAA,CAAQ,aAAA;EAAA,MAAA,IAAA,UAAA,OAAA,EAsBlD,oBAAA,EAAA,OAAA,GACC,eAAA,GACT,OAAA,CAAQ,cAAA;EAAA,OAAA,IAAA,UAAA,OAAA,GAiB0B,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAAA,OAAA,OAAA,UAAA,OAAA,UAAA,OAAA,GAYpD,gBAAA,GACT,OAAA,CAAQ,eAAA;EAAA,OAAA,IAAA,UAAA,KAAA,UAAA,OAAA,GAuBA,oBAAA,GACR,OAAA,CAAQ,mBAAA;EAAA,QAAA,iBAAA;EAAA,QAAA,OAAA;EAAA,QAAA,OAAA;EAAA,QAAA,WAAA;EAAA,KAAA,IAAA,UAAA,IAAA,EAoJH,MAAA,eAAA,OAAA,EACG,cAAA,GACR,OAAA,CAAQ,aAAA;EAAA,QAAA,mBAAA;EAAA,QAAA,aAAA;EAAA,QAAA,mBAAA;EAAA,QAAA,YAAA;EAAA,uBAAA,GA8EqB,OAAA;EAAA,oBAAA,GAqBH,OAAA;AAAA"}
1
+ {"version":3,"file":"afs.d.mts","names":[],"sources":["../src/afs.ts"],"mappings":";;;;;AA+EA;AAYA;UAZiB,SAAA;EAAA;EAAA,SAAA;EAAA;EAAA,IAAA;EAAA;EAAA,MAAA,EAMP,SAAA;AAAA;AAAA;AAMV;AAgBA;AAtBU,UAMO,YAAA;EAAA;EAAA,SAAA;EAAA;EAAA,OAAA;AAAA;AAAA,UAgBA,UAAA;EAAA,OAAA,GACL,SAAA;EAAA,OAAA,GACA,UAAA;AAAA;AAAA,cAGC,GAAA,SAAY,OAAA,CAAQ,aAAA,aAA0B,OAAA;EAAA,OAAA,EAG7B,UAAA;EAAA,IAAA;EAAA,YAAA,OAAA,GAAA,UAAA;EAAA;;;;EAAA,QAAA,MAAA;EAAA;;;;EAAA,YAAA,QAAA;EAAA;;;EAAA,QAAA,OAAA;EAAA;;;;EAAA,QAAA,oBAAA;EAAA;;;;;;;EAAA,MAAA,MAAA,EAwDd,SAAA,EAAA,IAAA,WAAA,OAAA,GAAoC,YAAA;EAAA;;;;;EAAA,UAAA,SAAA,mBAyFZ,SAAA;EAAA;;;EAAA,cAAA;EAAA;;;;;;;EAAA,QAAA,IAAA,UAAA,SAAA;EAAA;;;;;;EAAA,UAAA,IAAA,UAAA,SAAA;EAAA,YAAA,GA2DjB,OAAA;IAAA,IAAA;IAAA,IAAA;IAAA,SAAA;IAAA,WAAA;IAAA,MAAA,EAMT,SAAA;EAAA;EAAA;;;;EAAA,QAAA,sBAAA;EAAA;;;EAAA,QAAA,sBAAA;EAAA,KAAA,IAAA,UAAA,OAAA,GAqFsB,kBAAA,GAA0B,OAAA,CAAQ,iBAAA;EAAA,QAAA,KAAA;EAAA,KAAA,IAAA,UAAA,QAAA,GA6GhC,cAAA,GAAiB,OAAA,CAAQ,aAAA;EAAA,MAAA,IAAA,UAAA,OAAA,EAyBlD,oBAAA,EAAA,OAAA,GACC,eAAA,GACT,OAAA,CAAQ,cAAA;EAAA,OAAA,IAAA,UAAA,OAAA,GAuB0B,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAAA,OAAA,OAAA,UAAA,OAAA,UAAA,OAAA,GAkBpD,gBAAA,GACT,OAAA,CAAQ,eAAA;EAAA,OAAA,IAAA,UAAA,KAAA,UAAA,OAAA,GAsCA,oBAAA,GACR,OAAA,CAAQ,mBAAA;EAAA,QAAA,iBAAA;EAAA,QAAA,OAAA;EAAA,QAAA,OAAA;EAAA,KAAA,IAAA,UAAA,IAAA,EAsGH,MAAA,eAAA,OAAA,EACG,cAAA,GACR,OAAA,CAAQ,aAAA;EAAA,QAAA,mBAAA;EAAA,QAAA,aAAA;EAAA,QAAA,mBAAA;EAAA,QAAA,YAAA;EAAA,uBAAA,GAoFqB,OAAA;EAAA,oBAAA,GAuBH,OAAA;AAAA"}