sandlot 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +145 -518
  2. package/dist/build-emitter.d.ts +47 -0
  3. package/dist/build-emitter.d.ts.map +1 -0
  4. package/dist/builder.d.ts +370 -0
  5. package/dist/builder.d.ts.map +1 -0
  6. package/dist/bundler.d.ts +3 -3
  7. package/dist/bundler.d.ts.map +1 -1
  8. package/dist/commands/compile.d.ts +13 -0
  9. package/dist/commands/compile.d.ts.map +1 -0
  10. package/dist/commands/index.d.ts +17 -0
  11. package/dist/commands/index.d.ts.map +1 -0
  12. package/dist/commands/packages.d.ts +17 -0
  13. package/dist/commands/packages.d.ts.map +1 -0
  14. package/dist/commands/run.d.ts +40 -0
  15. package/dist/commands/run.d.ts.map +1 -0
  16. package/dist/commands/types.d.ts +141 -0
  17. package/dist/commands/types.d.ts.map +1 -0
  18. package/dist/fs.d.ts +60 -42
  19. package/dist/fs.d.ts.map +1 -1
  20. package/dist/index.d.ts +5 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +304 -491
  23. package/dist/internal.d.ts +5 -0
  24. package/dist/internal.d.ts.map +1 -1
  25. package/dist/internal.js +174 -95
  26. package/dist/runner.d.ts +314 -0
  27. package/dist/runner.d.ts.map +1 -0
  28. package/dist/sandbox-manager.d.ts +45 -33
  29. package/dist/sandbox-manager.d.ts.map +1 -1
  30. package/dist/sandbox.d.ts +144 -70
  31. package/dist/sandbox.d.ts.map +1 -1
  32. package/dist/shared-modules.d.ts +22 -3
  33. package/dist/shared-modules.d.ts.map +1 -1
  34. package/dist/shared-resources.d.ts +0 -3
  35. package/dist/shared-resources.d.ts.map +1 -1
  36. package/dist/typechecker.d.ts +1 -1
  37. package/package.json +3 -17
  38. package/src/build-emitter.ts +64 -0
  39. package/src/builder.ts +498 -0
  40. package/src/bundler.ts +86 -57
  41. package/src/commands/compile.ts +236 -0
  42. package/src/commands/index.ts +51 -0
  43. package/src/commands/packages.ts +154 -0
  44. package/src/commands/run.ts +245 -0
  45. package/src/commands/types.ts +172 -0
  46. package/src/fs.ts +90 -216
  47. package/src/index.ts +34 -12
  48. package/src/internal.ts +5 -2
  49. package/src/sandbox.ts +214 -220
  50. package/src/shared-modules.ts +74 -4
  51. package/src/shared-resources.ts +0 -3
  52. package/src/ts-libs.ts +1 -1
  53. package/src/typechecker.ts +1 -1
  54. package/dist/react.d.ts +0 -159
  55. package/dist/react.d.ts.map +0 -1
  56. package/dist/react.js +0 -149
  57. package/src/commands.ts +0 -733
  58. package/src/react.tsx +0 -331
  59. package/src/sandbox-manager.ts +0 -490
package/src/sandbox.ts CHANGED
@@ -1,92 +1,40 @@
1
- import { Bash, defineCommand } from "just-bash/browser";
2
- import { IndexedDbFs, type IndexedDbFsOptions } from "./fs";
3
- import { initBundler, type BundleResult } from "./bundler";
4
- import { createDefaultCommands, type CommandDeps } from "./commands";
1
+ import { Bash, defineCommand, type BashOptions } from "just-bash/browser";
2
+ import { Filesystem, type FilesystemOptions } from "./fs";
3
+ import { initBundler } from "./bundler";
4
+ import { createDefaultCommands, type CommandDeps, type BuildOutput, type ValidateFn } from "./commands";
5
5
  import { getDefaultResources, type SharedResources } from "./shared-resources";
