@nahisaho/katashiro-workspace 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # @nahisaho/katashiro-workspace
2
+
3
+ KATASHIRO Workspace Management - Unified workspace interface for file operations across local, remote, and Docker environments.
4
+
5
+ ## Features
6
+
7
+ - **Unified Interface** (REQ-011-01): Single interface for file operations regardless of workspace type
8
+ - **LocalWorkspace** (REQ-011-02): Operations on local file system
9
+ - **DockerWorkspace** (REQ-011-04): Operations within Docker containers
10
+ - **Identical API** (REQ-011-05): Tools work the same regardless of workspace type
11
+ - **Fast Search** (REQ-011-06): Glob search within 1 second for up to 10,000 files
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @nahisaho/katashiro-workspace
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### LocalWorkspace
22
+
23
+ ```typescript
24
+ import { LocalWorkspace, WorkspaceFactory } from '@nahisaho/katashiro-workspace';
25
+
26
+ // Direct instantiation
27
+ const workspace = new LocalWorkspace({
28
+ type: 'local',
29
+ workingDir: '/path/to/project',
30
+ });
31
+
32
+ // Or using factory
33
+ const workspace2 = WorkspaceFactory.createLocal('/path/to/project');
34
+
35
+ // File operations
36
+ const content = await workspace.read('README.md');
37
+ await workspace.write('output.txt', 'Hello World');
38
+ const files = await workspace.search('**/*.ts');
39
+ ```
40
+
41
+ ### DockerWorkspace
42
+
43
+ ```typescript
44
+ import { DockerWorkspace, WorkspaceFactory } from '@nahisaho/katashiro-workspace';
45
+
46
+ const workspace = new DockerWorkspace({
47
+ type: 'docker',
48
+ containerId: 'container123',
49
+ workingDir: '/app',
50
+ });
51
+
52
+ // Initialize connection
53
+ await workspace.initialize();
54
+
55
+ // Same API as LocalWorkspace
56
+ const content = await workspace.read('config.json');
57
+ await workspace.write('output.txt', 'Hello Docker');
58
+
59
+ // Cleanup
60
+ await workspace.cleanup();
61
+ ```
62
+
63
+ ### Factory Pattern
64
+
65
+ ```typescript
66
+ import { WorkspaceFactory, createWorkspace } from '@nahisaho/katashiro-workspace';
67
+
68
+ // Create workspace from config
69
+ const workspace = WorkspaceFactory.create({
70
+ type: 'local', // or 'docker'
71
+ workingDir: '/path/to/project',
72
+ });
73
+
74
+ // Create and initialize in one step
75
+ const workspace2 = await createWorkspace({
76
+ type: 'docker',
77
+ containerId: 'container123',
78
+ workingDir: '/app',
79
+ });
80
+ ```
81
+
82
+ ### Unified Tool Development (REQ-011-05)
83
+
84
+ Tools can be written once and work with any workspace type:
85
+
86
+ ```typescript
87
+ import type { Workspace } from '@nahisaho/katashiro-workspace';
88
+
89
+ // Tool that works with any workspace type
90
+ async function analyzeProject(workspace: Workspace): Promise<ProjectAnalysis> {
91
+ // Read package.json
92
+ const packageJson = await workspace.read('package.json');
93
+
94
+ // Search for TypeScript files
95
+ const tsFiles = await workspace.search('**/*.ts');
96
+
97
+ // List src directory
98
+ const srcFiles = await workspace.list('src');
99
+
100
+ return {
101
+ dependencies: JSON.parse(packageJson).dependencies,
102
+ fileCount: tsFiles.length,
103
+ srcEntries: srcFiles.map(f => f.name),
104
+ };
105
+ }
106
+
107
+ // Use with LocalWorkspace
108
+ const local = WorkspaceFactory.createLocal('/local/project');
109
+ const localResult = await analyzeProject(local);
110
+
111
+ // Use with DockerWorkspace
112
+ const docker = WorkspaceFactory.createDocker('container123', '/app');
113
+ await docker.initialize();
114
+ const dockerResult = await analyzeProject(docker);
115
+ ```
116
+
117
+ ## API Reference
118
+
119
+ ### Workspace Interface
120
+
121
+ ```typescript
122
+ interface Workspace {
123
+ readonly type: 'local' | 'remote' | 'docker';
124
+ readonly workingDir: string;
125
+ readonly readOnly: boolean;
126
+
127
+ read(path: string, encoding?: BufferEncoding): Promise<string>;
128
+ readBuffer(path: string): Promise<Buffer>;
129
+ write(path: string, content: string): Promise<void>;
130
+ writeBuffer(path: string, buffer: Buffer): Promise<void>;
131
+ list(path: string): Promise<FileInfo[]>;
132
+ listEntries(path: string): Promise<DirectoryEntry[]>;
133
+ search(pattern: string): Promise<string[]>;
134
+ exists(path: string): Promise<boolean>;
135
+ delete(path: string): Promise<void>;
136
+ mkdir(path: string, recursive?: boolean): Promise<void>;
137
+ stat(path: string): Promise<FileInfo>;
138
+ copy(src: string, dest: string): Promise<void>;
139
+ move(src: string, dest: string): Promise<void>;
140
+ initialize?(): Promise<void>;
141
+ cleanup?(): Promise<void>;
142
+ }
143
+ ```
144
+
145
+ ### Error Handling
146
+
147
+ ```typescript
148
+ import { WorkspaceError } from '@nahisaho/katashiro-workspace';
149
+
150
+ try {
151
+ await workspace.read('nonexistent.txt');
152
+ } catch (error) {
153
+ if (error instanceof WorkspaceError) {
154
+ switch (error.code) {
155
+ case 'NOT_FOUND':
156
+ console.log('File not found:', error.path);
157
+ break;
158
+ case 'PERMISSION_DENIED':
159
+ console.log('Permission denied');
160
+ break;
161
+ case 'READ_ONLY':
162
+ console.log('Workspace is read-only');
163
+ break;
164
+ }
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## Requirements Coverage
170
+
171
+ | Requirement | Status | Description |
172
+ |------------|--------|-------------|
173
+ | REQ-011-01 | ✅ | Unified interface for file operations |
174
+ | REQ-011-02 | ✅ | LocalWorkspace for local file system |
175
+ | REQ-011-03 | ⏳ | RemoteWorkspace (planned) |
176
+ | REQ-011-04 | ✅ | DockerWorkspace for Docker containers |
177
+ | REQ-011-05 | ✅ | Tools work identically regardless of type |
178
+ | REQ-011-06 | ✅ | Fast glob search |
179
+
180
+ ## License
181
+
182
+ MIT
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@nahisaho/katashiro-workspace",
3
+ "version": "0.4.0",
4
+ "description": "KATASHIRO Workspace Management - Unified workspace interface for file operations",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "typecheck": "tsc --noEmit",
19
+ "clean": "rm -rf dist"
20
+ },
21
+ "dependencies": {
22
+ "@nahisaho/katashiro-core": "workspace:*",
23
+ "fast-glob": "^3.3.2",
24
+ "dockerode": "^4.0.2"
25
+ },
26
+ "devDependencies": {
27
+ "@types/dockerode": "^3.3.31",
28
+ "@types/node": "^22.10.7",
29
+ "typescript": "^5.7.3",
30
+ "vitest": "^2.1.9"
31
+ },
32
+ "keywords": [
33
+ "katashiro",
34
+ "workspace",
35
+ "file-operations",
36
+ "local-workspace",
37
+ "remote-workspace",
38
+ "docker-workspace"
39
+ ],
40
+ "author": "nahisaho",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/nahisaho/KATASHIRO.git",
45
+ "directory": "katashiro/packages/workspace"
46
+ }
47
+ }
@@ -0,0 +1,416 @@
1
+ /**
2
+ * DockerWorkspace - Dockerコンテナ内ファイルシステムワークスペース
3
+ *
4
+ * @requirement REQ-011-04
5
+ */
6
+
7
+ import Dockerode from 'dockerode';
8
+ import * as path from 'node:path';
9
+ import type {
10
+ Workspace,
11
+ DockerWorkspaceConfig,
12
+ FileInfo,
13
+ DirectoryEntry,
14
+ } from './types.js';
15
+ import { WorkspaceError, DEFAULT_DOCKER_SOCKET } from './types.js';
16
+
17
+ /**
18
+ * DockerWorkspace - Dockerコンテナ内でのファイル操作
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const workspace = new DockerWorkspace({
23
+ * type: 'docker',
24
+ * containerId: 'container123',
25
+ * workingDir: '/app',
26
+ * });
27
+ *
28
+ * await workspace.initialize();
29
+ * const content = await workspace.read('config.json');
30
+ * await workspace.write('output.txt', 'Hello Docker');
31
+ * await workspace.cleanup();
32
+ * ```
33
+ */
34
+ export class DockerWorkspace implements Workspace {
35
+ readonly type = 'docker' as const;
36
+ readonly workingDir: string;
37
+ readonly readOnly: boolean;
38
+
39
+ private docker: Dockerode;
40
+ private container: Dockerode.Container;
41
+ private containerId: string;
42
+
43
+ constructor(config: DockerWorkspaceConfig) {
44
+ this.containerId = config.containerId;
45
+ this.workingDir = config.workingDir;
46
+ this.readOnly = config.readOnly ?? false;
47
+
48
+ this.docker = new Dockerode({
49
+ socketPath: config.socketPath || DEFAULT_DOCKER_SOCKET,
50
+ });
51
+ this.container = this.docker.getContainer(this.containerId);
52
+ }
53
+
54
+ /**
55
+ * パスを解決
56
+ */
57
+ private resolvePath(filePath: string): string {
58
+ if (path.isAbsolute(filePath)) {
59
+ return filePath;
60
+ }
61
+ return path.posix.join(this.workingDir, filePath);
62
+ }
63
+
64
+ /**
65
+ * 書き込み可能か検証
66
+ */
67
+ private validateWritable(): void {
68
+ if (this.readOnly) {
69
+ throw new WorkspaceError('READ_ONLY', 'Workspace is read-only');
70
+ }
71
+ }
72
+
73
+ /**
74
+ * コンテナ内でコマンドを実行
75
+ */
76
+ private async exec(cmd: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> {
77
+ try {
78
+ const exec = await this.container.exec({
79
+ Cmd: cmd,
80
+ AttachStdout: true,
81
+ AttachStderr: true,
82
+ });
83
+
84
+ return new Promise((resolve, reject) => {
85
+ exec.start({ hijack: true, stdin: false }, (err, stream) => {
86
+ if (err) {
87
+ reject(new WorkspaceError('OPERATION_FAILED', `Exec failed: ${err.message}`));
88
+ return;
89
+ }
90
+
91
+ if (!stream) {
92
+ reject(new WorkspaceError('OPERATION_FAILED', 'No stream returned'));
93
+ return;
94
+ }
95
+
96
+ let stdout = '';
97
+ let stderr = '';
98
+
99
+ // Docker multiplexed stream handling
100
+ stream.on('data', (chunk: Buffer) => {
101
+ // Docker stream format: 8-byte header + data
102
+ // First byte: 1=stdout, 2=stderr
103
+ if (chunk.length > 8) {
104
+ const streamType = chunk[0];
105
+ const data = chunk.slice(8).toString('utf-8');
106
+ if (streamType === 1) {
107
+ stdout += data;
108
+ } else if (streamType === 2) {
109
+ stderr += data;
110
+ }
111
+ } else {
112
+ stdout += chunk.toString('utf-8');
113
+ }
114
+ });
115
+
116
+ stream.on('end', async () => {
117
+ try {
118
+ const inspectResult = await exec.inspect();
119
+ resolve({
120
+ stdout: stdout.trim(),
121
+ stderr: stderr.trim(),
122
+ exitCode: inspectResult.ExitCode ?? 0,
123
+ });
124
+ } catch {
125
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 });
126
+ }
127
+ });
128
+
129
+ stream.on('error', (error: Error) => {
130
+ reject(new WorkspaceError('OPERATION_FAILED', `Stream error: ${error.message}`));
131
+ });
132
+ });
133
+ });
134
+ } catch (error) {
135
+ throw new WorkspaceError(
136
+ 'CONNECTION_FAILED',
137
+ `Failed to execute command: ${error instanceof Error ? error.message : String(error)}`
138
+ );
139
+ }
140
+ }
141
+
142
+ async initialize(): Promise<void> {
143
+ try {
144
+ const info = await this.container.inspect();
145
+ if (!info.State.Running) {
146
+ throw new WorkspaceError(
147
+ 'CONNECTION_FAILED',
148
+ `Container ${this.containerId} is not running`
149
+ );
150
+ }
151
+ } catch (error) {
152
+ if (error instanceof WorkspaceError) throw error;
153
+ throw new WorkspaceError(
154
+ 'CONNECTION_FAILED',
155
+ `Failed to connect to container: ${error instanceof Error ? error.message : String(error)}`
156
+ );
157
+ }
158
+ }
159
+
160
+ async cleanup(): Promise<void> {
161
+ // DockerWorkspaceはステートレスなので特にクリーンアップは不要
162
+ }
163
+
164
+ async read(filePath: string, _encoding: BufferEncoding = 'utf-8'): Promise<string> {
165
+ const resolved = this.resolvePath(filePath);
166
+
167
+ const result = await this.exec(['cat', resolved]);
168
+ if (result.exitCode !== 0) {
169
+ throw this.parseError(result.stderr, filePath);
170
+ }
171
+
172
+ return result.stdout;
173
+ }
174
+
175
+ async readBuffer(filePath: string): Promise<Buffer> {
176
+ const resolved = this.resolvePath(filePath);
177
+
178
+ const result = await this.exec(['cat', resolved]);
179
+ if (result.exitCode !== 0) {
180
+ throw this.parseError(result.stderr, filePath);
181
+ }
182
+
183
+ return Buffer.from(result.stdout, 'utf-8');
184
+ }
185
+
186
+ async write(filePath: string, content: string): Promise<void> {
187
+ this.validateWritable();
188
+ const resolved = this.resolvePath(filePath);
189
+
190
+ // ディレクトリを作成
191
+ const dir = path.posix.dirname(resolved);
192
+ await this.exec(['mkdir', '-p', dir]);
193
+
194
+ // base64エンコードして書き込み(特殊文字対策)
195
+ const encoded = Buffer.from(content, 'utf-8').toString('base64');
196
+ const result = await this.exec(['sh', '-c', `echo '${encoded}' | base64 -d > '${resolved}'`]);
197
+
198
+ if (result.exitCode !== 0) {
199
+ throw this.parseError(result.stderr, filePath);
200
+ }
201
+ }
202
+
203
+ async writeBuffer(filePath: string, buffer: Buffer): Promise<void> {
204
+ this.validateWritable();
205
+ const resolved = this.resolvePath(filePath);
206
+
207
+ const dir = path.posix.dirname(resolved);
208
+ await this.exec(['mkdir', '-p', dir]);
209
+
210
+ const encoded = buffer.toString('base64');
211
+ const result = await this.exec(['sh', '-c', `echo '${encoded}' | base64 -d > '${resolved}'`]);
212
+
213
+ if (result.exitCode !== 0) {
214
+ throw this.parseError(result.stderr, filePath);
215
+ }
216
+ }
217
+
218
+ async list(dirPath: string): Promise<FileInfo[]> {
219
+ const resolved = this.resolvePath(dirPath);
220
+
221
+ const result = await this.exec([
222
+ 'sh',
223
+ '-c',
224
+ `ls -la --time-style=+%s '${resolved}' | tail -n +2`,
225
+ ]);
226
+
227
+ if (result.exitCode !== 0) {
228
+ throw this.parseError(result.stderr, dirPath);
229
+ }
230
+
231
+ return this.parseLsOutput(result.stdout, resolved);
232
+ }
233
+
234
+ async listEntries(dirPath: string): Promise<DirectoryEntry[]> {
235
+ const resolved = this.resolvePath(dirPath);
236
+
237
+ const result = await this.exec(['sh', '-c', `ls -la '${resolved}' | tail -n +2`]);
238
+
239
+ if (result.exitCode !== 0) {
240
+ throw this.parseError(result.stderr, dirPath);
241
+ }
242
+
243
+ return this.parseLsEntries(result.stdout);
244
+ }
245
+
246
+ async search(pattern: string): Promise<string[]> {
247
+ const result = await this.exec([
248
+ 'sh',
249
+ '-c',
250
+ `cd '${this.workingDir}' && find . -type f -name '${pattern}' 2>/dev/null | head -10000`,
251
+ ]);
252
+
253
+ if (result.exitCode !== 0 && result.stderr) {
254
+ throw new WorkspaceError('OPERATION_FAILED', `Search failed: ${result.stderr}`);
255
+ }
256
+
257
+ return result.stdout
258
+ .split('\n')
259
+ .filter(line => line.trim())
260
+ .map(line => line.replace(/^\.\//, ''));
261
+ }
262
+
263
+ async exists(filePath: string): Promise<boolean> {
264
+ const resolved = this.resolvePath(filePath);
265
+
266
+ const result = await this.exec(['test', '-e', resolved]);
267
+ return result.exitCode === 0;
268
+ }
269
+
270
+ async delete(filePath: string): Promise<void> {
271
+ this.validateWritable();
272
+ const resolved = this.resolvePath(filePath);
273
+
274
+ const result = await this.exec(['rm', '-rf', resolved]);
275
+ if (result.exitCode !== 0) {
276
+ throw this.parseError(result.stderr, filePath);
277
+ }
278
+ }
279
+
280
+ async mkdir(dirPath: string, recursive = true): Promise<void> {
281
+ this.validateWritable();
282
+ const resolved = this.resolvePath(dirPath);
283
+
284
+ const cmd = recursive ? ['mkdir', '-p', resolved] : ['mkdir', resolved];
285
+ const result = await this.exec(cmd);
286
+
287
+ if (result.exitCode !== 0) {
288
+ throw this.parseError(result.stderr, dirPath);
289
+ }
290
+ }
291
+
292
+ async stat(filePath: string): Promise<FileInfo> {
293
+ const resolved = this.resolvePath(filePath);
294
+
295
+ const result = await this.exec([
296
+ 'stat',
297
+ '-c',
298
+ '%n|%s|%F|%Y|%W',
299
+ resolved,
300
+ ]);
301
+
302
+ if (result.exitCode !== 0) {
303
+ throw this.parseError(result.stderr, filePath);
304
+ }
305
+
306
+ const [name, size, type, mtime, ctime] = result.stdout.split('|');
307
+ return {
308
+ name: path.posix.basename(name),
309
+ path: resolved,
310
+ size: parseInt(size, 10),
311
+ isDirectory: type === 'directory',
312
+ modifiedAt: new Date(parseInt(mtime, 10) * 1000),
313
+ createdAt: new Date(parseInt(ctime, 10) * 1000),
314
+ };
315
+ }
316
+
317
+ async copy(src: string, dest: string): Promise<void> {
318
+ this.validateWritable();
319
+ const srcResolved = this.resolvePath(src);
320
+ const destResolved = this.resolvePath(dest);
321
+
322
+ const dir = path.posix.dirname(destResolved);
323
+ await this.exec(['mkdir', '-p', dir]);
324
+
325
+ const result = await this.exec(['cp', '-r', srcResolved, destResolved]);
326
+ if (result.exitCode !== 0) {
327
+ throw this.parseError(result.stderr, src);
328
+ }
329
+ }
330
+
331
+ async move(src: string, dest: string): Promise<void> {
332
+ this.validateWritable();
333
+ const srcResolved = this.resolvePath(src);
334
+ const destResolved = this.resolvePath(dest);
335
+
336
+ const dir = path.posix.dirname(destResolved);
337
+ await this.exec(['mkdir', '-p', dir]);
338
+
339
+ const result = await this.exec(['mv', srcResolved, destResolved]);
340
+ if (result.exitCode !== 0) {
341
+ throw this.parseError(result.stderr, src);
342
+ }
343
+ }
344
+
345
+ /**
346
+ * lsの出力をパース
347
+ */
348
+ private parseLsOutput(output: string, basePath: string): FileInfo[] {
349
+ return output
350
+ .split('\n')
351
+ .filter(line => line.trim())
352
+ .map(line => {
353
+ const parts = line.split(/\s+/);
354
+ if (parts.length < 7) return null;
355
+
356
+ const permissions = parts[0];
357
+ const size = parseInt(parts[4], 10);
358
+ const timestamp = parseInt(parts[5], 10);
359
+ const name = parts.slice(6).join(' ');
360
+
361
+ return {
362
+ name,
363
+ path: path.posix.join(basePath, name),
364
+ size: isNaN(size) ? 0 : size,
365
+ isDirectory: permissions.startsWith('d'),
366
+ modifiedAt: new Date(timestamp * 1000),
367
+ createdAt: new Date(timestamp * 1000),
368
+ };
369
+ })
370
+ .filter((info): info is FileInfo => info !== null);
371
+ }
372
+
373
+ /**
374
+ * lsエントリをパース
375
+ */
376
+ private parseLsEntries(output: string): DirectoryEntry[] {
377
+ return output
378
+ .split('\n')
379
+ .filter(line => line.trim())
380
+ .map(line => {
381
+ const parts = line.split(/\s+/);
382
+ if (parts.length < 7) return null;
383
+
384
+ const permissions = parts[0];
385
+ const name = parts.slice(8).join(' ');
386
+
387
+ return {
388
+ name,
389
+ isDirectory: permissions.startsWith('d'),
390
+ };
391
+ })
392
+ .filter((entry): entry is DirectoryEntry => entry !== null);
393
+ }
394
+
395
+ /**
396
+ * エラーメッセージをパース
397
+ */
398
+ private parseError(stderr: string, filePath: string): WorkspaceError {
399
+ const msg = stderr.toLowerCase();
400
+
401
+ if (msg.includes('no such file') || msg.includes('not found')) {
402
+ return new WorkspaceError('NOT_FOUND', `Not found: ${filePath}`, filePath);
403
+ }
404
+ if (msg.includes('permission denied')) {
405
+ return new WorkspaceError('PERMISSION_DENIED', `Permission denied: ${filePath}`, filePath);
406
+ }
407
+ if (msg.includes('is a directory')) {
408
+ return new WorkspaceError('IS_DIRECTORY', `Is a directory: ${filePath}`, filePath);
409
+ }
410
+ if (msg.includes('not a directory')) {
411
+ return new WorkspaceError('NOT_DIRECTORY', `Not a directory: ${filePath}`, filePath);
412
+ }
413
+
414
+ return new WorkspaceError('OPERATION_FAILED', stderr || `Operation failed: ${filePath}`, filePath);
415
+ }
416
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @nahisaho/katashiro-workspace
3
+ *
4
+ * Workspace management module for KATASHIRO.
5
+ * Provides unified interface for file operations across local, remote, and Docker environments.
6
+ *
7
+ * @module @nahisaho/katashiro-workspace
8
+ * @requirement REQ-011
9
+ */
10
+
11
+ // Types
12
+ export type {
13
+ WorkspaceType,
14
+ FileInfo,
15
+ DirectoryEntry,
16
+ WorkspaceConfigBase,
17
+ LocalWorkspaceConfig,
18
+ RemoteWorkspaceConfig,
19
+ DockerWorkspaceConfig,
20
+ WorkspaceConfig,
21
+ Workspace,
22
+ WorkspaceErrorCode,
23
+ } from './types.js';
24
+
25
+ export {
26
+ WorkspaceError,
27
+ DEFAULT_REMOTE_TIMEOUT,
28
+ DEFAULT_DOCKER_SOCKET,
29
+ } from './types.js';
30
+
31
+ // Workspace implementations
32
+ export { LocalWorkspace } from './local-workspace.js';
33
+ export { DockerWorkspace } from './docker-workspace.js';
34
+
35
+ // Factory and utilities
36
+ export {
37
+ WorkspaceFactory,
38
+ createWorkspace,
39
+ readFile,
40
+ writeFile,
41
+ } from './workspace-factory.js';