@cmdoss/file-manager 0.2.2 → 0.2.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.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # File Manager
2
+
3
+ A cross-platform file system abstraction for the CommandOSS ts-sdks, providing unified file operations in both Node.js and browser environments using [zenfs](https://github.com/zen-fs/core).
4
+
5
+ ## Features
6
+
7
+ - 🌐 **Universal**: Works in Node.js and browser environments seamlessly
8
+ - 📁 **Virtual FileSystem**: ZenFS-based in-memory file system
9
+ - 🔒 **Type-Safe**: Full TypeScript support with comprehensive interfaces
10
+ - 🚀 **Efficient**: Lazy-loaded, memory-efficient file operations
11
+ - 🎯 **Abstracted**: Simple interface for common file operations
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install @cmdoss/file-manager
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ import { ZenFsFileManager } from '@cmdoss/file-manager'
23
+
24
+ const fileManager = new ZenFsFileManager()
25
+
26
+ // Read a file
27
+ const content = await fileManager.readFile('/path/to/file.txt')
28
+
29
+ // Write a file
30
+ await fileManager.writeFile('/path/to/output.txt', 'content')
31
+
32
+ // List directory contents
33
+ const files = await fileManager.listDirectory('/path/to/dir')
34
+
35
+ // Check if file exists
36
+ const exists = await fileManager.fileExists('/path/to/file.txt')
37
+
38
+ // Get file metadata
39
+ const stat = await fileManager.stat('/path/to/file.txt')
40
+ ```
41
+
42
+ ## API
43
+
44
+ ### IFileManager Interface
45
+
46
+ All operations are defined by the `IFileManager` interface:
47
+
48
+ - `readFile(path: string): Promise<Buffer>` - Read file contents
49
+ - `writeFile(path: string, content: Buffer | string): Promise<void>` - Write file contents
50
+ - `listDirectory(path: string): Promise<string[]>` - List directory contents
51
+ - `fileExists(path: string): Promise<boolean>` - Check if file exists
52
+ - `stat(path: string): Promise<Stats>` - Get file metadata
53
+ - `mkdir(path: string): Promise<void>` - Create directory
54
+ - `rm(path: string): Promise<void>` - Delete file or directory
55
+ - `getWorkspacePath(): string` - Get current workspace directory
56
+
57
+ ## Workspace Concept
58
+
59
+ The file manager tracks a workspace directory boundary within the virtual filesystem. This is useful for:
60
+
61
+ - Organizing site resources in a contained directory
62
+ - Computing relative paths for deployment
63
+ - Isolating operations to a specific project folder
64
+
65
+ ```typescript
66
+ const fileManager = new ZenFsFileManager('/workspace')
67
+
68
+ // Operations are relative to workspace
69
+ await fileManager.writeFile('index.html', '<html>...</html>')
70
+ // Actually written to /workspace/index.html
71
+ ```
72
+
73
+ ## Architecture
74
+
75
+ - **ZenFsFileManager**: Concrete implementation using zenfs virtual filesystem
76
+ - **IFileManager**: Interface for cross-platform file operations
77
+ - Supports both Node.js `fs` module and browser File API backends
78
+
79
+ ## Usage with Site Builder
80
+
81
+ The file manager is typically used with the site-builder SDK:
82
+
83
+ ```typescript
84
+ import { WalrusSiteBuilderSdk } from '@cmdoss/site-builder'
85
+ import { ZenFsFileManager } from '@cmdoss/file-manager'
86
+
87
+ const fileManager = new ZenFsFileManager('/my-site')
88
+ const sdk = new WalrusSiteBuilderSdk({
89
+ fileManager,
90
+ // ... other options
91
+ })
92
+
93
+ // SDK uses file manager for reading/writing site resources
94
+ await sdk.publishSite()
95
+ ```
96
+
97
+ ## Browser vs Node.js
98
+
99
+ The file manager automatically adapts to the environment:
100
+
101
+ - **Node.js**: Uses native `fs` module for actual file operations
102
+ - **Browser**: Uses in-memory virtual filesystem with zenfs
103
+
104
+ This allows the same code to work in both environments without modification.
105
+
106
+ ## Contributing
107
+
108
+ 1. Implement the `IFileManager` interface for new backends
109
+ 2. Add tests for cross-platform compatibility
110
+ 3. Ensure all operations work in both Node.js and browser
111
+ 4. Submit a pull request
112
+
113
+ ## License
114
+
115
+ MIT
116
+
117
+ ## See Also
118
+
119
+ - [Site Builder SDK](../site-builder/) - Main SDK using this file manager
120
+ - [zenfs](https://github.com/zen-fs/core) - Virtual filesystem library
121
+ - [AGENTS.md](../../AGENTS.md) - Detailed project architecture
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmdoss/file-manager",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": "./dist/index.js",
@@ -9,6 +9,9 @@
9
9
  "access": "public",
10
10
  "tag": "latest"
11
11
  },
12
+ "files": [
13
+ "dist"
14
+ ],
12
15
  "peerDependencies": {
13
16
  "@zenfs/archives": "^1.3.1",
14
17
  "@zenfs/core": "^2.4.2",
@@ -20,15 +23,17 @@
20
23
  "@zenfs/core": "^2.4.4",
21
24
  "@zenfs/dom": "^1.2.5",
22
25
  "debug": "^4.4.3",
23
- "@cmdoss/site-builder": "0.2.2"
26
+ "@cmdoss/site-builder": "0.2.3"
24
27
  },
25
28
  "devDependencies": {
26
29
  "@types/node": "^22.19.1",
27
- "tsdown": "^0.16.8"
30
+ "tsdown": "^0.17.3"
28
31
  },
29
32
  "scripts": {
30
33
  "dev": "tsdown --platform browser --watch --ignore-watch .turbo --no-clean",
31
34
  "test": "node --test",
35
+ "test:coverage": "node --test --experimental-test-coverage --test-coverage-exclude='src/**/*.test.ts'",
36
+ "test:watch": "node --test --watch",
32
37
  "check:types": "tsc --noEmit",
33
38
  "build": "tsdown --platform browser --minify"
34
39
  }
@@ -1,14 +0,0 @@
1
-
2
- > @cmdoss/file-manager@0.2.2 build /home/runner/work/ts-sdks/ts-sdks/packages/file-manager
3
- > tsdown --platform browser --minify
4
-
5
- ℹ tsdown v0.16.8 powered by rolldown v1.0.0-beta.52
6
- ℹ entry: src/index.ts
7
- ℹ tsconfig: tsconfig.json
8
- ℹ Build start
9
- ℹ dist/index.js 3.01 kB │ gzip: 1.24 kB
10
- ℹ dist/index.js.map 8.42 kB │ gzip: 2.79 kB
11
- ℹ dist/index.d.ts.map 0.87 kB │ gzip: 0.37 kB
12
- ℹ dist/index.d.ts 1.06 kB │ gzip: 0.50 kB
13
- ℹ 4 files, total: 13.36 kB
14
- ✔ Build complete in 268ms
package/dist/index.d.ts DELETED
@@ -1,24 +0,0 @@
1
- import { FileChangedCallback, IFileManager } from "@cmdoss/site-builder";
2
-
3
- //#region src/file-manager.d.ts
4
- declare class ZenFsFileManager implements IFileManager {
5
- /** The directory of the workspace. Any files within this directory are considered part of the workspace. */
6
- readonly workspaceDir: string;
7
- /** The directory where the workspace is mounted in the virtual filesystem. */
8
- readonly mountDir?: string | undefined;
9
- protected changeListeners: Set<FileChangedCallback>;
10
- constructor(workspaceDir?: string, mountDir?: string | undefined);
11
- initialize(): Promise<void>;
12
- writeZipArchive(zipData: ArrayBuffer): Promise<void>;
13
- readFile(filePath: string): Promise<Uint8Array>;
14
- listFiles(): Promise<string[]>;
15
- getSize(): Promise<number>;
16
- writeFile(filePath: string, content: string | Uint8Array): Promise<void>;
17
- deleteFile(filePath: string): Promise<void>;
18
- onFileChange(callback: FileChangedCallback): () => void;
19
- private notifyChange;
20
- unmount(): void;
21
- }
22
- //#endregion
23
- export { ZenFsFileManager };
24
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","names":["mountDir?: string"],"sources":["../src/file-manager.ts"],"sourcesContent":[],"mappings":";;;cASa,gBAAA,YAA4B;;EAAzC,SAAa,YAAA,EAAA,MAAA;EACoB;EAAJ,SAAA,QAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAyBP,UAAA,eAAA,EAzBO,GAyBP,CAzBW,mBAyBX,CAAA;EAcW,WAAA,CAAA,YAAA,CAAA,EAAA,MAAA,EAAA,QAAA,CAAA,EAAA,MAAA,GAAA,SAAA;EAAc,UAAA,CAAA,CAAA,EAdzB,OAcyB,CAAA,IAAA,CAAA;EAmBH,eAAA,CAAA,OAAA,EAnBX,WAmBW,CAAA,EAnBG,OAmBH,CAAA,IAAA,CAAA;EAAR,QAAA,CAAA,QAAA,EAAA,MAAA,CAAA,EAAA,OAAA,CAAQ,UAAR,CAAA;EASf,SAAA,CAAA,CAAA,EAAA,OAAA,CAAA,MAAA,EAAA,CAAA;EAcF,OAAA,CAAA,CAAA,EAAA,OAAA,CAAA,MAAA,CAAA;EAwBG,SAAA,CAAA,QAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,GAAA,UAAA,CAAA,EACjB,OADiB,CAAA,IAAA,CAAA;EACjB,UAAA,CAAA,QAAA,EAAA,MAAA,CAAA,EAaiC,OAbjC,CAAA,IAAA,CAAA;EAaiC,YAAA,CAAA,QAAA,EASb,mBATa,CAAA,EAAA,GAAA,GAAA,IAAA;EASb,QAAA,YAAA;EAjIgB,OAAA,CAAA,CAAA,EAAA,IAAA"}
package/dist/index.js DELETED
@@ -1,2 +0,0 @@
1
- import{Zip as e}from"@zenfs/archives";import{configure as t,fs as n}from"@zenfs/core";import*as r from"@zenfs/core/path";import{IndexedDB as i}from"@zenfs/dom";import a from"debug";const o=a(`file-manager`);var s=class{changeListeners=new Set;constructor(e=`/workspace`,t){if(this.workspaceDir=e,this.mountDir=t,this.mountDir||=this.workspaceDir,this.mountDir!==this.workspaceDir){let e=r.normalize(this.workspaceDir),t=r.normalize(this.mountDir);if(!e.startsWith(t+`/`)&&e!==t)throw Error(`workspaceDir (${this.workspaceDir}) must be a subdirectory of mountDir (${this.mountDir})`)}}async initialize(){o(`🔧 Configuring filesystem...`),o(`📁 Mounting workspace at ${this.mountDir}`),await t({mounts:{[this.mountDir??this.workspaceDir]:{backend:i,storeName:this.mountDir??this.workspaceDir}}}),o(`✅ Filesystem configured`)}async writeZipArchive(r){let i=`/tmp/zip-write-${crypto.randomUUID()}`;o(`📦 Mounting ZIP archive to temporary directory...`),await t({mounts:{[i]:{backend:e,data:r,lazy:!1}}}),o(`📂 Copying files from ZIP archive to workspace...`),await n.promises.cp(i,this.workspaceDir,{recursive:!0}),o(`📦 Unmounting temporary ZIP directory...`),n.umount(i),o(`✅ Files copied to workspace from ZIP archive`)}async readFile(e){o(`📂 Reading file ${e}...`),e=c(e);let t=r.join(this.workspaceDir,e),i=await n.promises.readFile(t);return o(`✅ File ${e} read (${i.byteLength} bytes)`),i}async listFiles(){o(`📄 Listing files in workspace`);let e=(await n.promises.readdir(this.workspaceDir,{withFileTypes:!0,recursive:!0})).filter(e=>e.isFile()).map(e=>r.join(e.parentPath,e.name)).map(c);return o(`✅ Files currently in workspace:`,e),e}async getSize(){o(`📏 Calculating total size of files in workspace`);let e=0,t=await n.promises.readdir(this.workspaceDir,{withFileTypes:!0,recursive:!0});for(let i of t)if(i.isFile()){let t=r.join(this.workspaceDir,i.parentPath,i.name),a=await n.promises.stat(t);e+=a.size}return o(`✅ Total size of files in workspace:`,e,`bytes`),e}async writeFile(e,t){o(`✍️ Writing file to`,e),e=c(e);let i=r.join(this.workspaceDir,e),a=r.dirname(i);await n.promises.mkdir(a,{recursive:!0});let s=typeof t==`string`?new TextEncoder().encode(t):t;await n.promises.writeFile(i,s),o(`✅ File written to`,e,`(`,s.byteLength,`bytes )`),this.notifyChange({type:`updated`,path:e})}async deleteFile(e){o(`🗑️ Deleting file at`,e),e=c(e);let t=r.join(this.workspaceDir,e);await n.promises.rm(t),o(`✅ File deleted at`,e),this.notifyChange({type:`removed`,path:e})}onFileChange(e){return this.changeListeners.add(e),()=>{this.changeListeners.delete(e)}}notifyChange(e){for(let t of this.changeListeners)t(e)}unmount(){o(`🚪 Unmounting workspace directory (`,this.workspaceDir,`)`),this.changeListeners.clear(),this.mountDir===this.workspaceDir?n.umount(this.mountDir):o(`⚠️ Skipping unmount: mountDir and workspaceDir are different`)}};function c(e){return e.startsWith(`/`)?e:`/${e}`}export{s as ZenFsFileManager};
2
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":["mountDir?: string","path"],"sources":["../src/file-manager.ts"],"sourcesContent":["import type { FileChangedCallback, IFileManager } from '@cmdoss/site-builder'\nimport { Zip } from '@zenfs/archives'\nimport { configure, fs } from '@zenfs/core'\nimport * as path from '@zenfs/core/path'\nimport { IndexedDB } from '@zenfs/dom'\nimport debug from 'debug'\n\nconst log = debug('file-manager')\n\nexport class ZenFsFileManager implements IFileManager {\n protected changeListeners: Set<FileChangedCallback> = new Set()\n constructor(\n /** The directory of the workspace. Any files within this directory are considered part of the workspace. */\n public readonly workspaceDir = '/workspace',\n /** The directory where the workspace is mounted in the virtual filesystem. */\n public readonly mountDir?: string\n ) {\n if (!this.mountDir) {\n this.mountDir = this.workspaceDir\n }\n if (this.mountDir !== this.workspaceDir) {\n // Ensure workspaceDir is a subdirectory of mountDir\n const normalizedWorkspace = path.normalize(this.workspaceDir)\n const normalizedMount = path.normalize(this.mountDir)\n if (\n !normalizedWorkspace.startsWith(normalizedMount + '/') &&\n normalizedWorkspace !== normalizedMount\n ) {\n throw new Error(\n `workspaceDir (${this.workspaceDir}) must be a subdirectory of mountDir (${this.mountDir})`\n )\n }\n }\n }\n\n async initialize(): Promise<void> {\n log('🔧 Configuring filesystem...')\n log(`📁 Mounting workspace at ${this.mountDir}`)\n await configure({\n mounts: {\n [this.mountDir ?? this.workspaceDir]: {\n backend: IndexedDB,\n storeName: this.mountDir ?? this.workspaceDir\n }\n }\n })\n log('✅ Filesystem configured')\n }\n\n async writeZipArchive(zipData: ArrayBuffer): Promise<void> {\n const tmpDir = `/tmp/zip-write-${crypto.randomUUID()}`\n log('📦 Mounting ZIP archive to temporary directory...')\n await configure({\n mounts: {\n [tmpDir]: {\n backend: Zip,\n data: zipData,\n lazy: false // Extract all files immediately\n }\n }\n })\n log('📂 Copying files from ZIP archive to workspace...')\n await fs.promises.cp(tmpDir, this.workspaceDir, { recursive: true })\n log('📦 Unmounting temporary ZIP directory...')\n fs.umount(tmpDir)\n log('✅ Files copied to workspace from ZIP archive')\n }\n\n async readFile(filePath: string): Promise<Uint8Array> {\n log(`📂 Reading file ${filePath}...`)\n filePath = ensureLeadingSlash(filePath)\n const workspaceFilePath = path.join(this.workspaceDir, filePath)\n const content = await fs.promises.readFile(workspaceFilePath)\n log(`✅ File ${filePath} read (${content.byteLength} bytes)`)\n return content\n }\n\n async listFiles(): Promise<string[]> {\n log('📄 Listing files in workspace')\n const files = await fs.promises.readdir(this.workspaceDir, {\n withFileTypes: true,\n recursive: true\n })\n const result = files\n .filter(f => f.isFile())\n .map(f => path.join(f.parentPath, f.name))\n .map(ensureLeadingSlash)\n log('✅ Files currently in workspace:', result)\n return result\n }\n\n async getSize(): Promise<number> {\n log('📏 Calculating total size of files in workspace')\n let totalSize = 0\n const files = await fs.promises.readdir(this.workspaceDir, {\n withFileTypes: true,\n recursive: true\n })\n for (const file of files) {\n if (file.isFile()) {\n const filePath = path.join(\n this.workspaceDir,\n file.parentPath,\n file.name\n )\n const stats = await fs.promises.stat(filePath)\n totalSize += stats.size\n }\n }\n log('✅ Total size of files in workspace:', totalSize, 'bytes')\n return totalSize\n }\n\n async writeFile(\n filePath: string,\n content: string | Uint8Array\n ): Promise<void> {\n log('✍️ Writing file to', filePath)\n filePath = ensureLeadingSlash(filePath)\n const workspaceFilePath = path.join(this.workspaceDir, filePath)\n const dir = path.dirname(workspaceFilePath)\n await fs.promises.mkdir(dir, { recursive: true })\n const contentBytes =\n typeof content === 'string' ? new TextEncoder().encode(content) : content\n await fs.promises.writeFile(workspaceFilePath, contentBytes)\n log('✅ File written to', filePath, '(', contentBytes.byteLength, 'bytes )')\n this.notifyChange({ type: 'updated', path: filePath })\n }\n\n async deleteFile(filePath: string): Promise<void> {\n log('🗑️ Deleting file at', filePath)\n filePath = ensureLeadingSlash(filePath)\n const workspaceFilePath = path.join(this.workspaceDir, filePath)\n await fs.promises.rm(workspaceFilePath)\n log('✅ File deleted at', filePath)\n this.notifyChange({ type: 'removed', path: filePath })\n }\n\n onFileChange(callback: FileChangedCallback): () => void {\n this.changeListeners.add(callback)\n return () => {\n this.changeListeners.delete(callback)\n }\n }\n\n private notifyChange(arg: Parameters<FileChangedCallback>[0]): void {\n for (const listener of this.changeListeners) {\n listener(arg)\n }\n }\n\n unmount(): void {\n log('🚪 Unmounting workspace directory (', this.workspaceDir, ')')\n this.changeListeners.clear()\n if (this.mountDir === this.workspaceDir) {\n // Unmount only if mountDir and workspaceDir are the same\n fs.umount(this.mountDir)\n } else {\n log('⚠️ Skipping unmount: mountDir and workspaceDir are different')\n }\n }\n}\n\nfunction ensureLeadingSlash(path: string): string {\n return path.startsWith('/') ? path : `/${path}`\n}\n"],"mappings":"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"}
@@ -1,166 +0,0 @@
1
- import type { FileChangedCallback, IFileManager } from '@cmdoss/site-builder'
2
- import { Zip } from '@zenfs/archives'
3
- import { configure, fs } from '@zenfs/core'
4
- import * as path from '@zenfs/core/path'
5
- import { IndexedDB } from '@zenfs/dom'
6
- import debug from 'debug'
7
-
8
- const log = debug('file-manager')
9
-
10
- export class ZenFsFileManager implements IFileManager {
11
- protected changeListeners: Set<FileChangedCallback> = new Set()
12
- constructor(
13
- /** The directory of the workspace. Any files within this directory are considered part of the workspace. */
14
- public readonly workspaceDir = '/workspace',
15
- /** The directory where the workspace is mounted in the virtual filesystem. */
16
- public readonly mountDir?: string
17
- ) {
18
- if (!this.mountDir) {
19
- this.mountDir = this.workspaceDir
20
- }
21
- if (this.mountDir !== this.workspaceDir) {
22
- // Ensure workspaceDir is a subdirectory of mountDir
23
- const normalizedWorkspace = path.normalize(this.workspaceDir)
24
- const normalizedMount = path.normalize(this.mountDir)
25
- if (
26
- !normalizedWorkspace.startsWith(normalizedMount + '/') &&
27
- normalizedWorkspace !== normalizedMount
28
- ) {
29
- throw new Error(
30
- `workspaceDir (${this.workspaceDir}) must be a subdirectory of mountDir (${this.mountDir})`
31
- )
32
- }
33
- }
34
- }
35
-
36
- async initialize(): Promise<void> {
37
- log('🔧 Configuring filesystem...')
38
- log(`📁 Mounting workspace at ${this.mountDir}`)
39
- await configure({
40
- mounts: {
41
- [this.mountDir ?? this.workspaceDir]: {
42
- backend: IndexedDB,
43
- storeName: this.mountDir ?? this.workspaceDir
44
- }
45
- }
46
- })
47
- log('✅ Filesystem configured')
48
- }
49
-
50
- async writeZipArchive(zipData: ArrayBuffer): Promise<void> {
51
- const tmpDir = `/tmp/zip-write-${crypto.randomUUID()}`
52
- log('📦 Mounting ZIP archive to temporary directory...')
53
- await configure({
54
- mounts: {
55
- [tmpDir]: {
56
- backend: Zip,
57
- data: zipData,
58
- lazy: false // Extract all files immediately
59
- }
60
- }
61
- })
62
- log('📂 Copying files from ZIP archive to workspace...')
63
- await fs.promises.cp(tmpDir, this.workspaceDir, { recursive: true })
64
- log('📦 Unmounting temporary ZIP directory...')
65
- fs.umount(tmpDir)
66
- log('✅ Files copied to workspace from ZIP archive')
67
- }
68
-
69
- async readFile(filePath: string): Promise<Uint8Array> {
70
- log(`📂 Reading file ${filePath}...`)
71
- filePath = ensureLeadingSlash(filePath)
72
- const workspaceFilePath = path.join(this.workspaceDir, filePath)
73
- const content = await fs.promises.readFile(workspaceFilePath)
74
- log(`✅ File ${filePath} read (${content.byteLength} bytes)`)
75
- return content
76
- }
77
-
78
- async listFiles(): Promise<string[]> {
79
- log('📄 Listing files in workspace')
80
- const files = await fs.promises.readdir(this.workspaceDir, {
81
- withFileTypes: true,
82
- recursive: true
83
- })
84
- const result = files
85
- .filter(f => f.isFile())
86
- .map(f => path.join(f.parentPath, f.name))
87
- .map(ensureLeadingSlash)
88
- log('✅ Files currently in workspace:', result)
89
- return result
90
- }
91
-
92
- async getSize(): Promise<number> {
93
- log('📏 Calculating total size of files in workspace')
94
- let totalSize = 0
95
- const files = await fs.promises.readdir(this.workspaceDir, {
96
- withFileTypes: true,
97
- recursive: true
98
- })
99
- for (const file of files) {
100
- if (file.isFile()) {
101
- const filePath = path.join(
102
- this.workspaceDir,
103
- file.parentPath,
104
- file.name
105
- )
106
- const stats = await fs.promises.stat(filePath)
107
- totalSize += stats.size
108
- }
109
- }
110
- log('✅ Total size of files in workspace:', totalSize, 'bytes')
111
- return totalSize
112
- }
113
-
114
- async writeFile(
115
- filePath: string,
116
- content: string | Uint8Array
117
- ): Promise<void> {
118
- log('✍️ Writing file to', filePath)
119
- filePath = ensureLeadingSlash(filePath)
120
- const workspaceFilePath = path.join(this.workspaceDir, filePath)
121
- const dir = path.dirname(workspaceFilePath)
122
- await fs.promises.mkdir(dir, { recursive: true })
123
- const contentBytes =
124
- typeof content === 'string' ? new TextEncoder().encode(content) : content
125
- await fs.promises.writeFile(workspaceFilePath, contentBytes)
126
- log('✅ File written to', filePath, '(', contentBytes.byteLength, 'bytes )')
127
- this.notifyChange({ type: 'updated', path: filePath })
128
- }
129
-
130
- async deleteFile(filePath: string): Promise<void> {
131
- log('🗑️ Deleting file at', filePath)
132
- filePath = ensureLeadingSlash(filePath)
133
- const workspaceFilePath = path.join(this.workspaceDir, filePath)
134
- await fs.promises.rm(workspaceFilePath)
135
- log('✅ File deleted at', filePath)
136
- this.notifyChange({ type: 'removed', path: filePath })
137
- }
138
-
139
- onFileChange(callback: FileChangedCallback): () => void {
140
- this.changeListeners.add(callback)
141
- return () => {
142
- this.changeListeners.delete(callback)
143
- }
144
- }
145
-
146
- private notifyChange(arg: Parameters<FileChangedCallback>[0]): void {
147
- for (const listener of this.changeListeners) {
148
- listener(arg)
149
- }
150
- }
151
-
152
- unmount(): void {
153
- log('🚪 Unmounting workspace directory (', this.workspaceDir, ')')
154
- this.changeListeners.clear()
155
- if (this.mountDir === this.workspaceDir) {
156
- // Unmount only if mountDir and workspaceDir are the same
157
- fs.umount(this.mountDir)
158
- } else {
159
- log('⚠️ Skipping unmount: mountDir and workspaceDir are different')
160
- }
161
- }
162
- }
163
-
164
- function ensureLeadingSlash(path: string): string {
165
- return path.startsWith('/') ? path : `/${path}`
166
- }
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from './file-manager'
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/tsconfig",
3
- "extends": "../../configs/tsconfig.library.json",
4
- "compilerOptions": {
5
- "lib": ["ES2021", "DOM"],
6
- "outDir": "dist",
7
- "emitDeclarationOnly": true,
8
- "allowImportingTsExtensions": true
9
- },
10
- "include": ["src/**/*"]
11
- }