Skip to content

Commit da85b1e

Browse files
mishushakovclaude
andauthored
feat(sdk): add includeEntry option to filesystem watch (#1385)
Client-side counterpart to [e2b-dev/infra#2930](e2b-dev/infra#2930): adds an `includeEntry`/`include_entry` option to filesystem directory watching across the JS and Python (sync + async) SDKs, so each `FilesystemEvent` can carry the affected entry's `EntryInfo` (best-effort — unset for remove/rename-away events where the path no longer exists). This regenerates the filesystem proto code from the updated spec, threads the flag through `watchDir`/`watch_dir` (streaming `WatchDir` and polling `CreateWatcher`), maps the new `entry` field onto the event, and extracts a shared entry-mapping helper reused by `list`/`getInfo`/`rename`. The option degrades gracefully: older sandboxes (< envd 0.6.2) ignore it and leave `entry` unset, so there's no hard version gate. Includes new watch tests for all three SDKs and a minor-bump changeset for `e2b` and `@e2b/python-sdk`. > Note: the entry-info tests require envd 0.6.2 (shipped by the infra PR), so this should land with/after that deploy. ### Usage **JavaScript** ```ts const handle = await sandbox.files.watchDir( 'my-dir', (event) => { console.log(event.type, event.name, event.entry?.path, event.entry?.type) }, { includeEntry: true } ) ``` **Python (async)** ```python def on_event(e): print(e.type, e.name, e.entry.path if e.entry else None) handle = await sandbox.files.watch_dir("my-dir", on_event=on_event, include_entry=True) ``` **Python (sync)** ```python handle = sandbox.files.watch_dir("my-dir", include_entry=True) for e in handle.get_new_events(): print(e.type, e.name, e.entry.path if e.entry else None) ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 961ffba commit da85b1e

18 files changed

Lines changed: 343 additions & 199 deletions

File tree

.changeset/watch-include-entry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@e2b/python-sdk": minor
3+
"e2b": minor
4+
---
5+
6+
Add an `includeEntry`/`include_entry` option to filesystem directory watching. When enabled, each `FilesystemEvent` carries the affected entry's `EntryInfo` (best-effort; left unset for events where the path no longer exists, such as remove/rename-away). Requires envd 0.6.3 or later; watching with this option against an older sandbox raises a template error.

packages/js-sdk/src/envd/filesystem/filesystem_pb.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type { Message } from '@bufbuild/protobuf'
2424
export const file_filesystem_filesystem: GenFile =
2525
/*@__PURE__*/
2626
fileDesc(
27-
'ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8i5QIKCUVudHJ5SW5mbxIMCgRuYW1lGAEgASgJEiIKBHR5cGUYAiABKA4yFC5maWxlc3lzdGVtLkZpbGVUeXBlEgwKBHBhdGgYAyABKAkSDAoEc2l6ZRgEIAEoAxIMCgRtb2RlGAUgASgNEhMKC3Blcm1pc3Npb25zGAYgASgJEg0KBW93bmVyGAcgASgJEg0KBWdyb3VwGAggASgJEjEKDW1vZGlmaWVkX3RpbWUYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhsKDnN5bWxpbmtfdGFyZ2V0GAogASgJSACIAQESNQoIbWV0YWRhdGEYCyADKAsyIy5maWxlc3lzdGVtLkVudHJ5SW5mby5NZXRhZGF0YUVudHJ5Gi8KDU1ldGFkYXRhRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIRCg9fc3ltbGlua190YXJnZXQiLQoOTGlzdERpclJlcXVlc3QSDAoEcGF0aBgBIAEoCRINCgVkZXB0aBgCIAEoDSI5Cg9MaXN0RGlyUmVzcG9uc2USJgoHZW50cmllcxgBIAMoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIjIKD1dhdGNoRGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJEhEKCXJlY3Vyc2l2ZRgCIAEoCCJECg9GaWxlc3lzdGVtRXZlbnQSDAoEbmFtZRgBIAEoCRIjCgR0eXBlGAIgASgOMhUuZmlsZXN5c3RlbS5FdmVudFR5cGUi4AEKEFdhdGNoRGlyUmVzcG9uc2USOAoFc3RhcnQYASABKAsyJy5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UuU3RhcnRFdmVudEgAEjEKCmZpbGVzeXN0ZW0YAiABKAsyGy5maWxlc3lzdGVtLkZpbGVzeXN0ZW1FdmVudEgAEjsKCWtlZXBhbGl2ZRgDIAEoCzImLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXNwb25zZS5LZWVwQWxpdmVIABoMCgpTdGFydEV2ZW50GgsKCUtlZXBBbGl2ZUIHCgVldmVudCI3ChRDcmVhdGVXYXRjaGVyUmVxdWVzdBIMCgRwYXRoGAEgASgJEhEKCXJlY3Vyc2l2ZRgCIAEoCCIrChVDcmVhdGVXYXRjaGVyUmVzcG9uc2USEgoKd2F0Y2hlcl9pZBgBIAEoCSItChdHZXRXYXRjaGVyRXZlbnRzUmVxdWVzdBISCgp3YXRjaGVyX2lkGAEgASgJIkcKGEdldFdhdGNoZXJFdmVudHNSZXNwb25zZRIrCgZldmVudHMYASADKAsyGy5maWxlc3lzdGVtLkZpbGVzeXN0ZW1FdmVudCIqChRSZW1vdmVXYXRjaGVyUmVxdWVzdBISCgp3YXRjaGVyX2lkGAEgASgJIhcKFVJlbW92ZVdhdGNoZXJSZXNwb25zZSpSCghGaWxlVHlwZRIZChVGSUxFX1RZUEVfVU5TUEVDSUZJRUQQABISCg5GSUxFX1RZUEVfRklMRRABEhcKE0ZJTEVfVFlQRV9ESVJFQ1RPUlkQAiqYAQoJRXZlbnRUeXBlEhoKFkVWRU5UX1RZUEVfVU5TUEVDSUZJRUQQABIVChFFVkVOVF9UWVBFX0NSRUFURRABEhQKEEVWRU5UX1RZUEVfV1JJVEUQAhIVChFFVkVOVF9UWVBFX1JFTU9WRRADEhUKEUVWRU5UX1RZUEVfUkVOQU1FEAQSFAoQRVZFTlRfVFlQRV9DSE1PRBAFMp8FCgpGaWxlc3lzdGVtEjkKBFN0YXQSFy5maWxlc3lzdGVtLlN0YXRSZXF1ZXN0GhguZmlsZXN5c3RlbS5TdGF0UmVzcG9uc2USQgoHTWFrZURpchIaLmZpbGVzeXN0ZW0uTWFrZURpclJlcXVlc3QaGy5maWxlc3lzdGVtLk1ha2VEaXJSZXNwb25zZRI5CgRNb3ZlEhcuZmlsZXN5c3RlbS5Nb3ZlUmVxdWVzdBoYLmZpbGVzeXN0ZW0uTW92ZVJlc3BvbnNlEkIKB0xpc3REaXISGi5maWxlc3lzdGVtLkxpc3REaXJSZXF1ZXN0GhsuZmlsZXN5c3RlbS5MaXN0RGlyUmVzcG9uc2USPwoGUmVtb3ZlEhkuZmlsZXN5c3RlbS5SZW1vdmVSZXF1ZXN0GhouZmlsZXN5c3RlbS5SZW1vdmVSZXNwb25zZRJHCghXYXRjaERpchIbLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXF1ZXN0GhwuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlMAESVAoNQ3JlYXRlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLkNyZWF0ZVdhdGNoZXJSZXNwb25zZRJdChBHZXRXYXRjaGVyRXZlbnRzEiMuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVxdWVzdBokLmZpbGVzeXN0ZW0uR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlElQKDVJlbW92ZVdhdGNoZXISIC5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXF1ZXN0GiEuZmlsZXN5c3RlbS5SZW1vdmVXYXRjaGVyUmVzcG9uc2VCaQoOY29tLmZpbGVzeXN0ZW1CD0ZpbGVzeXN0ZW1Qcm90b1ABogIDRlhYqgIKRmlsZXN5c3RlbcoCCkZpbGVzeXN0ZW3iAhZGaWxlc3lzdGVtXEdQQk1ldGFkYXRh6gIKRmlsZXN5c3RlbWIGcHJvdG8z',
27+
'ChtmaWxlc3lzdGVtL2ZpbGVzeXN0ZW0ucHJvdG8SCmZpbGVzeXN0ZW0iMgoLTW92ZVJlcXVlc3QSDgoGc291cmNlGAEgASgJEhMKC2Rlc3RpbmF0aW9uGAIgASgJIjQKDE1vdmVSZXNwb25zZRIkCgVlbnRyeRgBIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIh4KDk1ha2VEaXJSZXF1ZXN0EgwKBHBhdGgYASABKAkiNwoPTWFrZURpclJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8iHQoNUmVtb3ZlUmVxdWVzdBIMCgRwYXRoGAEgASgJIhAKDlJlbW92ZVJlc3BvbnNlIhsKC1N0YXRSZXF1ZXN0EgwKBHBhdGgYASABKAkiNAoMU3RhdFJlc3BvbnNlEiQKBWVudHJ5GAEgASgLMhUuZmlsZXN5c3RlbS5FbnRyeUluZm8i5QIKCUVudHJ5SW5mbxIMCgRuYW1lGAEgASgJEiIKBHR5cGUYAiABKA4yFC5maWxlc3lzdGVtLkZpbGVUeXBlEgwKBHBhdGgYAyABKAkSDAoEc2l6ZRgEIAEoAxIMCgRtb2RlGAUgASgNEhMKC3Blcm1pc3Npb25zGAYgASgJEg0KBW93bmVyGAcgASgJEg0KBWdyb3VwGAggASgJEjEKDW1vZGlmaWVkX3RpbWUYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEhsKDnN5bWxpbmtfdGFyZ2V0GAogASgJSACIAQESNQoIbWV0YWRhdGEYCyADKAsyIy5maWxlc3lzdGVtLkVudHJ5SW5mby5NZXRhZGF0YUVudHJ5Gi8KDU1ldGFkYXRhRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4AUIRCg9fc3ltbGlua190YXJnZXQiLQoOTGlzdERpclJlcXVlc3QSDAoEcGF0aBgBIAEoCRINCgVkZXB0aBgCIAEoDSI5Cg9MaXN0RGlyUmVzcG9uc2USJgoHZW50cmllcxgBIAMoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvIkkKD1dhdGNoRGlyUmVxdWVzdBIMCgRwYXRoGAEgASgJEhEKCXJlY3Vyc2l2ZRgCIAEoCBIVCg1pbmNsdWRlX2VudHJ5GAMgASgIInkKD0ZpbGVzeXN0ZW1FdmVudBIMCgRuYW1lGAEgASgJEiMKBHR5cGUYAiABKA4yFS5maWxlc3lzdGVtLkV2ZW50VHlwZRIpCgVlbnRyeRgDIAEoCzIVLmZpbGVzeXN0ZW0uRW50cnlJbmZvSACIAQFCCAoGX2VudHJ5IuABChBXYXRjaERpclJlc3BvbnNlEjgKBXN0YXJ0GAEgASgLMicuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlLlN0YXJ0RXZlbnRIABIxCgpmaWxlc3lzdGVtGAIgASgLMhsuZmlsZXN5c3RlbS5GaWxlc3lzdGVtRXZlbnRIABI7CglrZWVwYWxpdmUYAyABKAsyJi5maWxlc3lzdGVtLldhdGNoRGlyUmVzcG9uc2UuS2VlcEFsaXZlSAAaDAoKU3RhcnRFdmVudBoLCglLZWVwQWxpdmVCBwoFZXZlbnQiTgoUQ3JlYXRlV2F0Y2hlclJlcXVlc3QSDAoEcGF0aBgBIAEoCRIRCglyZWN1cnNpdmUYAiABKAgSFQoNaW5jbHVkZV9lbnRyeRgDIAEoCCIrChVDcmVhdGVXYXRjaGVyUmVzcG9uc2USEgoKd2F0Y2hlcl9pZBgBIAEoCSItChdHZXRXYXRjaGVyRXZlbnRzUmVxdWVzdBISCgp3YXRjaGVyX2lkGAEgASgJIkcKGEdldFdhdGNoZXJFdmVudHNSZXNwb25zZRIrCgZldmVudHMYASADKAsyGy5maWxlc3lzdGVtLkZpbGVzeXN0ZW1FdmVudCIqChRSZW1vdmVXYXRjaGVyUmVxdWVzdBISCgp3YXRjaGVyX2lkGAEgASgJIhcKFVJlbW92ZVdhdGNoZXJSZXNwb25zZSpSCghGaWxlVHlwZRIZChVGSUxFX1RZUEVfVU5TUEVDSUZJRUQQABISCg5GSUxFX1RZUEVfRklMRRABEhcKE0ZJTEVfVFlQRV9ESVJFQ1RPUlkQAiqYAQoJRXZlbnRUeXBlEhoKFkVWRU5UX1RZUEVfVU5TUEVDSUZJRUQQABIVChFFVkVOVF9UWVBFX0NSRUFURRABEhQKEEVWRU5UX1RZUEVfV1JJVEUQAhIVChFFVkVOVF9UWVBFX1JFTU9WRRADEhUKEUVWRU5UX1RZUEVfUkVOQU1FEAQSFAoQRVZFTlRfVFlQRV9DSE1PRBAFMp8FCgpGaWxlc3lzdGVtEjkKBFN0YXQSFy5maWxlc3lzdGVtLlN0YXRSZXF1ZXN0GhguZmlsZXN5c3RlbS5TdGF0UmVzcG9uc2USQgoHTWFrZURpchIaLmZpbGVzeXN0ZW0uTWFrZURpclJlcXVlc3QaGy5maWxlc3lzdGVtLk1ha2VEaXJSZXNwb25zZRI5CgRNb3ZlEhcuZmlsZXN5c3RlbS5Nb3ZlUmVxdWVzdBoYLmZpbGVzeXN0ZW0uTW92ZVJlc3BvbnNlEkIKB0xpc3REaXISGi5maWxlc3lzdGVtLkxpc3REaXJSZXF1ZXN0GhsuZmlsZXN5c3RlbS5MaXN0RGlyUmVzcG9uc2USPwoGUmVtb3ZlEhkuZmlsZXN5c3RlbS5SZW1vdmVSZXF1ZXN0GhouZmlsZXN5c3RlbS5SZW1vdmVSZXNwb25zZRJHCghXYXRjaERpchIbLmZpbGVzeXN0ZW0uV2F0Y2hEaXJSZXF1ZXN0GhwuZmlsZXN5c3RlbS5XYXRjaERpclJlc3BvbnNlMAESVAoNQ3JlYXRlV2F0Y2hlchIgLmZpbGVzeXN0ZW0uQ3JlYXRlV2F0Y2hlclJlcXVlc3QaIS5maWxlc3lzdGVtLkNyZWF0ZVdhdGNoZXJSZXNwb25zZRJdChBHZXRXYXRjaGVyRXZlbnRzEiMuZmlsZXN5c3RlbS5HZXRXYXRjaGVyRXZlbnRzUmVxdWVzdBokLmZpbGVzeXN0ZW0uR2V0V2F0Y2hlckV2ZW50c1Jlc3BvbnNlElQKDVJlbW92ZVdhdGNoZXISIC5maWxlc3lzdGVtLlJlbW92ZVdhdGNoZXJSZXF1ZXN0GiEuZmlsZXN5c3RlbS5SZW1vdmVXYXRjaGVyUmVzcG9uc2VCaQoOY29tLmZpbGVzeXN0ZW1CD0ZpbGVzeXN0ZW1Qcm90b1ABogIDRlhYqgIKRmlsZXN5c3RlbcoCCkZpbGVzeXN0ZW3iAhZGaWxlc3lzdGVtXEdQQk1ldGFkYXRh6gIKRmlsZXN5c3RlbWIGcHJvdG8z',
2828
[file_google_protobuf_timestamp]
2929
)
3030

@@ -300,6 +300,13 @@ export type WatchDirRequest = Message<'filesystem.WatchDirRequest'> & {
300300
* @generated from field: bool recursive = 2;
301301
*/
302302
recursive: boolean
303+
304+
/**
305+
* If true, each FilesystemEvent includes the EntryInfo of the affected entry, when available.
306+
*
307+
* @generated from field: bool include_entry = 3;
308+
*/
309+
includeEntry: boolean
303310
}
304311

305312
/**
@@ -323,6 +330,15 @@ export type FilesystemEvent = Message<'filesystem.FilesystemEvent'> & {
323330
* @generated from field: filesystem.EventType type = 2;
324331
*/
325332
type: EventType
333+
334+
/**
335+
* Info of the entry that triggered the event. Only populated when include_entry
336+
* was requested and the entry could be stat-ed (e.g. not set for remove/rename-away
337+
* events, where the entry no longer exists at this path).
338+
*
339+
* @generated from field: optional filesystem.EntryInfo entry = 3;
340+
*/
341+
entry?: EntryInfo
326342
}
327343

328344
/**
@@ -415,6 +431,13 @@ export type CreateWatcherRequest =
415431
* @generated from field: bool recursive = 2;
416432
*/
417433
recursive: boolean
434+
435+
/**
436+
* If true, each FilesystemEvent includes the EntryInfo of the affected entry, when available.
437+
*
438+
* @generated from field: bool include_entry = 3;
439+
*/
440+
includeEntry: boolean
418441
}
419442

