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 let bytes_or_undefined = Type::union(vec![Type::Uint8Array, Type::Undefined]);
18 let string_or_undefined = Type::union(vec![Type::String, Type::Undefined]);
19
20 insert_stat_interface(&mut defs);
21 insert_peek_interface(&mut defs);
22 insert_file_writer_interface(&mut defs);
23 insert_dir_entry_interface(&mut defs);
24 insert_mount_info_interface(&mut defs);
25 insert_info_interface(&mut defs);
26
27 insert_fn(
28 &mut defs,
29 "cwd",
30 Vec::new(),
31 Type::String,
32 "/** The absolute guest working directory used by relative paths. Shared by the program and packages; no capability required. Defaults to `/`. */",
33 );
34 insert_fn(
35 &mut defs,
36 "maxReadSize",
37 Vec::new(),
38 Type::Number,
39 "/**\n * Maximum number of bytes `read` / `readText` will load whole; past this they return `undefined`. 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 */",
40 );
41 insert_fn(
42 &mut defs,
43 "info",
44 Vec::new(),
45 Type::InterfaceRef {
46 mangled: crate::mangle::package_symbol(MODULE_NAME, "Info"),
47 package: crate::Package(MODULE_NAME.to_string()),
48 name: "Info".to_string(),
49 args: Vec::new(),
50 },
51 "/**\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 */",
52 );
53 insert_fn(
54 &mut defs,
55 "exists",
56 vec![path_param.clone()],
57 Type::Boolean,
58 "/**\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 */",
59 );
60 insert_fn(
61 &mut defs,
62 "summary",
63 vec![path_param.clone()],
64 Type::String,
65 "/**\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, false)` to visit all of them.\n * @param path File or directory path under the VFS root.\n * @capability fs.list { path, recursive }\n */",
66 );
67 insert_fn(
68 &mut defs,
69 "isEmpty",
70 vec![path_param.clone()],
71 Type::Boolean,
72 "/**\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 */",
73 );
74 insert_fn(
75 &mut defs,
76 "size",
77 vec![path_param.clone()],
78 Type::Number,
79 "/**\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 */",
80 );
81 insert_fn(
82 &mut defs,
83 "stat",
84 vec![path_param.clone()],
85 Type::Union(vec![
86 Type::InterfaceRef {
87 mangled: crate::mangle::package_symbol(MODULE_NAME, "Stat"),
88 package: crate::Package(MODULE_NAME.to_string()),
89 name: "Stat".to_string(),
90 args: Vec::new(),
91 },
92 Type::Undefined,
93 ]),
94 "/**\n * Metadata about an entry under the VFS root. Returns `undefined` 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 */",
95 );
96 insert_fn(
97 &mut defs,
98 "peek",
99 vec![path_param.clone()],
100 Type::InterfaceRef {
101 mangled: crate::mangle::package_symbol(MODULE_NAME, "Peek"),
102 package: crate::Package(MODULE_NAME.to_string()),
103 name: "Peek".to_string(),
104 args: Vec::new(),
105 },
106 "/**\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 */",
107 );
108 insert_fn(
109 &mut defs,
110 "read",
111 vec![path_param.clone()],
112 bytes_or_undefined.clone(),
113 "/**\n * Read the whole file as a `Uint8Array`. Returns `undefined` when the file is larger than `maxReadSize()` — the typechecker forces narrowing before use. Traps on missing paths or directories.\n * @param path File path under the VFS root.\n * @capability fs.read { path }\n */",
114 );
115 insert_fn(
116 &mut defs,
117 "readText",
118 vec![path_param.clone()],
119 string_or_undefined.clone(),
120 "/**\n * Read the whole file as a UTF-8 string. Returns `undefined` 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 */",
121 );
122 insert_fn(
123 &mut defs,
124 "write",
125 vec![path_param.clone(), Param::new("content", Type::Uint8Array)],
126 Type::Void,
127 "/**\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 */",
128 );
129 insert_fn(
130 &mut defs,
131 "writeText",
132 vec![path_param.clone(), Param::new("content", Type::String)],
133 Type::Void,
134 "/**\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 */",
135 );
136 insert_fn(
137 &mut defs,
138 "append",
139 vec![path_param.clone(), Param::new("content", Type::Uint8Array)],
140 Type::Void,
141 "/**\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 */",
142 );
143 insert_fn(
144 &mut defs,
145 "appendText",
146 vec![path_param.clone(), Param::new("content", Type::String)],
147 Type::Void,
148 "/**\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 */",
149 );
150 insert_fn(
151 &mut defs,
152 "readBytes",
153 vec![
154 path_param.clone(),
155 Param::new("offset", Type::Number),
156 Param::new("length", Type::Number),
157 ],
158 Type::Uint8Array,
159 "/**\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 */",
160 );
161 insert_fn(
162 &mut defs,
163 "mkdir",
164 vec![path_param.clone(), Param::new("recursive", Type::Boolean)],
165 Type::Void,
166 "/**\n * Create the directory at `path`. `recursive=true` creates intermediate directories (like `mkdir -p`); `recursive=false` throws when the parent is missing or the target already exists. Until options objects land, the flag is positional.\n * @param path Directory path under the VFS root.\n * @param recursive Whether to create missing parents.\n * @capability fs.mkdir { path, recursive }\n */",
167 );
168 insert_fn(
169 &mut defs,
170 "remove",
171 vec![path_param.clone(), Param::new("recursive", Type::Boolean)],
172 Type::Void,
173 "/**\n * Remove the entry at `path`. Files unconditionally; directories require `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 recursive Whether to remove directories recursively.\n * @capability fs.remove { path, recursive }\n */",
174 );
175 insert_fn(
176 &mut defs,
177 "move",
178 vec![
179 Param::new("from", Type::String),
180 Param::new("to", Type::String),
181 ],
182 Type::Void,
183 "/**\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 */",
184 );
185 insert_fn(
186 &mut defs,
187 "copy",
188 vec![
189 Param::new("from", Type::String),
190 Param::new("to", Type::String),
191 Param::new("recursive", Type::Boolean),
192 ],
193 Type::Void,
194 "/**\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 */",
195 );
196 insert_fn(
197 &mut defs,
198 "writer",
199 vec![path_param.clone()],
200 Type::InterfaceRef {
201 mangled: crate::mangle::package_symbol(MODULE_NAME, "FileWriter"),
202 package: crate::Package(MODULE_NAME.to_string()),
203 name: "FileWriter".to_string(),
204 args: Vec::new(),
205 },
206 "/**\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 */",
207 );
208 insert_fn(
209 &mut defs,
210 "lines",
211 vec![path_param.clone()],
212 Type::InterfaceRef {
213 mangled: crate::mangle::prelude("Iterator"),
214 package: crate::Package::prelude(),
215 name: "Iterator".to_string(),
216 args: vec![Type::String],
217 },
218 "/**\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 */",
219 );
220 insert_fn(
221 &mut defs,
222 "bytes",
223 vec![path_param.clone(), Param::new("chunkSize", Type::Number)],
224 Type::InterfaceRef {
225 mangled: crate::mangle::prelude("Iterator"),
226 package: crate::Package::prelude(),
227 name: "Iterator".to_string(),
228 args: vec![Type::Uint8Array],
229 },
230 "/**\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 */",
231 );
232 insert_fn(
233 &mut defs,
234 "list",
235 vec![path_param.clone(), Param::new("recursive", Type::Boolean)],
236 Type::InterfaceRef {
237 mangled: crate::mangle::prelude("Iterator"),
238 package: crate::Package::prelude(),
239 name: "Iterator".to_string(),
240 args: vec![Type::InterfaceRef {
241 mangled: crate::mangle::package_symbol(MODULE_NAME, "DirEntry"),
242 package: crate::Package(MODULE_NAME.to_string()),
243 name: "DirEntry".to_string(),
244 args: Vec::new(),
245 }],
246 },
247 "/**\n * List the children of the directory at `path` as a constant-memory iterator of `DirEntry`. `recursive=true` walks descendants as well; `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 recursive Whether to walk descendants recursively.\n * @capability fs.list { path, recursive }\n */",
248 );
249
250 defs
251}
252
253fn insert_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>, ret: Type, doc: &str) {
254 defs.values.insert(
255 name.to_string(),
256 ValueSymbol {
257 name: name.to_string(),
258 mangled_name: crate::mangle::package_symbol(MODULE_NAME, name),
259 declaration_span: Span::at(crate::FileId::FS),
260 kind: ValueKind::Function {
261 generics: Vec::new(),
262 params,
263 ret,
264 type_predicate: None,
265 doc: crate::doc(crate::FileId::FS, doc),
266 },
267 },
268 );
269}
270
271fn insert_stat_interface(defs: &mut PackageDeclaration) {
272 let mut properties = BTreeMap::new();
273 insert_property(
274 &mut properties,
275 "kind",
276 Type::String,
277 "/** One of `\"file\"` / `\"directory\"` / `\"symlink\"` / `\"other\"`. Symlinks are reported as `\"symlink\"` without dereferencing. */",
278 );
279 insert_property(
280 &mut properties,
281 "size",
282 Type::Number,
283 "/** File size in bytes. `0` for non-files (directories, symlinks, other). */",
284 );
285 insert_property(
286 &mut properties,
287 "modifiedAt",
288 Type::Number,
289 "/** Last-modified timestamp in milliseconds since the Unix epoch. `0` when the underlying filesystem doesn't report one. */",
290 );
291 defs.types.insert(
292 "Stat".to_string(),
293 TypeSymbol {
294 name: "Stat".to_string(),
295 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Stat"),
296 declaration_span: Span::at(crate::FileId::FS),
297 kind: TypeKind::Interface { index: None,
298 generics: Vec::new(),
299 methods: BTreeMap::new(),
300 properties,
301 dispatch: Dispatch::Direct,
302 doc: crate::doc(crate::FileId::FS,
303 "/** Filesystem metadata returned by `stat(path)`. Constructed only by `stat`; user code reads `kind` / `size` / `modifiedAt` via property access. */",
304 ),
305 },
306 },
307 );
308}
309
310fn insert_peek_interface(defs: &mut PackageDeclaration) {
311 let mut properties = BTreeMap::new();
312 insert_property(
313 &mut properties,
314 "preview",
315 Type::String,
316 "/** First ~256 bytes of the file decoded as UTF-8 lossy. Use `lines` / `read` for full content. */",
317 );
318 insert_property(
319 &mut properties,
320 "encoding",
321 Type::String,
322 "/** 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`. */",
323 );
324 insert_property(
325 &mut properties,
326 "lineEnding",
327 Type::String,
328 "/** `\"crlf\"` if a CRLF sequence appears in the preview; `\"lf\"` otherwise (including files with no line terminators yet). */",
329 );
330 insert_property(
331 &mut properties,
332 "size",
333 Type::Number,
334 "/** Total file size in bytes — same value as `size(path)`. */",
335 );
336 defs.types.insert(
337 "Peek".to_string(),
338 TypeSymbol {
339 name: "Peek".to_string(),
340 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Peek"),
341 declaration_span: Span::at(crate::FileId::FS),
342 kind: TypeKind::Interface { index: None,
343 generics: Vec::new(),
344 methods: BTreeMap::new(),
345 properties,
346 dispatch: Dispatch::Direct,
347 doc: crate::doc(crate::FileId::FS,
348 "/** Lightweight file-only inspection — preview + transport metadata. Constructed only by `peek`; user code reads fields via property access. */",
349 ),
350 },
351 },
352 );
353}
354
355fn insert_dir_entry_interface(defs: &mut PackageDeclaration) {
356 let mut properties = BTreeMap::new();
357 insert_property(
358 &mut properties,
359 "kind",
360 Type::String,
361 "/** `\"file\"` / `\"directory\"` / `\"symlink\"` / `\"other\"`. */",
362 );
363 insert_property(
364 &mut properties,
365 "name",
366 Type::String,
367 "/** Basename of the entry (no parent path). */",
368 );
369 insert_property(
370 &mut properties,
371 "path",
372 Type::String,
373 "/** Full guest-visible path under the VFS root, with a leading `/`. */",
374 );
375 insert_property(
376 &mut properties,
377 "size",
378 Type::Number,
379 "/** File size in bytes; `0` for non-files. */",
380 );
381 defs.types.insert(
382 "DirEntry".to_string(),
383 TypeSymbol {
384 name: "DirEntry".to_string(),
385 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "DirEntry"),
386 declaration_span: Span::at(crate::FileId::FS),
387 kind: TypeKind::Interface { index: None,
388 generics: Vec::new(),
389 methods: BTreeMap::new(),
390 properties,
391 dispatch: Dispatch::Direct,
392 doc: crate::doc(crate::FileId::FS,
393 "/** Directory entry yielded by `list(path, recursive)`. Properties: `kind` / `name` / `path` / `size`. Constructed only by the iterator. */",
394 ),
395 },
396 },
397 );
398}
399
400fn insert_info_interface(defs: &mut PackageDeclaration) {
401 let mut properties = BTreeMap::new();
402 insert_property(
403 &mut properties,
404 "mode",
405 Type::String,
406 "/** The root's mode: `\"none\"` / `\"ephemeral\"` / `\"per_session\"` / `\"named\"`. */",
407 );
408 insert_property(
409 &mut properties,
410 "sizeLimit",
411 Type::Number,
412 "/** 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. */",
413 );
414 insert_property(
415 &mut properties,
416 "access",
417 Type::String,
418 "/** `\"read_write\"`, or `\"read_only\"` when every write to the root throws a `PermissionDeniedError`. */",
419 );
420 insert_property(
421 &mut properties,
422 "volume",
423 Type::String,
424 "/** The named volume backing the root under `mode: \"named\"`; `\"\"` otherwise, and for a directory `submilli run --vfs` exposes. */",
425 );
426 insert_property(
427 &mut properties,
428 "mounts",
429 Type::Array(Box::new(mount_info_type())),
430 "/** 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. */",
431 );
432 defs.types.insert(
433 "Info".to_string(),
434 TypeSymbol {
435 name: "Info".to_string(),
436 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "Info"),
437 declaration_span: Span::at(crate::FileId::FS),
438 kind: TypeKind::Interface { index: None,
439 generics: Vec::new(),
440 methods: BTreeMap::new(),
441 properties,
442 dispatch: Dispatch::Direct,
443 doc: crate::doc(crate::FileId::FS,
444 "/** 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. */",
445 ),
446 },
447 },
448 );
449}
450
451fn mount_info_type() -> Type {
452 Type::InterfaceRef {
453 mangled: crate::mangle::package_symbol(MODULE_NAME, "MountInfo"),
454 package: crate::Package(MODULE_NAME.to_string()),
455 name: "MountInfo".to_string(),
456 args: Vec::new(),
457 }
458}
459
460fn insert_mount_info_interface(defs: &mut PackageDeclaration) {
461 let mut properties = BTreeMap::new();
462 insert_property(
463 &mut properties,
464 "path",
465 Type::String,
466 "/** The guest path the volume is mounted at, such as `/memory`. */",
467 );
468 insert_property(
469 &mut properties,
470 "mode",
471 Type::String,
472 "/** Always `\"named\"`: mounts are named volumes the server declares. */",
473 );
474 insert_property(
475 &mut properties,
476 "volume",
477 Type::String,
478 "/** The volume's name. Its files persist across runs and sessions, and other blueprints that mount it see them. */",
479 );
480 insert_property(
481 &mut properties,
482 "access",
483 Type::String,
484 "/** `\"read_write\"`, or `\"read_only\"` when every write under `path` throws a `PermissionDeniedError`. */",
485 );
486 insert_property(
487 &mut properties,
488 "sizeLimit",
489 Type::Number,
490 "/** The most bytes the volume may hold, shared with everyone who mounts it, or `-1` when no limit applies. */",
491 );
492 defs.types.insert(
493 "MountInfo".to_string(),
494 TypeSymbol {
495 name: "MountInfo".to_string(),
496 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "MountInfo"),
497 declaration_span: Span::at(crate::FileId::FS),
498 kind: TypeKind::Interface { index: None,
499 generics: Vec::new(),
500 methods: BTreeMap::new(),
501 properties,
502 dispatch: Dispatch::Direct,
503 doc: crate::doc(crate::FileId::FS,
504 "/** One volume mounted below the VFS root, as `info().mounts` lists it. Mount points cannot be removed or moved. Never carries a host path. */",
505 ),
506 },
507 },
508 );
509}
510
511fn insert_file_writer_interface(defs: &mut PackageDeclaration) {
512 let mut methods = BTreeMap::new();
513 methods.insert(
514 "close".to_string(),
515 MethodSig {
516 optional: false,
517 generics: Vec::new(),
518 params: Vec::new(),
519 ret: Type::Void,
520 predicate: None,
521 doc: crate::doc(crate::FileId::FS,
522 "/** Flush the internal buffer, fsync the temp file, and atomically rename it over the destination. Subsequent method calls on this writer trap — close once. */",
523 ),
524 },
525 );
526 methods.insert(
527 "writeBytes".to_string(),
528 MethodSig {
529 optional: false,
530 generics: Vec::new(),
531 params: vec![Param::new("bytes", Type::Uint8Array)],
532 ret: Type::Void,
533 predicate: None,
534 doc: crate::doc(crate::FileId::FS,
535 "/** Append `bytes` to the buffer. Crash before `close()` means nothing reaches `final_path`. */",
536 ),
537 },
538 );
539 methods.insert(
540 "writeLine".to_string(),
541 MethodSig {
542 optional: false,
543 generics: Vec::new(),
544 params: vec![Param::new("line", Type::String)],
545 ret: Type::Void,
546 predicate: None,
547 doc: crate::doc(crate::FileId::FS,
548 "/** Append `line` followed by `\\n` (LF). Pair with `appendText` for CRLF-aware output. */",
549 ),
550 },
551 );
552 defs.types.insert(
553 "FileWriter".to_string(),
554 TypeSymbol {
555 name: "FileWriter".to_string(),
556 mangled_name: crate::mangle::package_symbol(MODULE_NAME, "FileWriter"),
557 declaration_span: Span::at(crate::FileId::FS),
558 kind: TypeKind::Interface { index: None,
559 generics: Vec::new(),
560 methods,
561 properties: BTreeMap::new(),
562 dispatch: Dispatch::Direct,
563 doc: crate::doc(crate::FileId::FS,
564 "/** 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`. */",
565 ),
566 },
567 },
568 );
569}
570
571fn insert_property(
572 properties: &mut BTreeMap<String, PropertySig>,
573 name: &str,
574 ty: Type,
575 doc: &str,
576) {
577 properties.insert(
578 name.to_string(),
579 PropertySig {
580 ty,
581 readonly: true,
582 intrinsic: false,
583 optional: false,
584 doc: crate::doc(crate::FileId::FS, doc),
585 },
586 );
587}