@aigne/afs 1.3.0 → 1.4.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,111 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0-beta.10](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.9...afs-v1.4.0-beta.10) (2026-01-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * **afs:** improve list behavior to always include current path ([cb91f80](https://github.com/AIGNE-io/aigne-framework/commit/cb91f80c6ea3aa6e93dde26b6feeea8689fceb48))
9
+
10
+ ## [1.4.0-beta.9](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.8...afs-v1.4.0-beta.9) (2026-01-14)
11
+
12
+
13
+ ### Features
14
+
15
+ * **afs:** add module access control and schema validation support ([#904](https://github.com/AIGNE-io/aigne-framework/issues/904)) ([d0b279a](https://github.com/AIGNE-io/aigne-framework/commit/d0b279aac07ebe2bcc1fd4148498fc3f6bbcd561))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * improve test coverage tracking and reporting ([#903](https://github.com/AIGNE-io/aigne-framework/issues/903)) ([031144e](https://github.com/AIGNE-io/aigne-framework/commit/031144e74f29e882cffe52ffda8f7a18c76ace7f))
21
+
22
+
23
+ ### Dependencies
24
+
25
+ * The following workspace dependencies were updated
26
+ * dependencies
27
+ * @aigne/platform-helpers bumped to 0.6.7-beta.2
28
+
29
+ ## [1.4.0-beta.8](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.7...afs-v1.4.0-beta.8) (2026-01-12)
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * **afs:** show gitignored files with marker instead of filtering ([c2bdea1](https://github.com/AIGNE-io/aigne-framework/commit/c2bdea155f47c9420f2fe810cdfed79ef70ef899))
35
+
36
+ ## [1.4.0-beta.7](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.6...afs-v1.4.0-beta.7) (2026-01-08)
37
+
38
+
39
+ ### Features
40
+
41
+ * **afs,bash:** add physical path mapping for AFS modules in bash execution ([#881](https://github.com/AIGNE-io/aigne-framework/issues/881)) ([50dbda2](https://github.com/AIGNE-io/aigne-framework/commit/50dbda224bd666d951494d2449779830d8db57fc))
42
+
43
+
44
+ ### Bug Fixes
45
+
46
+ * bump version ([696560f](https://github.com/AIGNE-io/aigne-framework/commit/696560fa2673eddcb4d00ac0523fbbbde7273cb3))
47
+
48
+
49
+ ### Dependencies
50
+
51
+ * The following workspace dependencies were updated
52
+ * dependencies
53
+ * @aigne/platform-helpers bumped to 0.6.7-beta.1
54
+
55
+ ## [1.4.0-beta.6](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.5...afs-v1.4.0-beta.6) (2026-01-06)
56
+
57
+
58
+ ### Bug Fixes
59
+
60
+ * **afs:** throw errors instead of logging in AFS module operations ([#874](https://github.com/AIGNE-io/aigne-framework/issues/874)) ([f0cc1c4](https://github.com/AIGNE-io/aigne-framework/commit/f0cc1c4056f8b95b631d595892bb12eb75da4b9f))
61
+
62
+ ## [1.4.0-beta.5](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.4...afs-v1.4.0-beta.5) (2025-12-31)
63
+
64
+
65
+ ### Features
66
+
67
+ * add session compact support for AIAgent ([#863](https://github.com/AIGNE-io/aigne-framework/issues/863)) ([9010918](https://github.com/AIGNE-io/aigne-framework/commit/9010918cd3f18b02b5c60ddc9ed5c34b568d0b28))
68
+
69
+ ## [1.4.0-beta.4](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.3...afs-v1.4.0-beta.4) (2025-12-26)
70
+
71
+
72
+ ### Features
73
+
74
+ * **core:** add session history support ([#858](https://github.com/AIGNE-io/aigne-framework/issues/858)) ([28a070e](https://github.com/AIGNE-io/aigne-framework/commit/28a070ed33b821d1fd344b899706d817ca992b9f))
75
+
76
+ ## [1.4.0-beta.3](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.2...afs-v1.4.0-beta.3) (2025-12-24)
77
+
78
+
79
+ ### Features
80
+
81
+ * add Agent Skill support ([#787](https://github.com/AIGNE-io/aigne-framework/issues/787)) ([f04fbe7](https://github.com/AIGNE-io/aigne-framework/commit/f04fbe76ec24cf3c59c74adf92d87b0c3784a8f7))
82
+
83
+ ## [1.4.0-beta.2](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta.1...afs-v1.4.0-beta.2) (2025-12-19)
84
+
85
+
86
+ ### Bug Fixes
87
+
88
+ * **afs:** use simple-list instead of tree as default type ([#839](https://github.com/AIGNE-io/aigne-framework/issues/839)) ([65a9a40](https://github.com/AIGNE-io/aigne-framework/commit/65a9a4054b3bdad6f7e40357299ef3dc48f7c3e4))
89
+
90
+ ## [1.4.0-beta.1](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.4.0-beta...afs-v1.4.0-beta.1) (2025-12-17)
91
+
92
+
93
+ ### Bug Fixes
94
+
95
+ * bump version ([70d217c](https://github.com/AIGNE-io/aigne-framework/commit/70d217c8360dd0dda7f5f17011c4e92ec836e801))
96
+
97
+ ## [1.4.0-beta](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.3.0...afs-v1.4.0-beta) (2025-12-17)
98
+
99
+
100
+ ### Features
101
+
102
+ * **afs:** support expand context into prompt template by call `$afs.xxx` ([#830](https://github.com/AIGNE-io/aigne-framework/issues/830)) ([5616acd](https://github.com/AIGNE-io/aigne-framework/commit/5616acd6ea257c91aa0b766608f45c5ce17f0345))
103
+
104
+
105
+ ### Bug Fixes
106
+
107
+ * **afs:** read method should not throw not found error ([#835](https://github.com/AIGNE-io/aigne-framework/issues/835)) ([ebfdfc1](https://github.com/AIGNE-io/aigne-framework/commit/ebfdfc1cdba23efd23ac2ad4621e3f046990fd8b))
108
+
3
109
  ## [1.3.0](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.3.0-beta.3...afs-v1.3.0) (2025-12-12)
4
110
 
5
111
  ## [1.3.0-beta.3](https://github.com/AIGNE-io/aigne-framework/compare/afs-v1.3.0-beta.2...afs-v1.3.0-beta.3) (2025-12-11)
package/lib/cjs/afs.d.ts CHANGED
@@ -1,12 +1,19 @@
1
1
  import { Emitter } from "strict-event-emitter";
2
- import type { AFSDeleteOptions, AFSEntry, AFSListOptions, AFSModule, AFSRenameOptions, AFSRoot, AFSRootEvents, AFSSearchOptions, AFSWriteEntryPayload, AFSWriteOptions } from "./type.js";
2
+ import { type AFSContext, type AFSDeleteOptions, type AFSDeleteResult, type AFSExecOptions, type AFSExecResult, type AFSModule, type AFSReadOptions, type AFSReadResult, type AFSRenameOptions, type AFSRenameResult, type AFSRoot, type AFSRootEvents, type AFSRootListOptions, type AFSRootListResult, type AFSRootSearchOptions, type AFSRootSearchResult, type AFSWriteEntryPayload, type AFSWriteOptions, type AFSWriteResult } from "./type.js";
3
3
  export interface AFSOptions {
4
4
  modules?: AFSModule[];
5
+ context?: AFSContext;
5
6
  }
6
7
  export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
8
+ options: AFSOptions;
7
9
  name: string;
8
10
  constructor(options?: AFSOptions);
9
11
  private modules;
12
+ /**
13
+ * Check if write operations are allowed for the given module.
14
+ * Throws AFSReadonlyError if not allowed.
15
+ */
16
+ private checkWritePermission;
10
17
  mount(module: AFSModule): this;
11
18
  listModules(): Promise<{
12
19
  name: string;
@@ -14,32 +21,22 @@ export declare class AFS extends Emitter<AFSRootEvents> implements AFSRoot {
14
21
  description?: string;
15
22
  module: AFSModule;
16
23
  }[]>;
17
- list(path: string, options?: AFSListOptions): Promise<{
18
- list: AFSEntry[];
19
- message?: string;
20
- }>;
21
- read(path: string): Promise<{
22
- result?: AFSEntry;
23
- message?: string;
24
- }>;
25
- write(path: string, content: AFSWriteEntryPayload, options?: AFSWriteOptions): Promise<{
26
- result: AFSEntry;
27
- message?: string;
28
- }>;
29
- delete(path: string, options?: AFSDeleteOptions): Promise<{
30
- message?: string;
31
- }>;
32
- rename(oldPath: string, newPath: string, options?: AFSRenameOptions): Promise<{
33
- message?: string;
34
- }>;
35
- search(path: string, query: string, options?: AFSSearchOptions): Promise<{
36
- list: AFSEntry[];
37
- message?: string;
38
- }>;
24
+ list(path: string, options?: AFSRootListOptions): Promise<AFSRootListResult>;
25
+ private _list;
26
+ read(path: string, _options?: AFSReadOptions): Promise<AFSReadResult>;
27
+ write(path: string, content: AFSWriteEntryPayload, options?: AFSWriteOptions): Promise<AFSWriteResult>;
28
+ delete(path: string, options?: AFSDeleteOptions): Promise<AFSDeleteResult>;
29
+ rename(oldPath: string, newPath: string, options?: AFSRenameOptions): Promise<AFSRenameResult>;
30
+ search(path: string, query: string, options?: AFSRootSearchOptions): Promise<AFSRootSearchResult>;
31
+ private processWithPreset;
32
+ private _select;
33
+ private _search;
39
34
  private findModules;
40
- exec(path: string, args: Record<string, any>, options: {
41
- context: any;
42
- }): Promise<{
43
- result: Record<string, any>;
44
- }>;
35
+ exec(path: string, args: Record<string, any>, options: AFSExecOptions): Promise<AFSExecResult>;
36
+ private buildSimpleListView;
37
+ private buildTreeView;
38
+ private buildMetadataSuffix;
39
+ private physicalPath?;
40
+ initializePhysicalPath(): Promise<string>;
41
+ cleanupPhysicalPath(): Promise<void>;
45
42
  }
package/lib/cjs/afs.js CHANGED
@@ -1,25 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AFS = void 0;
4
+ const index_js_1 = require("@aigne/platform-helpers/nodejs/index.js");
5
+ const uuid_1 = require("@aigne/uuid");
4
6
  const strict_event_emitter_1 = require("strict-event-emitter");
5
7
  const ufo_1 = require("ufo");
8
+ const zod_1 = require("zod");
9
+ const error_js_1 = require("./error.js");
10
+ const type_js_1 = require("./type.js");
6
11
  const DEFAULT_MAX_DEPTH = 1;
7
12
  const MODULES_ROOT_DIR = "/modules";
8
13
  class AFS extends strict_event_emitter_1.Emitter {
14
+ options;
9
15
  name = "AFSRoot";
10
- constructor(options) {
16
+ constructor(options = {}) {
11
17
  super();
18
+ this.options = options;
12
19
  for (const module of options?.modules ?? []) {
13
20
  this.mount(module);
14
21
  }
15
22
  }
16
23
  modules = new Map();
24
+ /**
25
+ * Check if write operations are allowed for the given module.
26
+ * Throws AFSReadonlyError if not allowed.
27
+ */
28
+ checkWritePermission(module, operation, path) {
29
+ // Module-level readonly (undefined means readonly by default)
30
+ if (module.accessMode !== "readwrite") {
31
+ throw new error_js_1.AFSReadonlyError(`Module '${module.name}' is readonly, cannot perform ${operation} to ${path}`);
32
+ }
33
+ }
17
34
  mount(module) {
18
- let path = (0, ufo_1.joinURL)("/", module.name);
19
- if (!/^\/[^/]+$/.test(path)) {
20
- throw new Error(`Invalid mount path: ${path}. Must start with '/' and contain no other '/'`);
35
+ // Validate module name (should not contain '/')
36
+ if (module.name.includes("/")) {
37
+ throw new Error(`Invalid module name: ${module.name}. Module name must not contain '/'`);
21
38
  }
22
- path = (0, ufo_1.joinURL)(MODULES_ROOT_DIR, path);
39
+ const path = (0, ufo_1.joinURL)(MODULES_ROOT_DIR, module.name);
23
40
  if (this.modules.has(path)) {
24
41
  throw new Error(`Module already mounted at path: ${path}`);
25
42
  }
@@ -35,74 +52,101 @@ class AFS extends strict_event_emitter_1.Emitter {
35
52
  module,
36
53
  }));
37
54
  }
38
- async list(path, options) {
39
- const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
40
- if (!(maxDepth >= 0))
41
- throw new Error(`Invalid maxDepth: ${maxDepth}`);
55
+ async list(path, options = {}) {
56
+ let preset;
57
+ if (options.preset) {
58
+ preset = this.options?.context?.list?.presets?.[options.preset];
59
+ if (!preset)
60
+ throw new Error(`Preset not found: ${options.preset}`);
61
+ }
62
+ return await this.processWithPreset(path, undefined, preset, {
63
+ ...options,
64
+ defaultSelect: () => this._list(path, options),
65
+ });
66
+ }
67
+ async _list(path, options = {}) {
42
68
  const results = [];
43
- const messages = [];
69
+ // Special case: listing root "/" should return /modules directory
70
+ if (path === "/" && this.modules.size > 0) {
71
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
72
+ // Always include /modules directory first
73
+ results.push({
74
+ id: "modules",
75
+ path: MODULES_ROOT_DIR,
76
+ summary: "All mounted modules",
77
+ });
78
+ if (maxDepth === 1) {
79
+ // Only show /modules directory
80
+ return { data: results };
81
+ }
82
+ // For maxDepth > 1, also get children of /modules with reduced depth
83
+ const childrenResult = await this._list(MODULES_ROOT_DIR, {
84
+ ...options,
85
+ maxDepth: maxDepth - 1,
86
+ });
87
+ results.push(...childrenResult.data);
88
+ return { data: results };
89
+ }
44
90
  const matches = this.findModules(path, options);
45
91
  for (const matched of matches) {
46
- const moduleEntry = {
47
- id: matched.module.name,
48
- path: matched.remainedModulePath,
49
- summary: matched.module.description,
50
- };
51
92
  if (matched.maxDepth === 0) {
93
+ // When maxDepth is 0, show the module entry
94
+ const moduleEntry = {
95
+ id: matched.module.name,
96
+ path: matched.modulePath,
97
+ summary: matched.module.description,
98
+ };
52
99
  results.push(moduleEntry);
53
100
  continue;
54
101
  }
55
102
  if (!matched.module.list)
56
103
  continue;
57
104
  try {
58
- const { list, message } = await matched.module.list(matched.subpath, {
105
+ const { data } = await matched.module.list(matched.subpath, {
59
106
  ...options,
60
107
  maxDepth: matched.maxDepth,
61
108
  });
62
- if (list.length) {
63
- results.push(...list.map((entry) => ({
64
- ...entry,
65
- path: (0, ufo_1.joinURL)(matched.modulePath, entry.path),
66
- })));
67
- }
68
- else {
69
- results.push(moduleEntry);
70
- }
71
- if (message)
72
- messages.push(message);
109
+ const children = data.map((entry) => ({
110
+ ...entry,
111
+ path: (0, ufo_1.joinURL)(matched.modulePath, entry.path),
112
+ }));
113
+ // Always include all nodes (including the current path itself)
114
+ // This ensures consistent behavior across all listing scenarios
115
+ results.push(...children);
73
116
  }
74
117
  catch (error) {
75
- console.error(`Error listing from module at ${matched.modulePath}`, error);
118
+ throw new Error(`Error listing from module at ${matched.modulePath}: ${error.message}`);
76
119
  }
77
120
  }
78
- return { list: results, message: messages.join("; ").trim() || undefined };
121
+ return { data: results };
79
122
  }
80
- async read(path) {
123
+ async read(path, _options) {
81
124
  const modules = this.findModules(path, { exactMatch: true });
82
125
  for (const { module, modulePath, subpath } of modules) {
83
126
  const res = await module.read?.(subpath);
84
- if (res?.result) {
127
+ if (res?.data) {
85
128
  return {
86
129
  ...res,
87
- result: {
88
- ...res.result,
89
- path: (0, ufo_1.joinURL)(modulePath, res.result.path),
130
+ data: {
131
+ ...res.data,
132
+ path: (0, ufo_1.joinURL)(modulePath, res.data.path),
90
133
  },
91
134
  };
92
135
  }
93
136
  }
94
- return { result: undefined, message: "File not found" };
137
+ return { data: undefined, message: "File not found" };
95
138
  }
96
139
  async write(path, content, options) {
97
140
  const module = this.findModules(path, { exactMatch: true })[0];
98
141
  if (!module?.module.write)
99
142
  throw new Error(`No module found for path: ${path}`);
143
+ this.checkWritePermission(module.module, "write", path);
100
144
  const res = await module.module.write(module.subpath, content, options);
101
145
  return {
102
146
  ...res,
103
- result: {
104
- ...res.result,
105
- path: (0, ufo_1.joinURL)(module.modulePath, res.result.path),
147
+ data: {
148
+ ...res.data,
149
+ path: (0, ufo_1.joinURL)(module.modulePath, res.data.path),
106
150
  },
107
151
  };
108
152
  }
@@ -110,6 +154,7 @@ class AFS extends strict_event_emitter_1.Emitter {
110
154
  const module = this.findModules(path, { exactMatch: true })[0];
111
155
  if (!module?.module.delete)
112
156
  throw new Error(`No module found for path: ${path}`);
157
+ this.checkWritePermission(module.module, "delete", path);
113
158
  return await module.module.delete(module.subpath, options);
114
159
  }
115
160
  async rename(oldPath, newPath, options) {
@@ -122,17 +167,64 @@ class AFS extends strict_event_emitter_1.Emitter {
122
167
  if (!oldModule.module.rename) {
123
168
  throw new Error(`Module does not support rename operation: ${oldModule.modulePath}`);
124
169
  }
170
+ this.checkWritePermission(oldModule.module, "rename", oldPath);
125
171
  return await oldModule.module.rename(oldModule.subpath, newModule.subpath, options);
126
172
  }
127
- async search(path, query, options) {
173
+ async search(path, query, options = {}) {
174
+ let preset;
175
+ if (options.preset) {
176
+ preset = this.options?.context?.search?.presets?.[options.preset];
177
+ if (!preset)
178
+ throw new Error(`Preset not found: ${options.preset}`);
179
+ }
180
+ return await this.processWithPreset(path, query, preset, {
181
+ ...options,
182
+ defaultSelect: () => this._search(path, query, options),
183
+ });
184
+ }
185
+ async processWithPreset(path, query, preset, options) {
186
+ const select = options.select || preset?.select;
187
+ const per = options.per || preset?.per;
188
+ const dedupe = options.dedupe || preset?.dedupe;
189
+ const format = options.format || preset?.format;
190
+ const entries = select
191
+ ? (await this._select(path, query, select, options)).data
192
+ : (await options.defaultSelect()).data;
193
+ const mapped = per
194
+ ? await Promise.all(entries.map((data) => per.invoke({ data }, options).then((res) => res.data)))
195
+ : entries;
196
+ const deduped = dedupe
197
+ ? await dedupe.invoke({ data: mapped }, options).then((res) => res.data)
198
+ : mapped;
199
+ let formatted = deduped;
200
+ if (format === "simple-list" || format === "tree") {
201
+ const valid = zod_1.z.array(type_js_1.afsEntrySchema).safeParse(deduped);
202
+ if (!valid.data)
203
+ throw new Error("Tree format requires entries to be AFSEntry objects");
204
+ if (format === "tree")
205
+ formatted = this.buildTreeView(valid.data);
206
+ else if (format === "simple-list")
207
+ formatted = this.buildSimpleListView(valid.data);
208
+ }
209
+ else if (typeof format === "object" && typeof format.invoke === "function") {
210
+ formatted = await format.invoke({ data: deduped }, options).then((res) => res.data);
211
+ }
212
+ return { data: formatted };
213
+ }
214
+ async _select(path, query, select, options) {
215
+ const { data } = await select.invoke({ path, query }, options);
216
+ const results = (await Promise.all(data.map((p) => this.read(p).then((res) => res.data)))).filter((i) => !!i);
217
+ return { data: results };
218
+ }
219
+ async _search(path, query, options) {
128
220
  const results = [];
129
221
  const messages = [];
130
222
  for (const { module, modulePath, subpath } of this.findModules(path)) {
131
223
  if (!module.search)
132
224
  continue;
133
225
  try {
134
- const { list, message } = await module.search(subpath, query, options);
135
- results.push(...list.map((entry) => ({
226
+ const { data, message } = await module.search(subpath, query, options);
227
+ results.push(...data.map((entry) => ({
136
228
  ...entry,
137
229
  path: (0, ufo_1.joinURL)(modulePath, entry.path),
138
230
  })));
@@ -140,10 +232,10 @@ class AFS extends strict_event_emitter_1.Emitter {
140
232
  messages.push(message);
141
233
  }
142
234
  catch (error) {
143
- console.error(`Error searching in module at ${modulePath}`, error);
235
+ throw new Error(`Error searching in module at ${modulePath}: ${error.message}`);
144
236
  }
145
237
  }
146
- return { list: results, message: messages.join("; ") };
238
+ return { data: results, message: messages.join("; ") };
147
239
  }
148
240
  findModules(path, options) {
149
241
  const maxDepth = Math.max(options?.maxDepth ?? DEFAULT_MAX_DEPTH, 1);
@@ -179,5 +271,80 @@ class AFS extends strict_event_emitter_1.Emitter {
179
271
  throw new Error(`No module found for path: ${path}`);
180
272
  return await module.module.exec(module.subpath, args, options);
181
273
  }
274
+ buildSimpleListView(entries) {
275
+ return entries.map((entry) => `${entry.path}${this.buildMetadataSuffix(entry)}`);
276
+ }
277
+ buildTreeView(entries) {
278
+ const tree = {};
279
+ const entryMap = new Map();
280
+ for (const entry of entries) {
281
+ entryMap.set(entry.path, entry);
282
+ const parts = entry.path.split("/").filter(Boolean);
283
+ let current = tree;
284
+ for (const part of parts) {
285
+ if (!current[part]) {
286
+ current[part] = {};
287
+ }
288
+ current = current[part];
289
+ }
290
+ }
291
+ const renderTree = (node, prefix = "", currentPath = "") => {
292
+ let result = "";
293
+ const keys = Object.keys(node);
294
+ keys.forEach((key, index) => {
295
+ const isLast = index === keys.length - 1;
296
+ const fullPath = currentPath ? `${currentPath}/${key}` : `/${key}`;
297
+ const entry = entryMap.get(fullPath);
298
+ result += `${prefix}${isLast ? "└── " : "├── "}${key}${entry ? this.buildMetadataSuffix(entry) : ""}`;
299
+ result += `\n`;
300
+ result += renderTree(node[key], `${prefix}${isLast ? " " : "│ "}`, fullPath);
301
+ });
302
+ return result;
303
+ };
304
+ return renderTree(tree);
305
+ }
306
+ buildMetadataSuffix(entry) {
307
+ // Build metadata suffix
308
+ const metadataParts = [];
309
+ // Children count
310
+ const childrenCount = entry?.metadata?.childrenCount;
311
+ if (typeof childrenCount === "number") {
312
+ metadataParts.push(`${childrenCount} items`);
313
+ }
314
+ // Children truncated
315
+ if (entry?.metadata?.childrenTruncated) {
316
+ metadataParts.push("truncated");
317
+ }
318
+ // Gitignored
319
+ if (entry?.metadata?.gitignored) {
320
+ metadataParts.push("gitignored");
321
+ }
322
+ // Executable
323
+ if (entry?.metadata?.execute) {
324
+ metadataParts.push("executable");
325
+ }
326
+ const metadataSuffix = metadataParts.length > 0 ? ` [${metadataParts.join(", ")}]` : "";
327
+ return metadataSuffix;
328
+ }
329
+ physicalPath;
330
+ async initializePhysicalPath() {
331
+ this.physicalPath ??= (async () => {
332
+ const rootDir = index_js_1.nodejs.path.join(index_js_1.nodejs.os.tmpdir(), (0, uuid_1.v7)());
333
+ await index_js_1.nodejs.fs.mkdir(rootDir, { recursive: true });
334
+ for (const [modulePath, module] of this.modules) {
335
+ const physicalModulePath = index_js_1.nodejs.path.join(rootDir, modulePath);
336
+ await index_js_1.nodejs.fs.mkdir(index_js_1.nodejs.path.dirname(physicalModulePath), { recursive: true });
337
+ await module.symlinkToPhysical?.(physicalModulePath);
338
+ }
339
+ return rootDir;
340
+ })();
341
+ return this.physicalPath;
342
+ }
343
+ async cleanupPhysicalPath() {
344
+ if (this.physicalPath) {
345
+ await index_js_1.nodejs.fs.rm(await this.physicalPath, { recursive: true, force: true });
346
+ this.physicalPath = undefined;
347
+ }
348
+ }
182
349
  }
183
350
  exports.AFS = AFS;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Base error class for all AFS errors.
3
+ */
4
+ export declare class AFSError extends Error {
5
+ readonly code: string;
6
+ constructor(message: string, code: string);
7
+ }
8
+ /**
9
+ * Error thrown when attempting write operations on a readonly AFS or module.
10
+ */
11
+ export declare class AFSReadonlyError extends AFSError {
12
+ constructor(message: string);
13
+ }
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AFSReadonlyError = exports.AFSError = void 0;
4
+ /**
5
+ * Base error class for all AFS errors.
6
+ */
7
+ class AFSError extends Error {
8
+ code;
9
+ constructor(message, code) {
10
+ super(message);
11
+ this.name = "AFSError";
12
+ this.code = code;
13
+ }
14
+ }
15
+ exports.AFSError = AFSError;
16
+ /**
17
+ * Error thrown when attempting write operations on a readonly AFS or module.
18
+ */
19
+ class AFSReadonlyError extends AFSError {
20
+ constructor(message) {
21
+ super(message, "AFS_READONLY");
22
+ this.name = "AFSReadonlyError";
23
+ }
24
+ }
25
+ exports.AFSReadonlyError = AFSReadonlyError;
@@ -1,2 +1,3 @@
1
1
  export * from "./afs.js";
2
+ export * from "./error.js";
2
3
  export * from "./type.js";
package/lib/cjs/index.js CHANGED
@@ -15,4 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./afs.js"), exports);
18
+ __exportStar(require("./error.js"), exports);
18
19
  __exportStar(require("./type.js"), exports);