6
-
7
- // Re-export for convenience
8
- export type { BundleResult } from "./bundler";
9
- export type { TypecheckResult } from "./typechecker";
10
- export type { SharedResources, TypesCache } from "./shared-resources";
11
- export type { PackageManifest, InstallResult } from "./packages";
12
- export type { RunContext, RunOptions, RunResult } from "./commands";
13
- export { installPackage, uninstallPackage, listPackages, getPackageManifest } from "./packages";
14
- export { InMemoryTypesCache } from "./shared-resources";
15
-
16
- // Loader utilities
17
- export {
18
- loadModule,
19
- loadExport,
20
- loadDefault,
21
- getExportNames,
22
- hasExport,
23
- createModuleUrl,
24
- revokeModuleUrl,
25
- ModuleLoadError,
26
- ExportNotFoundError,
27
- } from "./loader";
6
+ import { BuildEmitter } from "./build-emitter";
7
+ import { installPackage, parseImportPath } from "./packages";
28
8
 
29
9
  /**
30
- * Simple typed event emitter for build results.
31
- * Caches the last result so waitFor() can be called after build completes.
10
+ * Options that can be passed through to the just-bash Bash constructor.
11
+ * Excludes options that sandlot controls internally (fs, customCommands, files).
32
12
  */
33
- class BuildEmitter {
34
- private listeners = new Set<(result: BundleResult) => void | Promise<void>>();
35
- private lastResult: BundleResult | null = null;
36
-
37
- /**
38
- * Emit a build result to all listeners and cache it
39
- */
40
- emit = async (result: BundleResult): Promise<void> => {
41
- this.lastResult = result;
42
- const promises: Promise<void>[] = [];
43
- for (const listener of this.listeners) {
44
- const ret = listener(result);
45
- if (ret instanceof Promise) {
46
- promises.push(ret);
47
- }
48
- }
49
- await Promise.all(promises);
50
- };
51
-
52
- /**
53
- * Subscribe to build events. Returns an unsubscribe function.
54
- */
55
- on(callback: (result: BundleResult) => void | Promise<void>): () => void {
56
- this.listeners.add(callback);
57
- return () => {
58
- this.listeners.delete(callback);
59
- };
60
- }
61
-
62
- /**
63
- * Get the last build result, or wait for the next one if none exists.
64
- * Clears the cached result after returning, so subsequent calls wait for new builds.
65
- */
66
- waitFor(): Promise<BundleResult> {
67
- if (this.lastResult) {
68
- const result = this.lastResult;
69
- this.lastResult = null;
70
- return Promise.resolve(result);
71
- }
72
- return new Promise((resolve) => {
73
- const unsub = this.on((result) => {
74
- unsub();
75
- this.lastResult = null;
76
- resolve(result);
77
- });
78
- });
79
- }
80
- }
13
+ export type SandboxBashOptions = Omit<BashOptions, 'fs' | 'customCommands' | 'files'>;
81
14
 
82
15
  /**
83
16
  * Options for creating a sandbox environment
84
17
  */
