@aigne/afs 1.11.0-beta → 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.
Files changed (164) hide show
  1. package/LICENSE.md +17 -84
  2. package/README.md +4 -13
  3. package/dist/_virtual/rolldown_runtime.mjs +7 -0
  4. package/dist/afs.cjs +1330 -0
  5. package/dist/afs.d.cts +275 -0
  6. package/dist/afs.d.cts.map +1 -0
  7. package/dist/afs.d.mts +275 -0
  8. package/dist/afs.d.mts.map +1 -0
  9. package/dist/afs.mjs +1331 -0
  10. package/dist/afs.mjs.map +1 -0
  11. package/dist/capabilities/index.d.mts +2 -0
  12. package/dist/capabilities/types.d.cts +100 -0
  13. package/dist/capabilities/types.d.cts.map +1 -0
  14. package/dist/capabilities/types.d.mts +100 -0
  15. package/dist/capabilities/types.d.mts.map +1 -0
  16. package/dist/capabilities/world-mapping.cjs +20 -0
  17. package/dist/capabilities/world-mapping.d.cts +139 -0
  18. package/dist/capabilities/world-mapping.d.cts.map +1 -0
  19. package/dist/capabilities/world-mapping.d.mts +139 -0
  20. package/dist/capabilities/world-mapping.d.mts.map +1 -0
  21. package/dist/capabilities/world-mapping.mjs +20 -0
  22. package/dist/capabilities/world-mapping.mjs.map +1 -0
  23. package/dist/error.cjs +63 -0
  24. package/dist/error.d.cts +39 -0
  25. package/dist/error.d.cts.map +1 -0
  26. package/dist/error.d.mts +39 -0
  27. package/dist/error.d.mts.map +1 -0
  28. package/dist/error.mjs +59 -0
  29. package/dist/error.mjs.map +1 -0
  30. package/dist/index.cjs +72 -345
  31. package/dist/index.d.cts +18 -300
  32. package/dist/index.d.mts +20 -300
  33. package/dist/index.mjs +16 -342
  34. package/dist/loader/index.cjs +110 -0
  35. package/dist/loader/index.d.cts +48 -0
  36. package/dist/loader/index.d.cts.map +1 -0
  37. package/dist/loader/index.d.mts +48 -0
  38. package/dist/loader/index.d.mts.map +1 -0
  39. package/dist/loader/index.mjs +110 -0
  40. package/dist/loader/index.mjs.map +1 -0
  41. package/dist/meta/index.cjs +4 -0
  42. package/dist/meta/index.mjs +6 -0
  43. package/dist/meta/kind.cjs +161 -0
  44. package/dist/meta/kind.d.cts +134 -0
  45. package/dist/meta/kind.d.cts.map +1 -0
  46. package/dist/meta/kind.d.mts +134 -0
  47. package/dist/meta/kind.d.mts.map +1 -0
  48. package/dist/meta/kind.mjs +157 -0
  49. package/dist/meta/kind.mjs.map +1 -0
  50. package/dist/meta/path.cjs +116 -0
  51. package/dist/meta/path.d.cts +43 -0
  52. package/dist/meta/path.d.cts.map +1 -0
  53. package/dist/meta/path.d.mts +43 -0
  54. package/dist/meta/path.d.mts.map +1 -0
  55. package/dist/meta/path.mjs +112 -0
  56. package/dist/meta/path.mjs.map +1 -0
  57. package/dist/meta/type.d.cts +96 -0
  58. package/dist/meta/type.d.cts.map +1 -0
  59. package/dist/meta/type.d.mts +96 -0
  60. package/dist/meta/type.d.mts.map +1 -0
  61. package/dist/meta/validation.cjs +77 -0
  62. package/dist/meta/validation.d.cts +19 -0
  63. package/dist/meta/validation.d.cts.map +1 -0
  64. package/dist/meta/validation.d.mts +19 -0
  65. package/dist/meta/validation.d.mts.map +1 -0
  66. package/dist/meta/validation.mjs +77 -0
  67. package/dist/meta/validation.mjs.map +1 -0
  68. package/dist/meta/well-known-kinds.cjs +228 -0
  69. package/dist/meta/well-known-kinds.d.cts +52 -0
  70. package/dist/meta/well-known-kinds.d.cts.map +1 -0
  71. package/dist/meta/well-known-kinds.d.mts +52 -0
  72. package/dist/meta/well-known-kinds.d.mts.map +1 -0
  73. package/dist/meta/well-known-kinds.mjs +219 -0
  74. package/dist/meta/well-known-kinds.mjs.map +1 -0
  75. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts +141 -0
  76. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.cts.map +1 -0
  77. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts +141 -0
  78. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.mts.map +1 -0
  79. package/dist/path.cjs +255 -0
  80. package/dist/path.d.cts +93 -0
  81. package/dist/path.d.cts.map +1 -0
  82. package/dist/path.d.mts +93 -0
  83. package/dist/path.d.mts.map +1 -0
  84. package/dist/path.mjs +249 -0
  85. package/dist/path.mjs.map +1 -0
  86. package/dist/provider/base.cjs +425 -0
  87. package/dist/provider/base.d.cts +175 -0
  88. package/dist/provider/base.d.cts.map +1 -0
  89. package/dist/provider/base.d.mts +175 -0
  90. package/dist/provider/base.d.mts.map +1 -0
  91. package/dist/provider/base.mjs +426 -0
  92. package/dist/provider/base.mjs.map +1 -0
  93. package/dist/provider/decorators.cjs +268 -0
  94. package/dist/provider/decorators.d.cts +244 -0
  95. package/dist/provider/decorators.d.cts.map +1 -0
  96. package/dist/provider/decorators.d.mts +244 -0
  97. package/dist/provider/decorators.d.mts.map +1 -0
  98. package/dist/provider/decorators.mjs +256 -0
  99. package/dist/provider/decorators.mjs.map +1 -0
  100. package/dist/provider/index.cjs +19 -0
  101. package/dist/provider/index.d.cts +5 -0
  102. package/dist/provider/index.d.mts +5 -0
  103. package/dist/provider/index.mjs +5 -0
  104. package/dist/provider/router.cjs +185 -0
  105. package/dist/provider/router.d.cts +50 -0
  106. package/dist/provider/router.d.cts.map +1 -0
  107. package/dist/provider/router.d.mts +50 -0
  108. package/dist/provider/router.d.mts.map +1 -0
  109. package/dist/provider/router.mjs +185 -0
  110. package/dist/provider/router.mjs.map +1 -0
  111. package/dist/provider/types.d.cts +113 -0
  112. package/dist/provider/types.d.cts.map +1 -0
  113. package/dist/provider/types.d.mts +113 -0
  114. package/dist/provider/types.d.mts.map +1 -0
  115. package/dist/registry.cjs +358 -0
  116. package/dist/registry.d.cts +96 -0
  117. package/dist/registry.d.cts.map +1 -0
  118. package/dist/registry.d.mts +96 -0
  119. package/dist/registry.d.mts.map +1 -0
  120. package/dist/registry.mjs +360 -0
  121. package/dist/registry.mjs.map +1 -0
  122. package/dist/type.cjs +34 -0
  123. package/dist/type.d.cts +420 -0
  124. package/dist/type.d.cts.map +1 -0
  125. package/dist/type.d.mts +420 -0
  126. package/dist/type.d.mts.map +1 -0
  127. package/dist/type.mjs +33 -0
  128. package/dist/type.mjs.map +1 -0
  129. package/dist/utils/camelize.d.cts.map +1 -1
  130. package/dist/utils/camelize.d.mts.map +1 -1
  131. package/dist/utils/schema.cjs +129 -0
  132. package/dist/utils/schema.d.cts +65 -0
  133. package/dist/utils/schema.d.cts.map +1 -0
  134. package/dist/utils/schema.d.mts +65 -0
  135. package/dist/utils/schema.d.mts.map +1 -0
  136. package/dist/utils/schema.mjs +124 -0
  137. package/dist/utils/schema.mjs.map +1 -0
  138. package/dist/utils/type-utils.d.cts.map +1 -1
  139. package/dist/utils/type-utils.d.mts.map +1 -1
  140. package/dist/utils/uri-template.cjs +123 -0
  141. package/dist/utils/uri-template.d.cts +48 -0
  142. package/dist/utils/uri-template.d.cts.map +1 -0
  143. package/dist/utils/uri-template.d.mts +48 -0
  144. package/dist/utils/uri-template.d.mts.map +1 -0
  145. package/dist/utils/uri-template.mjs +120 -0
  146. package/dist/utils/uri-template.mjs.map +1 -0
  147. package/dist/utils/uri.cjs +49 -0
  148. package/dist/utils/uri.d.cts +34 -0
  149. package/dist/utils/uri.d.cts.map +1 -0
  150. package/dist/utils/uri.d.mts +34 -0
  151. package/dist/utils/uri.d.mts.map +1 -0
  152. package/dist/utils/uri.mjs +49 -0
  153. package/dist/utils/uri.mjs.map +1 -0
  154. package/dist/utils/zod.cjs +6 -8
  155. package/dist/utils/zod.d.cts +2 -2
  156. package/dist/utils/zod.d.cts.map +1 -1
  157. package/dist/utils/zod.d.mts +2 -2
  158. package/dist/utils/zod.d.mts.map +1 -1
  159. package/dist/utils/zod.mjs +6 -8
  160. package/dist/utils/zod.mjs.map +1 -1
  161. package/package.json +27 -4
  162. package/dist/index.d.cts.map +0 -1
  163. package/dist/index.d.mts.map +0 -1
  164. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,175 @@