420443
/**

packages/js-sdk/src/envd/versions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export const ENVD_DEFAULT_USER = '0.4.0'
55
export const ENVD_ENVD_CLOSE = '0.5.2'
66
export const ENVD_OCTET_STREAM_UPLOAD = '0.5.7'
77
export const ENVD_FILE_METADATA = '0.6.2'
8+
export const ENVD_VERSION_FS_EVENT_ENTRY_INFO = '0.6.3'

packages/js-sdk/src/sandbox/filesystem/index.ts

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { authenticationHeader, handleRpcError } from '../../envd/rpc'
2020

2121
import { EnvdApiClient } from '../../envd/api'
2222
import {
23+
EntryInfo as FsEntryInfo,
2324
Filesystem as FilesystemService,
2425
FileType as FsFileType,
2526
} from '../../envd/filesystem/filesystem_pb'
@@ -32,6 +33,7 @@ import {
3233
ENVD_DEFAULT_USER,
3334
ENVD_FILE_METADATA,
3435
ENVD_OCTET_STREAM_UPLOAD,
36+
ENVD_VERSION_FS_EVENT_ENTRY_INFO,
3537
ENVD_VERSION_RECURSIVE_WATCH,
3638
} from '../../envd/versions'
3739
import {
@@ -207,6 +209,25 @@ function metadataHeaders(
207209
return headers
208210
}
209211

212+
/**
213+
* Map a protobuf `EntryInfo` to the SDK `EntryInfo`.
214+
*/
215+
export function mapEntryInfo(entry: FsEntryInfo): EntryInfo {
216+
return {
217+
name: entry.name,
218+
type: mapFileType(entry.type),
219+
path: entry.path,
220+
size: Number(entry.size),
221+
mode: entry.mode,
222+
permissions: entry.permissions,
223+
owner: entry.owner,
224+
group: entry.group,
225+
modifiedTime: mapModifiedTime(entry.modifiedTime),
226+
symlinkTarget: entry.symlinkTarget,
227+
metadata: mapMetadata(entry.metadata),
228+
}
229+
}
230+
210231
/**
211232
* Options for the sandbox filesystem operations.
212233
*/
@@ -280,6 +301,16 @@ export interface WatchOpts extends FilesystemRequestOpts {
280301
* Watch the directory recursively
281302
*/
282303
recursive?: boolean
304+
/**
305+
* Include the {@link EntryInfo} of the affected entry in each {@link FilesystemEvent}.
306+
*
307+
* The entry is populated best-effort and may be `undefined` for events where the
308+
* entry no longer exists at the path (e.g. remove or rename-away events).
309+
*
310+
* Requires envd 0.6.3 or later. Watching with this option against an older sandbox
311+
* throws a `TemplateError`.
312+
*/
313+
includeEntry?: boolean
283314
}
284315