85
18
  export interface SandboxOptions {
86
19
  /**
87
- * Options for the IndexedDB filesystem
20
+ * Initial files to populate the filesystem with.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const sandbox = await createSandbox({
25
+ * initialFiles: {
26
+ * '/src/index.ts': 'export const x = 1;',
27
+ * '/tsconfig.json': JSON.stringify({ compilerOptions: { strict: true } }),
28
+ * },
29
+ * });
30
+ * ```
31
+ */
32
+ initialFiles?: Record<string, string>;
33
+
34
+ /**
35
+ * Maximum filesystem size in bytes (default: 50MB)
88
36
  */
89
- fsOptions?: IndexedDbFsOptions;
37
+ maxFilesystemSize?: number;
90
38
 
91
39
  /**
92
40
  * Path to tsconfig.json in the virtual filesystem.
@@ -105,10 +53,12 @@ export interface SandboxOptions {
105
53
 
106
54
  /**
107
55
  * Callback invoked when a build succeeds.
108
- * Receives the bundle result with the compiled code.
109
- * Use this to dynamically import the bundle or halt the agent.
56
+ * Receives the build output with the bundle and loaded module.
57
+ *
58
+ * For agent workflows, prefer using `createBuilder()` which handles
59
+ * build capture automatically.
110
60
  */
111
- onBuild?: (result: BundleResult) => void | Promise<void>;
61
+ onBuild?: (result: BuildOutput) => void | Promise<void>;
112
62
 
113
63
  /**
114
64
  * Additional custom commands to add to the bash environment
@@ -119,29 +69,64 @@ export interface SandboxOptions {
119
69
  * Module IDs that should be resolved from the host's SharedModuleRegistry
120
70
  * instead of esm.sh CDN. The host must have registered these modules
121
71
  * using `registerSharedModules()` before loading dynamic code.
122
- *
72
+ *
123
73
  * This solves the "multiple React instances" problem by allowing dynamic
124
74
  * components to share the same React instance as the host application.
125
- *
75
+ *
76
+ * Type definitions are automatically installed for these modules so that
77
+ * TypeScript can typecheck code that imports them.
78
+ *
126
79
  * @example
127
80
  * ```ts
128
81
  * // Host setup
129
82
  * import * as React from 'react';
130
83
  * import * as ReactDOM from 'react-dom/client';
131
84
  * import { registerSharedModules } from 'sandlot';
132
- *
85
+ *
133
86
  * registerSharedModules({
134
87
  * 'react': React,
135
88
  * 'react-dom/client': ReactDOM,
136
89
  * });
137
- *
138
- * // Create sandbox with shared modules
139
- * const sandbox = await createInMemorySandbox({
90
+ *
91
+ * // Create sandbox with shared modules (types auto-installed)
92
+ * const sandbox = await createSandbox({
140
93
  * sharedModules: ['react', 'react-dom/client'],
141
94
  * });
142
95
  * ```
143
96
  */
144
97
  sharedModules?: string[];
98
+
99
+ /**
100
+ * Options passed through to the just-bash Bash constructor.
101
+ * Use this to configure environment variables, execution limits,
102
+ * network access, logging, and other bash-level settings.
103
+ *
104
+ * Note: `fs`, `customCommands`, and `files` are controlled by sandlot
105
+ * and cannot be overridden here.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const sandbox = await createSandbox({
110
+ * bashOptions: {
111
+ * cwd: '/src',
112
+ * env: { NODE_ENV: 'development' },
113
+ * executionLimits: { maxCommandCount: 1000 },
114
+ * },
115
+ * });
116
+ * ```
117
+ */
118
+ bashOptions?: SandboxBashOptions;
119
+ }
120
+
121
+ /**
122
+ * Sandbox state that can be serialized for persistence.
123
+ */
124
+ export interface SandboxState {
125
+ /**
126
+ * All files in the filesystem as path -> content mapping.
127
+ * Can be passed as `initialFiles` when creating a new sandbox.
128
+ */
129
+ files: Record<string, string>;
145
130
  }
146
131
 
147
132
  /**
@@ -149,9 +134,9 @@ export interface SandboxOptions {
149
134
  */
