1use std::collections::BTreeMap;
5
6use crate::{
7 Dispatch, MethodSig, PackageDeclaration, Param, PropertySig, Span, Type, TypeKind, TypeSymbol,
8 ValueKind, ValueSymbol,
9};
10
11use super::MODULE_NAME;
12
13pub fn package_declaration() -> PackageDeclaration {
14 let mut defs = PackageDeclaration::with_package(MODULE_NAME);
15
16 let path_param = Param::new("path", Type::String);
17
18 insert_recursive_options(&mut defs);
19 insert_stat_interface(&mut defs);
20 insert_peek_interface(&mut defs);
21 insert_file_writer_interface(&mut defs);
22 insert_dir_entry_interface(&mut defs);
23 insert_mount_info_interface(&mut defs);
24 insert_info_interface(&mut defs);
25
26 insert_fn(
27 &mut defs,
28 "cwd",
29 Vec::new(),
30 Type::String,
31 "/** The absolute guest working directory used by relative paths. Shared by the program and packages; no capability required. Defaults to `/`. */",
32 );
33 insert_fn(
34 &mut defs,
35 "maxReadSize",
36 Vec::new(),
37 Type::Number,
38 "/**\n * Maximum number of bytes `read` / `readText` will load whole; past this they throw. Also the upper bound on `readBytes(path, offset, length)`'s `length`. Configured by the harness per Store; defaults to the 50 MB tier memory cap.\n */",
39 );
40 insert_fn(
41 &mut defs,
42 "info",
43 Vec::new(),
44 Type::InterfaceRef {
45 mangled: crate::mangle::package_symbol(MODULE_NAME, "Info"),
46 package: crate::Package(MODULE_NAME.to_string()),
47 name: "Info".to_string(),
48 args: Vec::new(),
49 },
50 "/**\n * The active VFS configuration: the root's `mode` (`\"none\"` / `\"ephemeral\"` / `\"per_session\"` / `\"named\"`), `access`, `volume` and `sizeLimit` (the cap on the bytes its files may hold, or `-1` when none applies), plus `mounts`, the named volumes grafted below the root. Deterministic; no capability required. The host directories backing the VFS are never exposed.\n */",
51 );
52 insert_fn(
53 &mut defs,
54 "exists",
55 vec![path_param.clone()],
56 Type::Boolean,
57 "/**\n * Returns `true` iff `path` resolves to a filesystem entry under the VFS root. Symlinks count as existing; broken symlinks count as not existing (mirrors `std::path::exists`).\n * @param path File path under the VFS root.\n * @capability fs.stat { path }\n */",
58 );
59 insert_fn(
60 &mut defs,
61 "summary",
62 vec![path_param.clone()],
63 Type::String,
64 "/**\n * Describe `path` in a few lines of text, for a program that needs to see what is there before deciding what to read. A directory lists its direct children — directories first, then by name — each with its kind, size and modification time, under a count line; an empty directory says `empty directory`. A file reports its size and modification time, and a missing path says `does not exist` rather than throwing. Shows at most 100 entries and counts the rest; iterate `list(path)` to visit all of them.\n * @param path File or directory path under the VFS root.\n * @capability fs.list { path, recursive }\n */",
65 );
66 insert_fn(
67 &mut defs,
68 "isEmpty",
69 vec![path_param.clone()],
70 Type::Boolean,
71 "/**\n * Returns `true` when the directory at `path` has no entries, or the file at `path` has zero bytes. Throws when `path` does not exist — call `exists(path)` first if it may not.\n * @param path File or directory path under the VFS root.\n * @capability fs.list { path, recursive }\n */",
72 );
73 insert_fn(
74 &mut defs,
75 "size",
76 vec![path_param.clone()],
77 Type::Number,
78 "/**\n * File size in bytes. Traps on directories or missing paths — call `exists(path)` first or use `stat(path)` if you need to handle absence.\n * @param path File path under the VFS root.\n * @capability fs.stat { path }\n */",
79 );
80 insert_fn(
81 &mut defs,
82 "stat",
83 vec![path_param.clone()],
84 Type::InterfaceRef {
85 mangled: crate::mangle::package_symbol(MODULE_NAME, "Stat"),
86 package: crate::Package(MODULE_NAME.to_string()),
87 name: "Stat".to_string(),
88 args: Vec::new(),
89 },
90 "/**\n * Metadata about an entry under the VFS root. Throws when the path does not exist; otherwise returns a `Stat` carrying `kind` / `size` / `modifiedAt`.\n * @param path File path under the VFS root.\n * @capability fs.stat { path }\n */",
91 );
92 insert_fn(
93 &mut defs,
94 "peek",
95 vec![path_param.clone()],
96 Type::InterfaceRef {
97 mangled: crate::mangle::package_symbol(MODULE_NAME, "Peek"),
98 package: crate::Package(MODULE_NAME.to_string()),
99 name: "Peek".to_string(),
100 args: Vec::new(),
101 },
102 "/**\n * Quick file-only inspection: preview (first ~256 bytes decoded UTF-8 lossy), detected `encoding` (`utf-8` / `utf-16le` / `utf-16be` / `latin1`), `lineEnding` (`lf` / `crlf`), and total `size`. Traps on directories / missing paths.\n * @param path File path under the VFS root.\n * @capability fs.stat { path }\n */",
103 );
104 insert_fn(
105 &mut defs,
106 "read",
107 vec![path_param.clone()],
108 Type::Uint8Array,
109 "/**\n * Read the whole file as a `Uint8Array`. Throws when the file is larger than `maxReadSize()`. Traps on missing paths or directories.\n * @param path File path under the VFS root.\n * @capability fs.read { path }\n */",
110 );
111 insert_fn(
112 &mut defs,
113 "readText",
114 vec![path_param.clone()],
115 Type::String,
116 "/**\n * Read the whole file as a UTF-8 string. Throws when the file is larger than `maxReadSize()`. Strips a leading UTF-8 BOM by default; invalid bytes are replaced by U+FFFD.\n * @param path File path under the VFS root.\n * @capability fs.read { path }\n */",
117 );
118 insert_fn(
119 &mut defs,
120 "write",
121 vec![path_param.clone(), Param::new("content", Type::Uint8Array)],
122 Type::Void,
123 "/**\n * Write `content` to `path` atomically (temp-file + rename). Throws when the parent directory does not exist — agents must `mkdir(parent, { recursive: true })` first.\n * @param path File path under the VFS root.\n * @param content Bytes to write.\n * @capability fs.write { path, length: number }\n */",
124 );
125 insert_fn(
126 &mut defs,
127 "writeText",
128 vec![path_param.clone(), Param::new("content", Type::String)],
129 Type::Void,
130 "/**\n * UTF-8 atomic write. Same parent-directory rule as `write`.\n * @param path File path under the VFS root.\n * @param content Text to write as UTF-8.\n * @capability fs.write { path, length: number }\n */",
131 );
132 insert_fn(
133 &mut defs,
134 "append",
135 vec![path_param.clone(), Param::new("content", Type::Uint8Array)],
136 Type::Void,
137 "/**\n * Append `content` to `path`. Creates the file if missing; throws when the parent directory does not exist. NOT atomic — concurrent writers race.\n * @param path File path under the VFS root.\n * @param content Bytes to append.\n * @capability fs.write { path, length: number }\n */",
138 );
139 insert_fn(
140 &mut defs,
141 "appendText",
142 vec![path_param.clone(), Param::new("content", Type::String)],
143 Type::Void,
144 "/**\n * UTF-8 append. Same parent-directory rule as `append`.\n * @param path File path under the VFS root.\n * @param content Text to append as UTF-8.\n * @capability fs.write { path, length: number }\n */",
145 );
146 insert_fn(
147 &mut defs,
148 "readBytes",
149 vec![
150 path_param.clone(),
151 Param::new("offset", Type::Number),
152 Param::new("length", Type::Number),
153 ],
154 Type::Uint8Array,
155 "/**\n * Random-access byte-range read. Traps if `length > maxReadSize()`, if `offset` is negative, or on I/O errors. The returned `Uint8Array` may be shorter than `length` if the request runs past EOF.\n * @param path File path under the VFS root.\n * @param offset Zero-based byte offset.\n * @param length Maximum number of bytes to read.\n * @capability fs.read { path, length }\n */",
156 );
157 insert_fn(
158 &mut defs,
159 "mkdir",
160 vec![path_param.clone(), recursive_options_param()],
161 Type::Void,
162 "/**\n * Create the directory at `path`. `options.recursive=true` creates intermediate directories (like `mkdir -p`); `options.recursive=false` throws when the parent is missing or the target already exists. Pass `{ recursive: true }` to recurse; omitted options default to false.\n * @param path Directory path under the VFS root.\n * @param options Whether to create missing parents.\n * @capability fs.mkdir { path, recursive }\n */",
163 );
164 insert_fn(
165 &mut defs,
166 "remove",
167 vec![path_param.clone(), recursive_options_param()],
168 Type::Void,
169 "/**\n * Remove the entry at `path`. Files unconditionally; directories require `options.recursive=true` (matches `rm -rf`). Without the flag, a non-empty directory throws.\n * @param path File or directory path under the VFS root.\n * @param options Whether to remove directories recursively.\n * @capability fs.remove { path, recursive }\n */",
170 );
171 insert_fn(
172 &mut defs,
173 "move",
174 vec![
175 Param::new("from", Type::String),
176 Param::new("to", Type::String),
177 ],
178 Type::Void,
179 "/**\n * Move / rename. Atomic within one volume. Between volumes (the root and a mount, or two mounts — see `info().mounts`) it copies, then removes the source, so it is not atomic: if removing the source fails, the destination is kept and the call throws with both copies present. Replacing a file in another volume needs room there for both versions until the move completes, and a link the copy cannot reproduce fails the move before the source is touched.\n * @param from Source path under the VFS root.\n * @param to Destination path under the VFS root.\n * @capability fs.move { from, to }\n */",
180 );
181 insert_fn(
182 &mut defs,
183 "copy",
184 vec![
185 Param::new("from", Type::String),
186 Param::new("to", Type::String),
187 Param::new("recursive", Type::Boolean),
188 ],
189 Type::Void,
190 "/**\n * Copy `from` to `to`. Directories require `recursive=true`. Symlinks are copied as links (the link target is preserved; the file it points at is not followed) so a writable VFS area can't be used to exfiltrate the target.\n * @param from Source path under the VFS root.\n * @param to Destination path under the VFS root.\n * @param recursive Whether to copy directories recursively.\n * @capability fs.copy { from, to, recursive }\n */",
191 );
192 insert_fn(
193 &mut defs,
194 "writer",
195 vec![path_param.clone()],
196 Type::InterfaceRef {
197 mangled: crate::mangle::package_symbol(MODULE_NAME, "FileWriter"),
198 package: crate::Package(MODULE_NAME.to_string()),
199 name: "FileWriter".to_string(),
200 args: Vec::new(),
201 },
202 "/**\n * Open `path` as an append-only `FileWriter`. The writer buffers internally and only commits on `close()` (atomic temp-file + rename) — half-written outputs from a crashed transform don't leak. Throws when the parent directory does not exist.\n * @param path Destination path under the VFS root.\n * @capability fs.write { path }\n */",
203 );
204 insert_fn(
205 &mut defs,
206 "lines",
207 vec![path_param.clone()],
208 Type::InterfaceRef {
209 mangled: crate::mangle::prelude("Iterator"),
210 package: crate::Package::prelude(),
211 name: "Iterator".to_string(),
212 args: vec![Type::String],
213 },
214 "/**\n * Stream the file at `path` as a constant-memory line iterator. Each `next()` returns one line stripped of its trailing CR/LF. A leading UTF-8 BOM on the first line is dropped. The file descriptor is released when the iterator is closed (a `for...of` loop closes it on exit, including `break`/`throw`) or, failing that, when it is garbage-collected.\n * @param path File path under the VFS root.\n * @capability fs.read { path }\n */",
215 );
216 insert_fn(
217 &mut defs,
218 "bytes",
219 vec![path_param.clone(), Param::new("chunkSize", Type::Number)],
220 Type::InterfaceRef {
221 mangled: crate::mangle::prelude("Iterator"),
222 package: crate::Package::prelude(),
223 name: "Iterator".to_string(),
224 args: vec![Type::Uint8Array],
225 },
226 "/**\n * Stream the file at `path` as a constant-memory byte iterator. Each `next()` returns up to `chunkSize` bytes; the last chunk may be shorter. Sized for large binary inputs that don't fit `maxReadSize`.\n * @param path File path under the VFS root.\n * @param chunkSize Maximum bytes per iterator chunk.\n * @capability fs.read { path, chunkSize }\n */",
227 );
228 insert_fn(
229 &mut defs,
230 "list",
231 vec![path_param.clone(), recursive_options_param()],
232 Type::InterfaceRef {
233 mangled: crate::mangle::prelude("Iterator"),
234 package: crate::Package::prelude(),
235 name: "Iterator".to_string(),
236 args: vec![Type::InterfaceRef {
237 mangled: crate::mangle::package_symbol(MODULE_NAME, "DirEntry"),
238 package: crate::Package(MODULE_NAME.to_string()),
239 name: "DirEntry".to_string(),
240 args: Vec::new(),
241 }],
242 },
243 "/**\n * List the children of the directory at `path` as a constant-memory iterator of `DirEntry`. `options.recursive=true` walks descendants as well; `options.recursive=false` yields only direct children.\n *\n * Order: a directory is always yielded before its contents, and a directory's whole subtree is yielded as one consecutive run — no entry from outside a subtree is interleaved into it. Down to 32 levels of nesting that is exactly depth-first (matching `find` / `os.walk`); deeper than that the walk runs out of directory handles and a subdirectory's contents follow the rest of its parent's entries instead of coming directly after it, so don't depend on strict depth-first order past that depth. Siblings come in whatever order the filesystem reports — not sorted.\n *\n * Symlinks surface as `kind=\"symlink\"` and are not followed. A recursive walk is best-effort: a subdirectory it cannot read — or, past a fixed ceiling on how many postponed descents one walk holds, cannot afford to remember — is skipped and the walk continues, so a listing is not proof that a path is absent.\n * @param path Directory path under the VFS root.\n * @param options Whether to walk descendants recursively.\n * @capability fs.list { path, recursive }\n */",
244 );
245
246 defs
247}
248
249fn insert_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type, doc: &str) {
250 defs.values.insert(
251 name.to_string(),
252 ValueSymbol {
253 name: name.to_string(),
254 mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
255 declaration_span: Span::at(crate::FileId::FS),
256 kind: ValueKind::Function {
257 generics: Default::default(),
258 params,
259 ret,
260 type_predicate: None,
261 doc: crate::doc(crate::FileId::FS, doc),
262 },
263 },
264 );
265}
266
267fn recursive_options_param() -> Param {
268 Param::with_default(
269 "options",
270 Type::union(vec![
271 Type::InterfaceRef {
272 mangled: crate::mangle::package_symbol(MODULE_NAME, "RecursiveOptions"),
273 package: crate::Package(MODULE_NAME.to_string()),
274 name: "RecursiveOptions".to_string(),
275 args: Vec::new(),
276 },
277 Type::Undefined,
278 ]),
279 crate::DefaultValue::Undefined,
280 )
281}
282
283fn insert_recursive_options(defs: &mut PackageDeclaration) {
284 let properties = BTreeMap::from([(
285 "recursive".to_string(),
286 PropertySig {
287 ty: Type::Boolean,
288 readonly: false,
289 intrinsic: false,
290 optional: true,
291 doc: crate::doc(
292 crate::FileId::FS,
293 "/** Include descendants or create missing parents. Defaults to false. */",
294 ),
295 },
296 )]);
297 defs.types.insert(
298 "RecursiveOptions".to_string(),
299 TypeSymbol {
300 name: "RecursiveOptions".to_string(),
301 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "RecursiveOptions"),
302 declaration_span: Span::at(crate::FileId::FS),
303 kind: TypeKind::Interface {
304 index: None,
305 generics: Default::default(),
306 methods: BTreeMap::new(),
307 properties,
308 dispatch: Dispatch::VTable,
309 doc: crate::doc(
310 crate::FileId::FS,
311 "/** Options for mkdir, remove and list. */",
312 ),
313 },
314 },
315 );
316}
317
318fn insert_stat_interface(defs: &mut PackageDeclaration) {
319 let mut properties = BTreeMap::new();
320 insert_property(
321 &mut properties,
322 "kind",
323 Type::String,
324 "/** One of `\"file\"` / `\"directory\"` / `\"symlink\"` / `\"other\"`. Symlinks are reported as `\"symlink\"` without dereferencing. */",
325 );
326 insert_property(
327 &mut properties,
328 "size",
329 Type::Number,
330 "/** File size in bytes. `0` for non-files (directories, symlinks, other). */",
331 );
332 insert_property(
333 &mut properties,
334 "modifiedAt",
335 Type::Number,
336 "/** Last-modified timestamp in milliseconds since the Unix epoch. `0` when the underlying filesystem doesn't report one. */",
337 );
338 defs.types.insert(
339 "Stat".to_string(),
340 TypeSymbol {
341 name: "Stat".to_string(),
342 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Stat"),
343 declaration_span: Span::at(crate::FileId::FS),
344 kind: TypeKind::Interface { index: None,
345 generics: Default::default(),
346 methods: BTreeMap::new(),
347 properties,
348 dispatch: Dispatch::VTable,
349 doc: crate::doc(crate::FileId::FS,
350 "/** Filesystem metadata returned by `stat(path)`. Returned by `stat`; user code reads `kind` / `size` / `modifiedAt` via property access. */",
351 ),
352 },
353 },
354 );
355}
356
357fn insert_peek_interface(defs: &mut PackageDeclaration) {
358 let mut properties = BTreeMap::new();
359 insert_property(
360 &mut properties,
361 "preview",
362 Type::String,
363 "/** First ~256 bytes of the file decoded as UTF-8 lossy. Use `lines` / `read` for full content. */",
364 );
365 insert_property(
366 &mut properties,
367 "encoding",
368 Type::String,
369 "/** Detected text encoding: `\"utf-8\"` / `\"utf-16le\"` / `\"utf-16be\"` / `\"latin1\"`. BOM-driven when a BOM is present; falls back to a UTF-8 validity check then `latin1`. */",
370 );
371 insert_property(
372 &mut properties,
373 "lineEnding",
374 Type::String,
375 "/** `\"crlf\"` if a CRLF sequence appears in the preview; `\"lf\"` otherwise (including files with no line terminators yet). */",
376 );
377 insert_property(
378 &mut properties,
379 "size",
380 Type::Number,
381 "/** Total file size in bytes — same value as `size(path)`. */",
382 );
383 defs.types.insert(
384 "Peek".to_string(),
385 TypeSymbol {
386 name: "Peek".to_string(),
387 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Peek"),
388 declaration_span: Span::at(crate::FileId::FS),
389 kind: TypeKind::Interface { index: None,
390 generics: Default::default(),
391 methods: BTreeMap::new(),
392 properties,
393 dispatch: Dispatch::VTable,
394 doc: crate::doc(crate::FileId::FS,
395 "/** Lightweight file-only inspection — preview + transport metadata. Returned by `peek`; user code reads fields via property access. */",
396 ),
397 },
398 },
399 );
400}
401
402fn insert_dir_entry_interface(defs: &mut PackageDeclaration) {
403 let mut properties = BTreeMap::new();
404 insert_property(
405 &mut properties,
406 "kind",
407 Type::String,
408 "/** `\"file\"` / `\"directory\"` / `\"symlink\"` / `\"other\"`. */",
409 );
410 insert_property(
411 &mut properties,
412 "name",
413 Type::String,
414 "/** Basename of the entry (no parent path). */",
415 );
416 insert_property(
417 &mut properties,
418 "path",
419 Type::String,
420 "/** Full guest-visible path under the VFS root, with a leading `/`. */",
421 );
422 insert_property(
423 &mut properties,
424 "size",
425 Type::Number,
426 "/** File size in bytes; `0` for non-files. */",
427 );
428 defs.types.insert(
429 "DirEntry".to_string(),
430 TypeSymbol {
431 name: "DirEntry".to_string(),
432 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "DirEntry"),
433 declaration_span: Span::at(crate::FileId::FS),
434 kind: TypeKind::Interface { index: None,
435 generics: Default::default(),
436 methods: BTreeMap::new(),
437 properties,
438 dispatch: Dispatch::VTable,
439 doc: crate::doc(crate::FileId::FS,
440 "/** Directory entry yielded by `list(path, options)`. Properties: `kind` / `name` / `path` / `size`. Returned by the iterator. */",
441 ),
442 },
443 },
444 );
445}
446
447fn insert_info_interface(defs: &mut PackageDeclaration) {
448 let mut properties = BTreeMap::new();
449 insert_property(
450 &mut properties,
451 "mode",
452 Type::String,
453 "/** The root's mode: `\"none\"` / `\"ephemeral\"` / `\"per_session\"` / `\"named\"`. */",
454 );
455 insert_property(
456 &mut properties,
457 "sizeLimit",
458 Type::Number,
459 "/** The most bytes the files in the root may hold, or `-1` when no limit applies. A write that would pass it throws a `QuotaExceededError`. A named volume's limit is shared with every session and blueprint that uses it. */",
460 );
461 insert_property(
462 &mut properties,
463 "access",
464 Type::String,
465 "/** `\"read_write\"`, or `\"read_only\"` when every write to the root throws a `PermissionDeniedError`. */",
466 );
467 insert_property(
468 &mut properties,
469 "volume",
470 Type::String,
471 "/** The named volume backing the root under `mode: \"named\"`; `\"\"` otherwise, and for a directory `submilli run --vfs` exposes. */",
472 );
473 insert_property(
474 &mut properties,
475 "mounts",
476 Type::Array(Box::new(mount_info_type())),
477 "/** The named volumes mounted below the root, sorted by path; empty when there are none. Paths under a mount's `path` read and write that volume. */",
478 );
479 defs.types.insert(
480 "Info".to_string(),
481 TypeSymbol {
482 name: "Info".to_string(),
483 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Info"),
484 declaration_span: Span::at(crate::FileId::FS),
485 kind: TypeKind::Interface { index: None,
486 generics: Default::default(),
487 methods: BTreeMap::new(),
488 properties,
489 dispatch: Dispatch::VTable,
490 doc: crate::doc(crate::FileId::FS,
491 "/** VFS configuration returned by `info()`: the root's `mode`, `access`, `volume` and `sizeLimit` (`-1` when no limit applies), plus its `mounts`. Never carries a host path. */",
492 ),
493 },
494 },
495 );
496}
497
498fn mount_info_type() -> Type {
499 Type::InterfaceRef {
500 mangled: crate::mangle::package_symbol(MODULE_NAME, "MountInfo"),
501 package: crate::Package(MODULE_NAME.to_string()),
502 name: "MountInfo".to_string(),
503 args: Vec::new(),
504 }
505}
506
507fn insert_mount_info_interface(defs: &mut PackageDeclaration) {
508 let mut properties = BTreeMap::new();
509 insert_property(
510 &mut properties,
511 "path",
512 Type::String,
513 "/** The guest path the volume is mounted at, such as `/memory`. */",
514 );
515 insert_property(
516 &mut properties,
517 "mode",
518 Type::String,
519 "/** Always `\"named\"`: mounts are named volumes the server declares. */",
520 );
521 insert_property(
522 &mut properties,
523 "volume",
524 Type::String,
525 "/** The volume's name. Its files persist across runs and sessions, and other blueprints that mount it see them. */",
526 );
527 insert_property(
528 &mut properties,
529 "access",
530 Type::String,
531 "/** `\"read_write\"`, or `\"read_only\"` when every write under `path` throws a `PermissionDeniedError`. */",
532 );
533 insert_property(
534 &mut properties,
535 "sizeLimit",
536 Type::Number,
537 "/** The most bytes the volume may hold, shared with everyone who mounts it, or `-1` when no limit applies. */",
538 );
539 defs.types.insert(
540 "MountInfo".to_string(),
541 TypeSymbol {
542 name: "MountInfo".to_string(),
543 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "MountInfo"),
544 declaration_span: Span::at(crate::FileId::FS),
545 kind: TypeKind::Interface { index: None,
546 generics: Default::default(),
547 methods: BTreeMap::new(),
548 properties,
549 dispatch: Dispatch::VTable,
550 doc: crate::doc(crate::FileId::FS,
551 "/** One volume mounted below the VFS root, as `info().mounts` lists it. Mount points cannot be removed or moved. Never carries a host path. */",
552 ),
553 },
554 },
555 );
556}
557
558fn insert_file_writer_interface(defs: &mut PackageDeclaration) {
559 let mut methods = BTreeMap::new();
560 methods.insert(
561 "close".to_string(),
562 MethodSig {
563 optional: false,
564 generics: Default::default(),
565 params: Vec::new(),
566 ret: Type::Void,
567 predicate: None,
568 doc: crate::doc(crate::FileId::FS,
569 "/** Flush the internal buffer, fsync the temp file, and atomically rename it over the destination. Subsequent method calls on this writer trap — close once. */",
570 ),
571 },
572 );
573 methods.insert(
574 "writeBytes".to_string(),
575 MethodSig {
576 optional: false,
577 generics: Default::default(),
578 params: vec![Param::new("bytes", Type::Uint8Array)],
579 ret: Type::Void,
580 predicate: None,
581 doc: crate::doc(crate::FileId::FS,
582 "/** Append `bytes` to the buffer. Crash before `close()` means nothing reaches `final_path`. */",
583 ),
584 },
585 );
586 methods.insert(
587 "writeLine".to_string(),
588 MethodSig {
589 optional: false,
590 generics: Default::default(),
591 params: vec![Param::new("line", Type::String)],
592 ret: Type::Void,
593 predicate: None,
594 doc: crate::doc(crate::FileId::FS,
595 "/** Append `line` followed by `\\n` (LF). Pair with `appendText` for CRLF-aware output. */",
596 ),
597 },
598 );
599 defs.types.insert(
600 "FileWriter".to_string(),
601 TypeSymbol {
602 name: "FileWriter".to_string(),
603 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "FileWriter"),
604 declaration_span: Span::at(crate::FileId::FS),
605 kind: TypeKind::Interface { index: None,
606 generics: Default::default(),
607 methods,
608 properties: BTreeMap::new(),
609 dispatch: Dispatch::Direct,
610 doc: crate::doc(crate::FileId::FS,
611 "/** Append-only writer returned by `writer(path)`. Internal buffer + atomic temp-file + rename on `close()`. Construct only via `writer`; user code uses `writeLine` / `writeBytes` / `close`. */",
612 ),
613 },
614 },
615 );
616}
617
618fn insert_property(
619 properties: &mut BTreeMap<String, PropertySig>,
620 name: &str,
621 ty: Type,
622 doc: &str,
623) {
624 properties.insert(
625 name.to_string(),
626 PropertySig {
627 ty,
628 readonly: true,
629 intrinsic: false,
630 optional: false,
631 doc: crate::doc(crate::FileId::FS, doc),
632 },
633 );
634}