285316
/**
@@ -662,23 +693,12 @@ export class Filesystem {
662693
const entries: EntryInfo[] = []
663694

664695
for (const e of res.entries) {
665-
const type = mapFileType(e.type)
666-
667-
if (type) {
668-
entries.push({
669-
name: e.name,
670-
type,
671-
path: e.path,
672-
size: Number(e.size),
673-
mode: e.mode,
674-
permissions: e.permissions,
675-
owner: e.owner,
676-
group: e.group,
677-
modifiedTime: mapModifiedTime(e.modifiedTime),
678-
symlinkTarget: e.symlinkTarget,
679-
metadata: mapMetadata(e.metadata),
680-
})
696+
// Skip entries with an unknown file type.
697+
if (!mapFileType(e.type)) {
698+
continue
681699
}
700+
701+
entries.push(mapEntryInfo(e))
682702
}
683703

684704
return entries
@@ -754,19 +774,7 @@ export class Filesystem {
754774
throw new Error('Expected to receive information about moved object')
755775
}
756776

757-
return {
758-
name: entry.name,
759-
type: mapFileType(entry.type),
760-
path: entry.path,
761-
size: Number(entry.size),
762-
mode: entry.mode,
763-
permissions: entry.permissions,
764-
owner: entry.owner,
765-
group: entry.group,
766-
modifiedTime: mapModifiedTime(entry.modifiedTime),
767-
symlinkTarget: entry.symlinkTarget,
768-
metadata: mapMetadata(entry.metadata),
769-
}
777+
return mapEntryInfo(entry)
770778
} catch (err) {
771779
throw handleFilesystemRpcError(err)
772780
}
@@ -858,19 +866,7 @@ export class Filesystem {
858866
)
859867
}
860868

861-
return {
862-
name: res.entry.name,
863-
type: mapFileType(res.entry.type),
864-
path: res.entry.path,
865-
size: Number(res.entry.size),
866-
mode: res.entry.mode,
867-
permissions: res.entry.permissions,
868-
owner: res.entry.owner,
869-
group: res.entry.group,
870-
modifiedTime: mapModifiedTime(res.entry.modifiedTime),
871-
symlinkTarget: res.entry.symlinkTarget,
872-
metadata: mapMetadata(res.entry.metadata),
873-
}
869+
return mapEntryInfo(res.entry)
874870
} catch (err) {
875871
throw handleFilesystemRpcError(err)
876872
}
@@ -902,6 +898,17 @@ export class Filesystem {
902898
)
903899
}
904900

901+
if (
902+
opts?.includeEntry &&
903+
this.envdApi.version &&
904+
compareVersions(this.envdApi.version, ENVD_VERSION_FS_EVENT_ENTRY_INFO) <
905+
0
906+
) {
907+
throw new TemplateError(
908+
'You need to update the template to include entry info in watch events.'
909+
)
910+
}
911+
905912
const requestTimeoutMs =
906913
opts?.requestTimeoutMs ?? this.connectionConfig.requestTimeoutMs
907914

@@ -914,6 +921,7 @@ export class Filesystem {
914921
{
915922
path,
916923
recursive: opts?.recursive ?? this.defaultWatchRecursive,
924+
includeEntry: opts?.includeEntry ?? false,
917925
},
918926
{
919927
headers: {

packages/js-sdk/src/sandbox/filesystem/watchHandle.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
EventType,
44
WatchDirResponse,
55
} from '../../envd/filesystem/filesystem_pb'
6+
import { EntryInfo, mapEntryInfo } from './index'
67

78
/**
89
* Sandbox filesystem event types.
@@ -57,6 +58,14 @@ export interface FilesystemEvent {
5758
* Filesystem operation event type.
5859
*/
5960
type: FilesystemEventType
61+
/**
62+
* Information about the entry that triggered the event.
63+
*
64+
* Only populated when the watch was started with `includeEntry: true` and the
65+
* sandbox's envd version supports it. It may be `undefined` for events where the
66+
* entry no longer exists at the path (e.g. remove or rename-away events).
67+
*/
68+
entry?: EntryInfo
6069
}
6170