150
135
  export interface Sandbox {
151
136
  /**
152
- * The virtual filesystem (IndexedDB-backed)
137
+ * The virtual filesystem
153
138
  */
154
- fs: IndexedDbFs;
139
+ fs: Filesystem;
155
140
 
156
141
  /**
157
142
  * The just-bash shell environment
@@ -159,55 +144,94 @@ export interface Sandbox {
159
144
  bash: Bash;
160
145
 
161
146
  /**
162
- * Check if there are unsaved changes in the filesystem.
163
- *
147
+ * The last successful build output, or null if no build has succeeded yet.
148
+ *
149
+ * This is updated automatically whenever a `build` command succeeds.
150
+ * Contains both the bundle and the loaded (and validated, if applicable) module.
151
+ *
164
152
  * @example
165
153
  * ```ts
166
- * await sandbox.bash.exec('echo "hello" > /test.txt');
167
- * console.log(sandbox.isDirty()); // true
168
- * await sandbox.save();
169
- * console.log(sandbox.isDirty()); // false
154
+ * // Agent loop pattern
155
+ * while (!sandbox.lastBuild) {
156
+ * const response = await agent.step();
157
+ * await sandbox.bash.exec(response.command);
158
+ * }
159
+ * // Build succeeded, sandbox.lastBuild contains bundle + module
160
+ * const App = sandbox.lastBuild.module.App;
170
161
  * ```
171
162
  */
172
- isDirty(): boolean;
163
+ lastBuild: BuildOutput | null;
173
164
 
174
165
  /**
175
- * Save the filesystem to IndexedDB.
166
+ * Get the current sandbox state for persistence.
167
+ *
168
+ * Returns a serializable object containing all files that can be
169
+ * JSON-serialized and used to restore the sandbox later.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * // Save sandbox state
174
+ * const state = sandbox.getState();
175
+ * localStorage.setItem('my-project', JSON.stringify(state));
176
176
  *
177
- * @returns true if saved, false if nothing to save or in-memory only
177
+ * // Later, restore the sandbox
178
+ * const saved = JSON.parse(localStorage.getItem('my-project'));
179
+ * const sandbox2 = await createSandbox({ initialFiles: saved.files });
180
+ * ```
178
181
  */
179
- save(): Promise<boolean>;
182
+ getState(): SandboxState;
180
183
 
181
184
  /**
182
- * Close all resources (filesystem, etc.)
185
+ * Subscribe to build events. Called whenever a build succeeds.
186
+ * Returns an unsubscribe function.
187
+ *
188
+ * For agent workflows, prefer using `createBuilder()` which handles
189
+ * build capture automatically. Use `onBuild` directly when you need
190
+ * more control over the subscription lifecycle.
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * let lastBuild: BuildOutput | null = null;
195
+ * const unsubscribe = sandbox.onBuild((result) => {
196
+ * lastBuild = result;
197
+ * });
198
+ *
199
+ * await sandbox.bash.exec('build /src/index.ts');
200
+ * unsubscribe();
201
+ *
202
+ * if (lastBuild) {
203
+ * const App = lastBuild.module.App as React.ComponentType;
204
+ * }
205
+ * ```
183
206
  */
184
- close(): void;
207
+ onBuild(callback: (result: BuildOutput) => void | Promise<void>): () => void;
185
208
 
186
209
  /**
187
- * Subscribe to build events. Called whenever a build succeeds.
188
- * Returns an unsubscribe function.
189
- *
190
- * Use this to capture the bundle result when build succeeds.
191
- * The callback receives the BundleResult with the compiled code.
192
- *
210
+ * Set a validation function for the build command.
211
+ *
212
+ * When set, the build command will run this function after loading
213
+ * the module. If validation throws, the build fails and the agent
214
+ * sees the error. If validation passes, the validated module is
215
+ * available in the build output.
216
+ *
193
217
  * @example
194
218
  * ```ts
195
- * let bundle: BundleResult | null = null;
196
- *
197
- * const sandbox = await createInMemorySandbox({
198
- * onBuild: (result) => {
199
- * bundle = result;
200
- * console.log('Built:', result.code.length, 'bytes');
201
- * },
219
+ * sandbox.setValidation((mod) => {
220
+ * if (!mod.App) throw new Error("Must export App component");
221
+ * return { App: mod.App as React.ComponentType };
202
222
  * });
203
- *
204
- * // ... agent writes files and calls build ...
205
- *
206
- * // After successful build, bundle is available
207
- * console.log(bundle?.code);
223
+ *
224
+ * // Now build will fail if App is missing
225
+ * await sandbox.bash.exec('build /src/index.ts');
208
226
  * ```
209
227
  */
210
- onBuild(callback: (result: BundleResult) => void | Promise<void>): () => void;
228
+ setValidation(fn: ValidateFn): void;
229
+
230
+ /**
231
+ * Clear the validation function.
232
+ * After calling this, builds will not perform validation.
233
+ */
234
+ clearValidation(): void;
211
235
  }
212
236
 