1
+ import { AFSExplainResult } from "../meta/type.mjs";
2
+ import { AFSAccessMode, AFSDeleteOptions, AFSDeleteResult, AFSEntry, AFSEntryMetadata, AFSExecOptions, AFSExecResult, AFSExplainOptions, AFSListOptions, AFSListResult, AFSModule, AFSReadOptions, AFSReadResult, AFSRenameOptions, AFSRenameResult, AFSSearchOptions, AFSSearchResult, AFSStatOptions, AFSStatResult, AFSWriteEntryPayload, AFSWriteOptions, AFSWriteResult } from "../type.mjs";
3
+ import { OperationsDeclaration } from "../capabilities/types.mjs";
4
+ import { ProviderRouter } from "./router.mjs";
5
+
6
+ //#region src/provider/base.d.ts
7
+ /**
8
+ * Abstract base class for AFS providers using virtual routing
9
+ *
10
+ * All providers extend this class and use decorators to define routes:
11
+ * - @List(pattern) - Handle list operations
12
+ * - @Read(pattern) - Handle read operations
13
+ * - @Write(pattern) - Handle write operations
14
+ * - @Delete(pattern) - Handle delete operations
15
+ * - @Exec(pattern) - Handle exec operations
16
+ * - @Search(pattern) - Handle search operations
17
+ * - @Meta(pattern) - Handle .meta path reads
18
+ * - @Actions(pattern) - Handle .actions path listing
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * class MyProvider extends AFSBaseProvider {
23
+ * readonly name = "my-provider";
24
+ *
25
+ * @List("/")
26
+ * async listRoot(ctx: RouteContext) {
27
+ * return { data: [...] };
28
+ * }
29
+ *
30
+ * @Read("/:id")
31
+ * async getItem(ctx: RouteContext<{ id: string }>) {
32
+ * return { id: ctx.params.id, path: ctx.path, content: ... };
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ declare abstract class AFSBaseProvider implements AFSModule {
38
+ /** Provider name, must be unique when mounted */
39
+ abstract readonly name: string;
40
+ /** Optional description */
41
+ readonly description?: string;
42
+ /** Access mode: "readonly" or "readwrite" */
43
+ readonly accessMode: AFSAccessMode;
44
+ /** Internal router for path matching */
45
+ private router;
46
+ constructor();
47
+ /**
48
+ * Collect routes from decorators and register them
49
+ */
50
+ private collectDecoratorRoutes;
51
+ /**
52
+ * Remove methods for operations that have no routes registered.
53
+ * This allows core to check if a provider supports a capability via typeof check.
54
+ *
55
+ * We set the method to undefined on the instance to shadow the prototype method,
56
+ * since `delete` only removes instance properties, not inherited ones.
57
+ */
58
+ private removeUnimplementedMethods;
59
+ /**
60
+ * Build an OperationsDeclaration based on which methods are implemented.
61
+ * Uses the fact that removeUnimplementedMethods() sets unsupported methods to undefined.
62
+ * Write/delete/exec/rename are also gated by accessMode.
63
+ */
64
+ protected getOperationsDeclaration(): OperationsDeclaration;
65
+ /**
66
+ * List entries at a path
67
+ */
68
+ list(path: string, options?: AFSListOptions): Promise<AFSListResult>;
69
+ /**
70
+ * Expand list results to specified depth using BFS traversal.
71
+ * Used when handler has handleDepth: false.
72
+ *
73
+ * The handler is expected to return single-level results (direct children only).
74
+ * This method recursively calls the handler to expand directories up to maxDepth.
75
+ * Pattern filtering is applied after BFS completes to ensure directories are
76
+ * traversed even if they don't match the pattern.
77
+ */
78
+ private listWithDepthExpansion;
79
+ /**
80
+ * Check if a path matches a glob pattern
81
+ */
82
+ private matchesPattern;
83
+ /**
84
+ * Read an entry at a path
85
+ */
86
+ read(path: string, options?: AFSReadOptions): Promise<AFSReadResult>;
87
+ /**
88
+ * Write an entry at a path
89
+ */
90
+ write(path: string, content: AFSWriteEntryPayload, options?: AFSWriteOptions): Promise<AFSWriteResult>;
91
+ /**
92
+ * Delete an entry at a path
93
+ */
94
+ delete(path: string, options?: AFSDeleteOptions): Promise<AFSDeleteResult>;
95
+ /**
96
+ * Search entries at a path
97
+ */
98
+ search(path: string, query: string, options?: AFSSearchOptions): Promise<AFSSearchResult>;
99
+ /**
100
+ * Execute an action at a path
101
+ */
102
+ exec(path: string, args: Record<string, unknown>, options?: AFSExecOptions): Promise<AFSExecResult>;
103
+ /**
104
+ * Get stat information for a path
105
+ *
106
+ * Note: Fallback to read() is handled at AFS core level, not here.
107
+ */
108
+ stat(path: string, options?: AFSStatOptions): Promise<AFSStatResult>;
109
+ /**
110
+ * Get human-readable explanation for a path
111
+ *
112
+ * Note: Fallback to stat() is handled at AFS core level, not here.
113
+ */
114
+ explain(path: string, options?: AFSExplainOptions): Promise<AFSExplainResult>;
115
+ /**
116
+ * Rename/move a path
117
+ */
118
+ rename(oldPath: string, newPath: string, options?: AFSRenameOptions): Promise<AFSRenameResult>;
119
+ /**
120
+ * Normalize a path to ensure consistent format
121
+ * - Always starts with /
122
+ * - No trailing slash (except for root)
123
+ */
124
+ protected normalizePath(path: string): string;
125
+ /**
126
+ * Join path segments
127
+ */
128
+ protected joinPath(...segments: string[]): string;
129
+ /**
130
+ * Build an AFSEntry helper
131
+ *
132
+ * @param path - The entry path (will be normalized)
133
+ * @param options - Entry options
134
+ * @param options.id - Entry ID. Defaults to normalized path if not provided
135
+ * @param options.content - Entry content
136
+ * @param options.meta - Entry metadata
137
+ * @param options.createdAt - Creation time. Undefined if not available from data source
138
+ * @param options.updatedAt - Last update time. Undefined if not available from data source
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * // Simple entry with just path
143
+ * this.buildEntry("/items/1")
144
+ *
145
+ * // Entry with content and metadata
146
+ * this.buildEntry("/items/1", {
147
+ * content: { name: "Item 1" },
148
+ * meta: { size: 100 },
149
+ * })
150
+ *
151
+ * // Entry with all fields
152
+ * this.buildEntry("/items/1", {
153
+ * id: "custom-id",
154
+ * content: data,
155
+ * meta: { size: stats.size },
156
+ * createdAt: stats.birthtime,
157
+ * updatedAt: stats.mtime,
158
+ * })
159
+ * ```
160
+ */
161
+ protected buildEntry(path: string, options?: {
162
+ id?: string;
163
+ content?: unknown;
164
+ meta?: Partial<AFSEntryMetadata>;
165
+ createdAt?: Date;
166
+ updatedAt?: Date;
167
+ }): AFSEntry;
168
+ /**
169
+ * Get the router (for testing/debugging)
170
+ */
171
+ protected getRouter(): ProviderRouter;
172
+ }
173
+ //#endregion
174
+ export { AFSBaseProvider };
175
+ //# sourceMappingURL=base.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.mts","names":[],"sources":["../../src/provider/base.ts"],"mappings":";;;;;;;;;AA0EA;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAAsB,eAAA,YAA2B,SAAA;EAmdT;EAAA,kBAjdpB,IAAA;EAidwC;EAAA,SA9cjD,WAAA;EAueE;EAAA,SApeF,UAAA,EAAY,aAAA;EAkkBF;EAAA,QA/jBX,MAAA;;EAikBQ;;;EAAA,QAtjBR,sBAAA;EAtBgD;;;;;;;EAAA,QAqDhD,0BAAA;;;;;;YA4BE,wBAAA,CAAA,GAA4B,qBAAA;EAmB3B;;;EAAL,IAAA,CAAK,IAAA,UAAc,OAAA,GAAU,cAAA,GAAiB,OAAA,CAAQ,aAAA;EAAA;;;;;;;;;EAAA,QAyD9C,sBAAA;EAsLZ;;;EAAA,QAlEM,cAAA;EAoEN;;;EA7DI,IAAA,CAAK,IAAA,UAAc,OAAA,GAAU,cAAA,GAAiB,OAAA,CAAQ,aAAA;EAuF/C;;;EA7BP,KAAA,CACJ,IAAA,UACA,OAAA,EAAS,oBAAA,EACT,OAAA,GAAU,eAAA,GACT,OAAA,CAAQ,cAAA;EAyBqD;;;EAA1D,MAAA,CAAO,IAAA,UAAc,OAAA,GAAU,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAyBZ;;;EAA9C,MAAA,CAAO,IAAA,UAAc,KAAA,UAAe,OAAA,GAAU,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAqBzE;;;EAAA,IAAA,CACJ,IAAA,UACA,IAAA,EAAM,MAAA,mBACN,OAAA,GAAU,cAAA,GACT,OAAA,CAAQ,aAAA;EADC;;;;;EA4BN,IAAA,CAAK,IAAA,UAAc,OAAA,GAAU,cAAA,GAAiB,OAAA,CAAQ,aAAA;EAAzB;;;;;EAuB7B,OAAA,CAAQ,IAAA,UAAc,OAAA,GAAU,iBAAA,GAAoB,OAAA,CAAQ,gBAAA;EAA5B;;;EAqBhC,MAAA,CACJ,OAAA,UACA,OAAA,UACA,OAAA,GAAU,gBAAA,GACT,OAAA,CAAQ,eAAA;EAJL;;;;;EAAA,UAiCI,aAAA,CAAc,IAAA;EA7Bb;;;EAAA,UAgDD,QAAA,CAAA,GAAY,QAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAA,UAyCZ,UAAA,CACR,IAAA,UACA,OAAA;IACE,EAAA;IACA,OAAA;IACA,IAAA,GAAO,OAAA,CAAQ,gBAAA;IACf,SAAA,GAAY,IAAA;IACZ,SAAA,GAAY,IAAA;EAAA,IAEb,QAAA;;;;YAeO,SAAA,CAAA,GAAa,cAAA;AAAA"}
@@ -0,0 +1,426 @@
1
+ import { AFSNotFoundError, AFSReadonlyError } from "../error.mjs";
2
+ import { getRoutes } from "./decorators.mjs";
3
+ import { ProviderRouter } from "./router.mjs";
4
+ import { minimatch } from "minimatch";
5
+
6
+ //#region src/provider/base.ts
7
+ /**
8
+ * Abstract base class for AFS providers using virtual routing
9
+ *
10
+ * All providers extend this class and use decorators to define routes:
11
+ * - @List(pattern) - Handle list operations
12
+ * - @Read(pattern) - Handle read operations
13
+ * - @Write(pattern) - Handle write operations
14
+ * - @Delete(pattern) - Handle delete operations
15
+ * - @Exec(pattern) - Handle exec operations
16
+ * - @Search(pattern) - Handle search operations
17
+ * - @Meta(pattern) - Handle .meta path reads
18
+ * - @Actions(pattern) - Handle .actions path listing
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * class MyProvider extends AFSBaseProvider {
23
+ * readonly name = "my-provider";
24
+ *
25
+ * @List("/")
26
+ * async listRoot(ctx: RouteContext) {
27
+ * return { data: [...] };
28
+ * }
29
+ *
30
+ * @Read("/:id")
31
+ * async getItem(ctx: RouteContext<{ id: string }>) {
32
+ * return { id: ctx.params.id, path: ctx.path, content: ... };
33
+ * }
34
+ * }
35
+ * ```
36
+ */
37
+ var AFSBaseProvider = class {
38
+ /** Optional description */
39
+ description;
40
+ /** Access mode: "readonly" or "readwrite" */
41
+ accessMode = "readonly";
42
+ /** Internal router for path matching */
43
+ router;
44
+ constructor() {
45
+ this.router = new ProviderRouter();
46
+ this.collectDecoratorRoutes();
47
+ this.removeUnimplementedMethods();
48
+ }
49
+ /**
50
+ * Collect routes from decorators and register them
51
+ */
52
+ collectDecoratorRoutes() {
53
+ const routes = getRoutes(this.constructor);
54
+ for (const route of routes) {
55
+ const handler = this[route.methodName];
56
+ if (typeof handler !== "function") {
57
+ console.warn(`[AFSBaseProvider] Method ${route.methodName} not found on ${this.constructor.name}`);
58
+ continue;
59
+ }
60
+ const boundHandler = handler.bind(this);
61
+ this.router.registerRoute(route.pattern, route.operation, boundHandler, route.description, route.listOptions);
62
+ }
63
+ }
64
+ /**
65
+ * Remove methods for operations that have no routes registered.
66
+ * This allows core to check if a provider supports a capability via typeof check.
67
+ *
68
+ * We set the method to undefined on the instance to shadow the prototype method,
69
+ * since `delete` only removes instance properties, not inherited ones.
70
+ */
71
+ removeUnimplementedMethods() {
72
+ for (const [operation, methodName] of Object.entries({
73
+ list: "list",
74
+ read: "read",
75
+ write: "write",
76
+ delete: "delete",
77
+ exec: "exec",
78
+ search: "search",
79
+ stat: "stat",
80
+ explain: "explain",
81
+ rename: "rename"
82
+ })) if (this.router.getRoutesForOperation(operation).length === 0) this[methodName] = void 0;
83
+ }
84
+ /**
85
+ * Build an OperationsDeclaration based on which methods are implemented.
86
+ * Uses the fact that removeUnimplementedMethods() sets unsupported methods to undefined.
87
+ * Write/delete/exec/rename are also gated by accessMode.
88
+ */
89
+ getOperationsDeclaration() {
90
+ const isReadwrite = this.accessMode === "readwrite";
91
+ return {
92
+ read: typeof this.read === "function",
93
+ list: typeof this.list === "function",
94
+ write: typeof this.write === "function" && isReadwrite,
95
+ delete: typeof this.delete === "function" && isReadwrite,
96
+ search: typeof this.search === "function",
97
+ exec: typeof this.exec === "function",
98
+ stat: typeof this.stat === "function",
99
+ explain: typeof this.explain === "function"
100
+ };
101
+ }
102
+ /**
103
+ * List entries at a path
104
+ */
105
+ async list(path, options) {
106
+ const maxDepth = options?.maxDepth ?? 1;
107
+ if (maxDepth === 0) return { data: [] };
108
+ const normalizedPath = this.normalizePath(path);
109
+ const match = this.router.match(normalizedPath, "list");
110
+ if (!match) throw new AFSNotFoundError(normalizedPath);
111
+ const limit = options?.limit;
112
+ if ((match.route.listOptions?.handleDepth ?? false) || maxDepth <= 1) {
113
+ const ctx = {
114
+ path: normalizedPath,
115
+ params: match.params,
116
+ options
117
+ };
118
+ const handler = match.route.handler;
119
+ const handlerResult = await handler(ctx);
120
+ const result = {
121
+ data: handlerResult.data,
122
+ total: handlerResult.total
123
+ };
124
+ if (limit !== void 0 && result.data.length > limit) return {
125
+ data: result.data.slice(0, limit),
126
+ total: result.total ?? result.data.length
127
+ };
128
+ return result;
129
+ }
130
+ return this.listWithDepthExpansion(normalizedPath, options, maxDepth);
131
+ }
132
+ /**
133
+ * Expand list results to specified depth using BFS traversal.
134
+ * Used when handler has handleDepth: false.
135
+ *
136
+ * The handler is expected to return single-level results (direct children only).
137
+ * This method recursively calls the handler to expand directories up to maxDepth.
138
+ * Pattern filtering is applied after BFS completes to ensure directories are
139
+ * traversed even if they don't match the pattern.
140
+ */
141
+ async listWithDepthExpansion(basePath, options, maxDepth) {
142
+ const limit = options?.limit ?? 1e3;
143
+ const pattern = options?.pattern;
144
+ const allEntries = [];
145
+ const visited = /* @__PURE__ */ new Set();
146
+ const queue = [{
147
+ path: basePath,
148
+ depth: 0
149
+ }];
150
+ const unfilteredEntries = [];
151
+ while (queue.length > 0) {
152
+ const { path, depth } = queue.shift();
153
+ if (visited.has(path)) continue;
154
+ visited.add(path);
155
+ const singleLevelOptions = {
156
+ ...options,
157
+ maxDepth: 1,
158
+ pattern: void 0
159
+ };
160
+ const match = this.router.match(path, "list");
161
+ if (!match) continue;
162
+ const ctx = {
163
+ path,
164
+ params: match.params,
165
+ options: singleLevelOptions
166
+ };
167
+ const handler = match.route.handler;
168
+ let result;
169
+ try {
170
+ result = await handler(ctx);
171
+ } catch (error) {
172
+ const code = error.code;
173
+ if (code === "ENOENT" || code === "ENOTDIR" || error instanceof Error && error.message.includes("not found")) continue;
174
+ throw error;
175
+ }
176
+ const noExpandSet = new Set(result.noExpand ?? []);
177
+ for (const entry of result.data) {
178
+ if (entry.path === path && depth > 0) continue;
179
+ unfilteredEntries.push(entry);
180
+ const childrenCount = entry.meta?.childrenCount;
181
+ const isDirectory = childrenCount !== void 0 && childrenCount !== 0;
182
+ const shouldExpand = !noExpandSet.has(entry.path);
183
+ if (depth < maxDepth - 1 && isDirectory && shouldExpand && entry.path !== path) queue.push({
184
+ path: entry.path,
185
+ depth: depth + 1
186
+ });
187
+ }
188
+ }
189
+ if (pattern) for (const entry of unfilteredEntries) {
190
+ if (allEntries.length >= limit) break;
191
+ if (this.matchesPattern(entry.path, pattern)) allEntries.push(entry);
192
+ }
193
+ else for (const entry of unfilteredEntries) {
194
+ if (allEntries.length >= limit) break;
195
+ allEntries.push(entry);
196
+ }
197
+ return {
198
+ data: allEntries,
199
+ total: allEntries.length >= limit ? void 0 : allEntries.length
200
+ };
201
+ }
202
+ /**
203
+ * Check if a path matches a glob pattern
204
+ */
205
+ matchesPattern(path, pattern) {
206
+ return minimatch(path, pattern, { matchBase: true });
207
+ }
208
+ /**
209
+ * Read an entry at a path
210
+ */
211
+ async read(path, options) {
212
+ const normalizedPath = this.normalizePath(path);
213
+ const actionsMatch = normalizedPath.match(/^(.*)\/\.actions\/([^/]+)$/);
214
+ if (actionsMatch) {
215
+ const actionsListPath = `${actionsMatch[1] || ""}/.actions`;
216
+ const actionName = actionsMatch[2];
217
+ const listMatch = this.router.match(actionsListPath, "list");
218
+ if (listMatch) {
219
+ const ctx = {
220
+ path: actionsListPath,
221
+ params: listMatch.params,
222
+ options: {}
223
+ };
224
+ const handler = listMatch.route.handler;
225
+ const actionEntry = (await handler(ctx)).data.find((entry) => entry.summary === actionName || entry.id === actionName || entry.id.endsWith(`:${actionName}`) || entry.meta?.name === actionName);
226
+ if (actionEntry) return { data: actionEntry };
227
+ }
228
+ }
229
+ const match = this.router.match(normalizedPath, "read");
230
+ if (match) {
231
+ const ctx = {
232
+ path: normalizedPath,
233
+ params: match.params,
234
+ options
235
+ };
236
+ const handler = match.route.handler;
237
+ return { data: await handler(ctx) };
238
+ }
239
+ throw new AFSNotFoundError(normalizedPath);
240
+ }
241
+ /**
242
+ * Write an entry at a path
243
+ */
244
+ async write(path, content, options) {
245
+ if (this.accessMode === "readonly") throw new AFSReadonlyError("Cannot write on a readonly provider");
246
+ const normalizedPath = this.normalizePath(path);
247
+ const match = this.router.match(normalizedPath, "write");
248
+ if (!match) throw new Error(`No write handler for path: ${path}`);
249
+ const ctx = {
250
+ path: normalizedPath,
251
+ params: match.params,
252
+ options
253
+ };
254
+ const handler = match.route.handler;
255
+ return handler(ctx, content);
256
+ }
257
+ /**
258
+ * Delete an entry at a path
259
+ */
260
+ async delete(path, options) {
261
+ if (this.accessMode === "readonly") throw new AFSReadonlyError("Cannot delete on a readonly provider");
262
+ const normalizedPath = this.normalizePath(path);
263
+ const match = this.router.match(normalizedPath, "delete");
264
+ if (!match) throw new Error(`No delete handler for path: ${path}`);
265
+ const ctx = {
266
+ path: normalizedPath,
267
+ params: match.params,
268
+ options
269
+ };
270
+ const handler = match.route.handler;
271
+ return handler(ctx);
272
+ }
273
+ /**
274
+ * Search entries at a path
275
+ */
276
+ async search(path, query, options) {
277
+ const normalizedPath = this.normalizePath(path);
278
+ const match = this.router.match(normalizedPath, "search");
279
+ if (!match) return { data: [] };
280
+ const ctx = {
281
+ path: normalizedPath,
282
+ params: match.params,
283
+ options
284
+ };
285
+ const handler = match.route.handler;
286
+ return handler(ctx, query, options);
287
+ }
288
+ /**
289
+ * Execute an action at a path
290
+ */
291
+ async exec(path, args, options) {
292
+ if (this.accessMode === "readonly") throw new AFSReadonlyError("Cannot exec on a readonly provider");
293
+ const normalizedPath = this.normalizePath(path);
294
+ const match = this.router.match(normalizedPath, "exec");
295
+ if (!match) throw new Error(`No exec handler for path: ${path}`);
296
+ const ctx = {
297
+ path: normalizedPath,
298
+ params: match.params,
299
+ options
300
+ };
301
+ const handler = match.route.handler;
302
+ return handler(ctx, args);
303
+ }
304
+ /**
305
+ * Get stat information for a path
306
+ *
307
+ * Note: Fallback to read() is handled at AFS core level, not here.
308
+ */
309
+ async stat(path, options) {
310
+ const normalizedPath = this.normalizePath(path);
311
+ const match = this.router.match(normalizedPath, "stat");
312
+ if (match) {
313
+ const ctx = {
314
+ path: normalizedPath,
315
+ params: match.params,
316
+ options
317
+ };
318
+ const handler = match.route.handler;
319
+ return handler(ctx);
320
+ }
321
+ throw new AFSNotFoundError(normalizedPath);
322
+ }
323
+ /**
324
+ * Get human-readable explanation for a path
325
+ *
326
+ * Note: Fallback to stat() is handled at AFS core level, not here.
327
+ */
328
+ async explain(path, options) {
329
+ const normalizedPath = this.normalizePath(path);
330
+ const match = this.router.match(normalizedPath, "explain");
331
+ if (!match) throw new Error(`No explain handler for path: ${path}`);
332
+ const ctx = {
333
+ path: normalizedPath,
334
+ params: match.params,
335
+ options
336
+ };
337
+ const handler = match.route.handler;
338
+ return handler(ctx);
339
+ }
340
+ /**
341
+ * Rename/move a path
342
+ */
343
+ async rename(oldPath, newPath, options) {
344
+ if (this.accessMode === "readonly") throw new AFSReadonlyError("Cannot rename on a readonly provider");
345
+ const normalizedPath = this.normalizePath(oldPath);
346
+ const match = this.router.match(normalizedPath, "rename");
347
+ if (!match) throw new AFSNotFoundError(normalizedPath);
348
+ const ctx = {
349
+ path: normalizedPath,
350
+ params: match.params,
351
+ options
352
+ };
353
+ const handler = match.route.handler;
354
+ return handler(ctx, newPath);
355
+ }
356
+ /**
357
+ * Normalize a path to ensure consistent format
358
+ * - Always starts with /
359
+ * - No trailing slash (except for root)
360
+ */
361
+ normalizePath(path) {
362
+ if (!path || path === "/") return "/";
363
+ let normalized = path.startsWith("/") ? path : `/${path}`;
364
+ if (normalized.length > 1 && normalized.endsWith("/")) normalized = normalized.slice(0, -1);
365
+ return normalized;
366
+ }
367
+ /**
368
+ * Join path segments
369
+ */
370
+ joinPath(...segments) {
371
+ return `/${segments.map((s) => s.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/")}`;
372
+ }
373
+ /**
374
+ * Build an AFSEntry helper
375
+ *
376
+ * @param path - The entry path (will be normalized)
377
+ * @param options - Entry options
378
+ * @param options.id - Entry ID. Defaults to normalized path if not provided
379
+ * @param options.content - Entry content
380
+ * @param options.meta - Entry metadata
381
+ * @param options.createdAt - Creation time. Undefined if not available from data source
382
+ * @param options.updatedAt - Last update time. Undefined if not available from data source
383
+ *
384
+ * @example
385
+ * ```typescript
386
+ * // Simple entry with just path
387
+ * this.buildEntry("/items/1")
388
+ *
389
+ * // Entry with content and metadata
390
+ * this.buildEntry("/items/1", {
391
+ * content: { name: "Item 1" },
392
+ * meta: { size: 100 },
393
+ * })
394
+ *
395
+ * // Entry with all fields
396
+ * this.buildEntry("/items/1", {
397
+ * id: "custom-id",
398
+ * content: data,
399
+ * meta: { size: stats.size },
400
+ * createdAt: stats.birthtime,
401
+ * updatedAt: stats.mtime,
402
+ * })
403
+ * ```
404
+ */
405
+ buildEntry(path, options) {
406
+ const normalizedPath = this.normalizePath(path);
407
+ return {
408
+ id: options?.id ?? normalizedPath,
409
+ path: normalizedPath,
410
+ content: options?.content,
411
+ meta: options?.meta,
412
+ createdAt: options?.createdAt,
413
+ updatedAt: options?.updatedAt
414
+ };
415
+ }
416
+ /**
417
+ * Get the router (for testing/debugging)
418
+ */
419
+ getRouter() {
420
+ return this.router;
421
+ }
422
+ };
423
+
424
+ //#endregion
425
+ export { AFSBaseProvider };
426
+ //# sourceMappingURL=base.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.mjs","names":[],"sources":["../../src/provider/base.ts"],"sourcesContent":["import { minimatch } from \"minimatch\";\nimport type { OperationsDeclaration } from \"../capabilities/types.js\";\nimport { AFSNotFoundError, AFSReadonlyError } from \"../error.js\";\nimport type {\n AFSAccessMode,\n AFSDeleteOptions,\n AFSDeleteResult,\n AFSEntry,\n AFSEntryMetadata,\n AFSExecOptions,\n AFSExecResult,\n AFSExplainOptions,\n AFSExplainResult,\n AFSListOptions,\n AFSListResult,\n AFSModule,\n AFSReadOptions,\n AFSReadResult,\n AFSRenameOptions,\n AFSRenameResult,\n AFSSearchOptions,\n AFSSearchResult,\n AFSStatOptions,\n AFSStatResult,\n AFSWriteEntryPayload,\n AFSWriteOptions,\n AFSWriteResult,\n} from \"../type.js\";\nimport { getRoutes } from \"./decorators.js\";\nimport { ProviderRouter } from \"./router.js\";\nimport type {\n DeleteRouteHandler,\n ExecRouteHandler,\n ExplainRouteHandler,\n ListRouteHandler,\n ReadRouteHandler,\n RenameRouteHandler,\n RouteContext,\n RouteOperation,\n SearchRouteHandler,\n StatRouteHandler,\n WriteRouteHandler,\n} from \"./types.js\";\n\n/**\n * Abstract base class for AFS providers using virtual routing\n *\n * All providers extend this class and use decorators to define routes:\n * - @List(pattern) - Handle list operations\n * - @Read(pattern) - Handle read operations\n * - @Write(pattern) - Handle write operations\n * - @Delete(pattern) - Handle delete operations\n * - @Exec(pattern) - Handle exec operations\n * - @Search(pattern) - Handle search operations\n * - @Meta(pattern) - Handle .meta path reads\n * - @Actions(pattern) - Handle .actions path listing\n *\n * @example\n * ```typescript\n * class MyProvider extends AFSBaseProvider {\n * readonly name = \"my-provider\";\n *\n * @List(\"/\")\n * async listRoot(ctx: RouteContext) {\n * return { data: [...] };\n * }\n *\n * @Read(\"/:id\")\n * async getItem(ctx: RouteContext<{ id: string }>) {\n * return { id: ctx.params.id, path: ctx.path, content: ... };\n * }\n * }\n * ```\n */\nexport abstract class AFSBaseProvider implements AFSModule {\n /** Provider name, must be unique when mounted */\n abstract readonly name: string;\n\n /** Optional description */\n readonly description?: string;\n\n /** Access mode: \"readonly\" or \"readwrite\" */\n readonly accessMode: AFSAccessMode = \"readonly\";\n\n /** Internal router for path matching */\n private router: ProviderRouter;\n\n constructor() {\n this.router = new ProviderRouter();\n this.collectDecoratorRoutes();\n this.removeUnimplementedMethods();\n }\n\n /**\n * Collect routes from decorators and register them\n */\n private collectDecoratorRoutes(): void {\n const routes = getRoutes(this.constructor);\n\n for (const route of routes) {\n const handler = (this as any)[route.methodName];\n if (typeof handler !== \"function\") {\n console.warn(\n `[AFSBaseProvider] Method ${route.methodName} not found on ${this.constructor.name}`,\n );\n continue;\n }\n\n // Bind the handler to this instance\n const boundHandler = handler.bind(this);\n this.router.registerRoute(\n route.pattern,\n route.operation,\n boundHandler,\n route.description,\n route.listOptions,\n );\n }\n }\n\n /**\n * Remove methods for operations that have no routes registered.\n * This allows core to check if a provider supports a capability via typeof check.\n *\n * We set the method to undefined on the instance to shadow the prototype method,\n * since `delete` only removes instance properties, not inherited ones.\n */\n private removeUnimplementedMethods(): void {\n const operationToMethod: Record<string, keyof AFSBaseProvider> = {\n list: \"list\",\n read: \"read\",\n write: \"write\",\n delete: \"delete\",\n exec: \"exec\",\n search: \"search\",\n stat: \"stat\",\n explain: \"explain\",\n rename: \"rename\",\n };\n\n for (const [operation, methodName] of Object.entries(operationToMethod)) {\n const routes = this.router.getRoutesForOperation(operation as RouteOperation);\n\n // Keep the method only if there are routes registered for this operation\n if (routes.length === 0) {\n (this as any)[methodName] = undefined;\n }\n }\n }\n\n /**\n * Build an OperationsDeclaration based on which methods are implemented.\n * Uses the fact that removeUnimplementedMethods() sets unsupported methods to undefined.\n * Write/delete/exec/rename are also gated by accessMode.\n */\n protected getOperationsDeclaration(): OperationsDeclaration {\n const isReadwrite = this.accessMode === \"readwrite\";\n return {\n read: typeof this.read === \"function\",\n list: typeof this.list === \"function\",\n write: typeof this.write === \"function\" && isReadwrite,\n delete: typeof this.delete === \"function\" && isReadwrite,\n search: typeof this.search === \"function\",\n exec: typeof this.exec === \"function\",\n stat: typeof this.stat === \"function\",\n explain: typeof this.explain === \"function\",\n };\n }\n\n // ========== AFSModule Interface Implementation ==========\n\n /**\n * List entries at a path\n */\n async list(path: string, options?: AFSListOptions): Promise<AFSListResult> {\n const maxDepth = options?.maxDepth ?? 1;\n\n // maxDepth=0: return empty array (no children levels to expand)\n if (maxDepth === 0) {\n return { data: [] };\n }\n\n const normalizedPath = this.normalizePath(path);\n const match = this.router.match(normalizedPath, \"list\");\n\n if (!match) {\n throw new AFSNotFoundError(normalizedPath);\n }\n\n const limit = options?.limit;\n const handleDepth = match.route.listOptions?.handleDepth ?? false;\n\n if (handleDepth || maxDepth <= 1) {\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as ListRouteHandler;\n const handlerResult = await handler(ctx);\n\n const result: AFSListResult = {\n data: handlerResult.data,\n total: handlerResult.total,\n };\n\n // Apply limit if specified\n if (limit !== undefined && result.data.length > limit) {\n return {\n data: result.data.slice(0, limit),\n total: result.total ?? result.data.length,\n };\n }\n\n return result;\n }\n\n // Base provider handles depth expansion via BFS\n return this.listWithDepthExpansion(normalizedPath, options, maxDepth);\n }\n\n /**\n * Expand list results to specified depth using BFS traversal.\n * Used when handler has handleDepth: false.\n *\n * The handler is expected to return single-level results (direct children only).\n * This method recursively calls the handler to expand directories up to maxDepth.\n * Pattern filtering is applied after BFS completes to ensure directories are\n * traversed even if they don't match the pattern.\n */\n private async listWithDepthExpansion(\n basePath: string,\n options: AFSListOptions | undefined,\n maxDepth: number,\n ): Promise<AFSListResult> {\n const limit = options?.limit ?? 1000;\n const pattern = options?.pattern;\n\n const allEntries: AFSEntry[] = [];\n const visited = new Set<string>();\n\n // BFS queue: [path, currentDepth]\n // depth 0 = basePath itself, depth 1 = children, etc.\n const queue: Array<{ path: string; depth: number }> = [{ path: basePath, depth: 0 }];\n\n // Track entries before pattern filtering (need more than limit for filtering)\n const unfilteredEntries: AFSEntry[] = [];\n\n while (queue.length > 0) {\n const { path, depth } = queue.shift()!;\n\n // Prevent infinite loops\n if (visited.has(path)) continue;\n visited.add(path);\n\n // Call handler with depth=1 and WITHOUT pattern to get all entries for expansion\n const singleLevelOptions: AFSListOptions = {\n ...options,\n maxDepth: 1, // Always request single level from handler\n pattern: undefined, // Don't filter during BFS - filter at end\n };\n\n const match = this.router.match(path, \"list\");\n if (!match) continue;\n\n const ctx: RouteContext = {\n path,\n params: match.params,\n options: singleLevelOptions,\n };\n\n const handler = match.route.handler as ListRouteHandler;\n\n // Wrap in try-catch to handle files deleted during BFS traversal\n let result: Awaited<ReturnType<ListRouteHandler>>;\n try {\n result = await handler(ctx);\n } catch (error) {\n // Ignore not found errors - file may have been deleted during expansion\n const code = (error as NodeJS.ErrnoException).code;\n const isNotFound =\n code === \"ENOENT\" ||\n code === \"ENOTDIR\" ||\n (error instanceof Error && error.message.includes(\"not found\"));\n if (isNotFound) {\n continue;\n }\n throw error;\n }\n\n // Collect paths that should not be expanded (internal hint from handler)\n const noExpandSet = new Set(result.noExpand ?? []);\n\n // Note: maxChildren is passed to handler via options, so handler applies it to children\n // We don't apply it again here to avoid double-limiting\n\n // Add entries to results and queue directories for expansion\n for (const entry of result.data) {\n // Skip the current path entry for depth > 0 (it was already added as a child by parent)\n // At depth 0, include the root entry\n if (entry.path === path && depth > 0) {\n continue;\n }\n\n unfilteredEntries.push(entry);\n\n // Queue entry for expansion if:\n // 1. Within depth limit (depth < maxDepth means we can go one more level)\n // 2. Entry is a directory (childrenCount > 0 or -1, NOT undefined which means leaf)\n // 3. Entry is not the current path (which is already being processed)\n // 4. Entry is not in noExpand set (used for ignored directories)\n const childrenCount = entry.meta?.childrenCount;\n const isDirectory = childrenCount !== undefined && childrenCount !== 0;\n const shouldExpand = !noExpandSet.has(entry.path);\n\n if (depth < maxDepth - 1 && isDirectory && shouldExpand && entry.path !== path) {\n queue.push({ path: entry.path, depth: depth + 1 });\n }\n }\n }\n\n // Apply pattern filtering after BFS completes\n if (pattern) {\n for (const entry of unfilteredEntries) {\n if (allEntries.length >= limit) break;\n if (this.matchesPattern(entry.path, pattern)) {\n allEntries.push(entry);\n }\n }\n } else {\n // No pattern, apply limit directly\n for (const entry of unfilteredEntries) {\n if (allEntries.length >= limit) break;\n allEntries.push(entry);\n }\n }\n\n return {\n data: allEntries,\n total: allEntries.length >= limit ? undefined : allEntries.length,\n };\n }\n\n /**\n * Check if a path matches a glob pattern\n */\n private matchesPattern(path: string, pattern: string): boolean {\n return minimatch(path, pattern, { matchBase: true });\n }\n\n /**\n * Read an entry at a path\n */\n async read(path: string, options?: AFSReadOptions): Promise<AFSReadResult> {\n const normalizedPath = this.normalizePath(path);\n\n // Handle reading individual action definitions: */.actions/:actionName\n // This allows providers to expose action details via read() without\n // needing to define individual read handlers for each action\n const actionsMatch = normalizedPath.match(/^(.*)\\/\\.actions\\/([^/]+)$/);\n if (actionsMatch) {\n const actionsListPath = `${actionsMatch[1] || \"\"}/.actions`;\n const actionName = actionsMatch[2];\n\n // Try to find a list handler for the .actions path\n const listMatch = this.router.match(actionsListPath, \"list\");\n if (listMatch) {\n const ctx: RouteContext = {\n path: actionsListPath,\n params: listMatch.params,\n options: {},\n };\n const handler = listMatch.route.handler as ListRouteHandler;\n const result = await handler(ctx);\n\n // Find the action by name (check summary, id, or meta.name)\n const actionEntry = result.data.find(\n (entry) =>\n entry.summary === actionName ||\n entry.id === actionName ||\n entry.id.endsWith(`:${actionName}`) ||\n entry.meta?.name === actionName,\n );\n\n if (actionEntry) {\n return { data: actionEntry };\n }\n }\n }\n\n // Standard handler matching\n const match = this.router.match(normalizedPath, \"read\");\n if (match) {\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as ReadRouteHandler;\n const entry = await handler(ctx);\n\n return { data: entry };\n }\n\n throw new AFSNotFoundError(normalizedPath);\n }\n\n /**\n * Write an entry at a path\n */\n async write(\n path: string,\n content: AFSWriteEntryPayload,\n options?: AFSWriteOptions,\n ): Promise<AFSWriteResult> {\n if (this.accessMode === \"readonly\") {\n throw new AFSReadonlyError(\"Cannot write on a readonly provider\");\n }\n\n const normalizedPath = this.normalizePath(path);\n const match = this.router.match(normalizedPath, \"write\");\n\n if (!match) {\n throw new Error(`No write handler for path: ${path}`);\n }\n\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as WriteRouteHandler;\n return handler(ctx, content);\n }\n\n /**\n * Delete an entry at a path\n */\n async delete(path: string, options?: AFSDeleteOptions): Promise<AFSDeleteResult> {\n if (this.accessMode === \"readonly\") {\n throw new AFSReadonlyError(\"Cannot delete on a readonly provider\");\n }\n\n const normalizedPath = this.normalizePath(path);\n const match = this.router.match(normalizedPath, \"delete\");\n\n if (!match) {\n throw new Error(`No delete handler for path: ${path}`);\n }\n\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as DeleteRouteHandler;\n return handler(ctx);\n }\n\n /**\n * Search entries at a path\n */\n async search(path: string, query: string, options?: AFSSearchOptions): Promise<AFSSearchResult> {\n const normalizedPath = this.normalizePath(path);\n const match = this.router.match(normalizedPath, \"search\");\n\n if (!match) {\n return { data: [] };\n }\n\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as SearchRouteHandler;\n return handler(ctx, query, options);\n }\n\n /**\n * Execute an action at a path\n */\n async exec(\n path: string,\n args: Record<string, unknown>,\n options?: AFSExecOptions,\n ): Promise<AFSExecResult> {\n if (this.accessMode === \"readonly\") {\n throw new AFSReadonlyError(\"Cannot exec on a readonly provider\");\n }\n\n const normalizedPath = this.normalizePath(path);\n const match = this.router.match(normalizedPath, \"exec\");\n\n if (!match) {\n throw new Error(`No exec handler for path: ${path}`);\n }\n\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as ExecRouteHandler;\n return handler(ctx, args);\n }\n\n /**\n * Get stat information for a path\n *\n * Note: Fallback to read() is handled at AFS core level, not here.\n */\n async stat(path: string, options?: AFSStatOptions): Promise<AFSStatResult> {\n const normalizedPath = this.normalizePath(path);\n const match = this.router.match(normalizedPath, \"stat\");\n\n if (match) {\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as StatRouteHandler;\n return handler(ctx);\n }\n\n throw new AFSNotFoundError(normalizedPath);\n }\n\n /**\n * Get human-readable explanation for a path\n *\n * Note: Fallback to stat() is handled at AFS core level, not here.\n */\n async explain(path: string, options?: AFSExplainOptions): Promise<AFSExplainResult> {\n const normalizedPath = this.normalizePath(path);\n const match = this.router.match(normalizedPath, \"explain\");\n\n if (!match) {\n throw new Error(`No explain handler for path: ${path}`);\n }\n\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as ExplainRouteHandler;\n return handler(ctx);\n }\n\n /**\n * Rename/move a path\n */\n async rename(\n oldPath: string,\n newPath: string,\n options?: AFSRenameOptions,\n ): Promise<AFSRenameResult> {\n if (this.accessMode === \"readonly\") {\n throw new AFSReadonlyError(\"Cannot rename on a readonly provider\");\n }\n\n const normalizedPath = this.normalizePath(oldPath);\n const match = this.router.match(normalizedPath, \"rename\");\n\n if (!match) {\n throw new AFSNotFoundError(normalizedPath);\n }\n\n const ctx: RouteContext = {\n path: normalizedPath,\n params: match.params,\n options,\n };\n\n const handler = match.route.handler as RenameRouteHandler;\n return handler(ctx, newPath);\n }\n\n // ========== Utility Methods ==========\n\n /**\n * Normalize a path to ensure consistent format\n * - Always starts with /\n * - No trailing slash (except for root)\n */\n protected normalizePath(path: string): string {\n if (!path || path === \"/\") {\n return \"/\";\n }\n\n // Ensure leading slash\n let normalized = path.startsWith(\"/\") ? path : `/${path}`;\n\n // Remove trailing slash\n if (normalized.length > 1 && normalized.endsWith(\"/\")) {\n normalized = normalized.slice(0, -1);\n }\n\n return normalized;\n }\n\n /**\n * Join path segments\n */\n protected joinPath(...segments: string[]): string {\n const joined = segments\n .map((s) => s.replace(/^\\/+|\\/+$/g, \"\"))\n .filter(Boolean)\n .join(\"/\");\n\n return `/${joined}`;\n }\n\n /**\n * Build an AFSEntry helper\n *\n * @param path - The entry path (will be normalized)\n * @param options - Entry options\n * @param options.id - Entry ID. Defaults to normalized path if not provided\n * @param options.content - Entry content\n * @param options.meta - Entry metadata\n * @param options.createdAt - Creation time. Undefined if not available from data source\n * @param options.updatedAt - Last update time. Undefined if not available from data source\n *\n * @example\n * ```typescript\n * // Simple entry with just path\n * this.buildEntry(\"/items/1\")\n *\n * // Entry with content and metadata\n * this.buildEntry(\"/items/1\", {\n * content: { name: \"Item 1\" },\n * meta: { size: 100 },\n * })\n *\n * // Entry with all fields\n * this.buildEntry(\"/items/1\", {\n * id: \"custom-id\",\n * content: data,\n * meta: { size: stats.size },\n * createdAt: stats.birthtime,\n * updatedAt: stats.mtime,\n * })\n * ```\n */\n protected buildEntry(\n path: string,\n options?: {\n id?: string;\n content?: unknown;\n meta?: Partial<AFSEntryMetadata>;\n createdAt?: Date;\n updatedAt?: Date;\n },\n ): AFSEntry {\n const normalizedPath = this.normalizePath(path);\n return {\n id: options?.id ?? normalizedPath,\n path: normalizedPath,\n content: options?.content,\n meta: options?.meta as AFSEntryMetadata,\n createdAt: options?.createdAt,\n updatedAt: options?.updatedAt,\n };\n }\n\n /**\n * Get the router (for testing/debugging)\n */\n protected getRouter(): ProviderRouter {\n return this.router;\n }\n}\n"],"mappings":"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"}