6271
/**
@@ -106,6 +115,9 @@ export class WatchHandle {
106115
this.onEvent?.({
107116
name: event.value.name,
108117
type: eventType,
118+
entry: event.value.entry
119+
? mapEntryInfo(event.value.entry)
120+
: undefined,
109121
})
110122
}
111123
this.onExit?.()

packages/js-sdk/tests/sandbox/files/watch.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { expect, onTestFinished } from 'vitest'
33
import { isDebug, sandboxTest } from '../../setup.js'
44
import {
55
FileNotFoundError,
6+
FilesystemEvent,
67
FilesystemEventType,
8+
FileType,
79
SandboxError,
810
} from '../../../src'
911

@@ -138,6 +140,43 @@ sandboxTest(
138140
}
139141
)
140142

143+
sandboxTest('watch directory changes with entry info', async ({ sandbox }) => {
144+
const dirname = 'test_watch_dir_entry'
145+
const filename = 'test_watch.txt'
146+
const content = 'This file will be watched.'
147+
const newContent = 'This file has been modified.'
148+
149+
await sandbox.files.makeDir(dirname)
150+
await sandbox.files.write(`${dirname}/${filename}`, content)
151+
152+
let resolveEvent: (event: FilesystemEvent) => void
153+
const eventPromise = new Promise<FilesystemEvent>((resolve) => {
154+
resolveEvent = resolve
155+
})
156+
157+
const handle = await sandbox.files.watchDir(
158+
dirname,
159+
async (event) => {
160+
if (event.type === FilesystemEventType.WRITE && event.name === filename) {
161+
resolveEvent(event)
162+
}
163+
},
164+
{ includeEntry: true }
165+
)
166+
167+
await sandbox.files.write(`${dirname}/${filename}`, newContent)
168+
169+
const event = await eventPromise
170+
171+
// The entry is populated best-effort for events where the path still exists.
172+
expect(event.entry).toBeDefined()
173+
expect(event.entry?.name).toBe(filename)
174+
expect(event.entry?.path).toBe(`/home/user/${dirname}/${filename}`)
175+
expect(event.entry?.type).toBe(FileType.FILE)
176+
177+
await handle.stop()
178+
})
179+
141180
sandboxTest('watch non-existing directory', async ({ sandbox }) => {
142181
const dirname = 'non_existing_watch_dir'
143182

0 commit comments

Comments
 (0)