213
237
  /**
@@ -217,9 +241,12 @@ export interface Sandbox {
217
241
  * The sandbox provides a just-bash shell with custom commands:
218
242
  * - `tsc [entry]` - Type check the project
219
243
  * - `build [entry] [options]` - Build the project (runs typecheck first)
244
+ * - `install <pkg>` - Install npm packages
245
+ * - `uninstall <pkg>` - Remove packages
246
+ * - `list` - List installed packages
247
+ * - `run <entry>` - Run a script
220
248
  *
221
249
  * Build options:
222
- * - `--output, -o <path>` - Output path (default: /dist/bundle.js)
223
250
  * - `--format, -f <esm|iife|cjs>` - Output format (default: esm)
224
251
  * - `--minify, -m` - Enable minification
225
252
  * - `--skip-typecheck, -s` - Skip type checking
@@ -229,18 +256,14 @@ export interface Sandbox {
229
256
  * let bundleResult: BundleResult | null = null;
230
257
  *
231
258
  * const sandbox = await createSandbox({
232
- * fsOptions: {
233
- * dbName: "my-project",
234
- * initialFiles: {
235
- * "/src/index.ts": "export const hello = 'world';",
236
- * "/tsconfig.json": JSON.stringify({
237
- * compilerOptions: { target: "ES2020", strict: true }
238
- * }),
239
- * },
259
+ * initialFiles: {
260
+ * '/src/index.ts': 'export const hello = "world";',
261
+ * '/tsconfig.json': JSON.stringify({
262
+ * compilerOptions: { target: 'ES2020', strict: true }
263
+ * }),
240
264
  * },
241
265
  * onBuild: (result) => {
242
266
  * bundleResult = result;
243
- * // Could also: dynamically import, halt agent, etc.
244
267
  * },
245
268
  * });
246
269
  *
@@ -248,137 +271,100 @@ export interface Sandbox {
248
271
  * await sandbox.bash.exec('echo "console.log(1);" > /src/index.ts');
249
272
  *
250
273
  * // Type check
251
- * const tscResult = await sandbox.bash.exec("tsc");
274
+ * const tscResult = await sandbox.bash.exec('tsc /src/index.ts');
252
275
  * console.log(tscResult.stdout);
253
276
  *
254
277
  * // Build (includes typecheck, triggers onBuild callback)
255
- * const buildResult = await sandbox.bash.exec("build");
278
+ * const buildResult = await sandbox.bash.exec('build /src/index.ts');
256
279
  * console.log(buildResult.stdout);
257
280
  * console.log(bundleResult?.code); // The compiled bundle
258
281
  *
259
- * // Save to IndexedDB
260
- * await sandbox.save();
261
- *
262
- * // Clean up
263
- * sandbox.close();
282
+ * // Save state for later
283
+ * const state = sandbox.getState();
284
+ * localStorage.setItem('my-project', JSON.stringify(state));
264
285
  * ```
265
286
  */
266
287
  export async function createSandbox(options: SandboxOptions = {}): Promise<Sandbox> {
267
288
  const {
268
- fsOptions = {},
289
+ initialFiles,
290
+ maxFilesystemSize,
269
291
  tsconfigPath = "/tsconfig.json",
270
292
  resources: providedResources,
271
293
  onBuild: onBuildCallback,
272
294
  customCommands = [],
273
295
  sharedModules,
296
+ bashOptions = {},
274
297
  } = options;
275
298
 
276
- // Start parallel initialization tasks
277
- const fsPromise = IndexedDbFs.create(fsOptions);
299
+ // Create filesystem (synchronous)
300
+ const fs = Filesystem.create({
301
+ initialFiles,
302
+ maxSizeBytes: maxFilesystemSize,
303
+ });
278
304
 
279
- // Determine which resources to use:
280
- // 1. Provided resources (preferred)
281
- // 2. Default resources singleton
305
+ // Load shared resources and bundler in parallel
282
306
  const resourcesPromise = providedResources
283
307
  ? Promise.resolve(providedResources)
284
308
  : getDefaultResources();
285
309
 
286
310
  const bundlerPromise = initBundler();
287
311
 
288
- // Wait for all initialization
289
- const [fs, resources] = await Promise.all([fsPromise, resourcesPromise, bundlerPromise]);
312
+ // Wait for async initialization
313
+ const [resources] = await Promise.all([resourcesPromise, bundlerPromise]);
290
314
 
291
315
  // Extract lib files and types cache from resources
292
316
  const libFiles = resources.libFiles;
293
317
  const typesCache = resources.typesCache;
294
318
 
295
- // Create build event emitter
296
- const buildEmitter = new BuildEmitter();
319
+ // Auto-install types for shared modules so TypeScript can typecheck them
320
+ // Only install base packages, not subpath exports (e.g., "react" not "react/jsx-runtime")
321
+ // Subpath types are fetched automatically when the base package is installed
322
+ if (sharedModules && sharedModules.length > 0) {
323
+ // Extract unique base package names
324
+ const basePackages = new Set<string>();
325
+ for (const moduleId of sharedModules) {
326
+ const { packageName } = parseImportPath(moduleId);
327
+ basePackages.add(packageName);
328
+ }
297
329
 
298
- // If a callback was provided in options, subscribe it
299
- if (onBuildCallback) {
300
- buildEmitter.on(onBuildCallback);
330
+ await Promise.all(
331
+ Array.from(basePackages).map(async (packageName) => {
332
+ try {
333
+ // Install the package to get its type definitions
334
+ // The runtime will use the shared module, but we need types for typechecking
335
+ await installPackage(fs, packageName, { cache: typesCache });
336
+ } catch (err) {
337
+ // Log but don't fail - module might not have types available
338
+ console.warn(`[sandlot] Failed to install types for shared module "${packageName}":`, err);
339
+ }
340
+ })
341
+ );
301
342
  }
302
343
 
303
- // Create commands using the extracted factories
304
- // Commands emit to the build emitter
305
- const commandDeps: CommandDeps = {
306
- fs,
307
- libFiles,
308
- tsconfigPath,
309
- onBuild: buildEmitter.emit,
310
- typesCache,
311
- sharedModules,
312
- };
313
- const defaultCommands = createDefaultCommands(commandDeps);
314
-
315
- // Create bash environment with the custom filesystem
316
- const bash = new Bash({
317
- fs,
318
- cwd: "/",
319
- customCommands: [...defaultCommands, ...customCommands],
320
- });
321
-
322
- return {
323
- fs,
324
- bash,
325
- isDirty: () => fs.isDirty(),
326
- save: () => fs.save(),
327
- close: () => fs.close(),
328
- onBuild: (callback) => buildEmitter.on(callback),
329
- };
330
- }
331
-
332
- /**
333
- * Create an in-memory sandbox (no IndexedDB persistence).
334
- * Useful for testing or temporary workspaces.
335
- */
336
- export async function createInMemorySandbox(
337
- options: Omit<SandboxOptions, "fsOptions"> & {
338
- initialFiles?: Record<string, string>;
339
- } = {}
340
- ): Promise<Sandbox> {
341
- const {
342
- initialFiles,
343
- tsconfigPath = "/tsconfig.json",
344
- resources: providedResources,
345
- onBuild: onBuildCallback,
346
- customCommands = [],
347
- sharedModules,
348
- } = options;
349
-
350
- // Determine which resources to use
351
- const resourcesPromise = providedResources
352
- ? Promise.resolve(providedResources)
353
- : getDefaultResources();
354
-
355
- const bundlerPromise = initBundler();
356
-
357
- // Create in-memory filesystem synchronously
358
- const fs = IndexedDbFs.createInMemory({ initialFiles });
359
-
360
- // Wait for resources and bundler
361
- const [resources] = await Promise.all([resourcesPromise, bundlerPromise]);
362
-
363
- // Extract lib files and types cache from resources
364
- const libFiles = resources.libFiles;
365
- const typesCache = resources.typesCache;
366
-
367
344
  // Create build event emitter
368
345
  const buildEmitter = new BuildEmitter();
369
346
 
347
+ // Track the last successful build
348
+ let lastBuild: BuildOutput | null = null;
349
+ buildEmitter.on((result) => {
350
+ lastBuild = result;
351
+ });
352
+
370
353
  // If a callback was provided in options, subscribe it
371
354
  if (onBuildCallback) {
372
355
  buildEmitter.on(onBuildCallback);
373
356
  }
374
357
 
375
- // Create commands using the extracted factories
376
- // Commands emit to the build emitter
358
+ // Validation function (can be set/cleared dynamically)
359
+ let validationFn: ValidateFn | null = null;
360
+
361
+ // Create commands
377
362
  const commandDeps: CommandDeps = {
378
363
  fs,
379
364
  libFiles,
380
365
  tsconfigPath,
381
366
  onBuild: buildEmitter.emit,
367
+ getValidation: () => validationFn,
382
368
  typesCache,
383
369
  sharedModules,
384
370
  };
@@ -386,17 +372,25 @@ export async function createInMemorySandbox(
386
372
 
387
373
  // Create bash environment with the custom filesystem
388
374
  const bash = new Bash({
375
+ ...bashOptions,
389
376
  fs,
390
- cwd: "/",
391
377
  customCommands: [...defaultCommands, ...customCommands],
392
378
  });
393
379
 
394
380
  return {
395
381
  fs,
396
382
  bash,
397
- isDirty: () => fs.isDirty(),
398
- save: () => Promise.resolve(false), // No persistence for in-memory
399
- close: () => { }, // No resources to close
383
+ get lastBuild() {
384
+ return lastBuild;
385
+ },
386
+ getState: () => ({ files: fs.getFiles() }),
400
387
  onBuild: (callback) => buildEmitter.on(callback),
388
+ setValidation: (fn: ValidateFn) => {
389
+ validationFn = fn;
390
+ },
391
+ clearValidation: () => {
392
+ validationFn = null;
393
+ },
401
394
  };
402
395
  }
396
+