@ecmaos/coreutils 0.3.0 → 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.
Files changed (69) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +68 -0
  3. package/dist/commands/cron.d.ts +4 -0
  4. package/dist/commands/cron.d.ts.map +1 -0
  5. package/dist/commands/cron.js +532 -0
  6. package/dist/commands/cron.js.map +1 -0
  7. package/dist/commands/env.d.ts +4 -0
  8. package/dist/commands/env.d.ts.map +1 -0
  9. package/dist/commands/env.js +129 -0
  10. package/dist/commands/env.js.map +1 -0
  11. package/dist/commands/head.d.ts.map +1 -1
  12. package/dist/commands/head.js +184 -77
  13. package/dist/commands/head.js.map +1 -1
  14. package/dist/commands/less.d.ts.map +1 -1
  15. package/dist/commands/less.js +1 -0
  16. package/dist/commands/less.js.map +1 -1
  17. package/dist/commands/man.d.ts.map +1 -1
  18. package/dist/commands/man.js +13 -1
  19. package/dist/commands/man.js.map +1 -1
  20. package/dist/commands/mount.d.ts +4 -0
  21. package/dist/commands/mount.d.ts.map +1 -0
  22. package/dist/commands/mount.js +1041 -0
  23. package/dist/commands/mount.js.map +1 -0
  24. package/dist/commands/open.d.ts +4 -0
  25. package/dist/commands/open.d.ts.map +1 -0
  26. package/dist/commands/open.js +74 -0
  27. package/dist/commands/open.js.map +1 -0
  28. package/dist/commands/play.d.ts +4 -0
  29. package/dist/commands/play.d.ts.map +1 -0
  30. package/dist/commands/play.js +231 -0
  31. package/dist/commands/play.js.map +1 -0
  32. package/dist/commands/tar.d.ts.map +1 -1
  33. package/dist/commands/tar.js +67 -17
  34. package/dist/commands/tar.js.map +1 -1
  35. package/dist/commands/umount.d.ts +4 -0
  36. package/dist/commands/umount.d.ts.map +1 -0
  37. package/dist/commands/umount.js +104 -0
  38. package/dist/commands/umount.js.map +1 -0
  39. package/dist/commands/video.d.ts +4 -0
  40. package/dist/commands/video.d.ts.map +1 -0
  41. package/dist/commands/video.js +250 -0
  42. package/dist/commands/video.js.map +1 -0
  43. package/dist/commands/view.d.ts +5 -0
  44. package/dist/commands/view.d.ts.map +1 -0
  45. package/dist/commands/view.js +830 -0
  46. package/dist/commands/view.js.map +1 -0
  47. package/dist/commands/web.d.ts +4 -0
  48. package/dist/commands/web.d.ts.map +1 -0
  49. package/dist/commands/web.js +348 -0
  50. package/dist/commands/web.js.map +1 -0
  51. package/dist/index.d.ts +9 -0
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +29 -2
  54. package/dist/index.js.map +1 -1
  55. package/package.json +11 -2
  56. package/src/commands/cron.ts +591 -0
  57. package/src/commands/env.ts +143 -0
  58. package/src/commands/head.ts +176 -77
  59. package/src/commands/less.ts +1 -0
  60. package/src/commands/man.ts +12 -1
  61. package/src/commands/mount.ts +1193 -0
  62. package/src/commands/open.ts +84 -0
  63. package/src/commands/play.ts +249 -0
  64. package/src/commands/tar.ts +62 -19
  65. package/src/commands/umount.ts +117 -0
  66. package/src/commands/video.ts +267 -0
  67. package/src/commands/view.ts +916 -0
  68. package/src/commands/web.ts +377 -0
  69. package/src/index.ts +29 -2
@@ -0,0 +1,84 @@
1
+ import path from 'path'
2
+ import chalk from 'chalk'
3
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
4
+ import { TerminalCommand } from '../shared/terminal-command.js'
5
+ import { writelnStderr } from '../shared/helpers.js'
6
+
7
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
8
+ const usage = `Usage: open [FILE|URL]
9
+ Open a file or URL.
10
+
11
+ --help display this help and exit
12
+
13
+ Examples:
14
+ open file.txt open a file in the current directory
15
+ open /path/to/file.txt open a file by absolute path
16
+ open sample-1/sample-5 (1).jpg open a file with spaces in the name
17
+ open https://example.com open a URL in a new tab`
18
+ writelnStderr(process, terminal, usage)
19
+ }
20
+
21
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
22
+ return new TerminalCommand({
23
+ command: 'open',
24
+ description: 'Open a file or URL',
25
+ kernel,
26
+ shell,
27
+ terminal,
28
+ run: async (pid: number, argv: string[]) => {
29
+ const process = kernel.processes.get(pid) as Process | undefined
30
+
31
+ if (!process) return 1
32
+
33
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
34
+ printUsage(process, terminal)
35
+ return 0
36
+ }
37
+
38
+ if (argv.length === 0) {
39
+ await writelnStderr(process, terminal, `open: missing file or URL argument`)
40
+ await writelnStderr(process, terminal, `Try 'open --help' for more information.`)
41
+ return 1
42
+ }
43
+
44
+ const filePath = argv.join(' ')
45
+
46
+ if (!filePath) {
47
+ await writelnStderr(process, terminal, `open: missing file or URL argument`)
48
+ return 1
49
+ }
50
+
51
+ // Check if it's a URL by looking for URL schemes
52
+ const urlPattern = /^[a-zA-Z][a-zA-Z\d+\-.]*:/
53
+ const isURL = urlPattern.test(filePath)
54
+
55
+ if (isURL) {
56
+ window.open(filePath, '_blank')
57
+ return 0
58
+ }
59
+
60
+ // Treat as file path - resolve relative to current working directory
61
+ const fullPath = path.resolve(shell.cwd, filePath)
62
+
63
+ try {
64
+ if (!(await shell.context.fs.promises.exists(fullPath))) {
65
+ await writelnStderr(process, terminal, chalk.red(`open: file not found: ${fullPath}`))
66
+ return 1
67
+ }
68
+
69
+ const file = await shell.context.fs.promises.readFile(fullPath)
70
+ const blob = new Blob([new Uint8Array(file)], { type: 'application/octet-stream' })
71
+ const url = window.URL.createObjectURL(blob)
72
+ const a = document.createElement('a')
73
+ a.href = url
74
+ a.download = path.basename(fullPath)
75
+ a.click()
76
+ window.URL.revokeObjectURL(url)
77
+ return 0
78
+ } catch (error) {
79
+ await writelnStderr(process, terminal, chalk.red(`open: ${error instanceof Error ? error.message : 'Unknown error'}`))
80
+ return 1
81
+ }
82
+ }
83
+ })
84
+ }
@@ -0,0 +1,249 @@
1
+ import path from 'path'
2
+ import chalk from 'chalk'
3
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
4
+ import { TerminalCommand } from '../shared/terminal-command.js'
5
+ import { writelnStderr, writelnStdout } from '../shared/helpers.js'
6
+
7
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
8
+ const usage = `Usage: play [OPTIONS] [FILE...]
9
+ Play an audio file.
10
+
11
+ --help display this help and exit
12
+ --no-autoplay don't start playing automatically
13
+ --loop loop the audio
14
+ --muted start muted
15
+ --volume <0-100> set volume (0-100, default: 100)
16
+ --quiet play without opening a window (background playback)
17
+
18
+ Examples:
19
+ play song.mp3 play an audio file
20
+ play --loop music.mp3 play audio in a loop
21
+ play --no-autoplay audio.mp3 load audio without auto-playing
22
+ play --volume 50 track.mp3 play at 50% volume
23
+ play --quiet background.mp3 play audio in background
24
+ play song1.mp3 song2.mp3 play multiple audio files`
25
+ writelnStderr(process, terminal, usage)
26
+ }
27
+
28
+ function getMimeType(filePath: string): string {
29
+ const ext = path.extname(filePath).toLowerCase()
30
+ const mimeTypes: Record<string, string> = {
31
+ '.mp3': 'audio/mpeg',
32
+ '.wav': 'audio/wav',
33
+ '.ogg': 'audio/ogg',
34
+ '.oga': 'audio/ogg',
35
+ '.opus': 'audio/opus',
36
+ '.m4a': 'audio/mp4',
37
+ '.aac': 'audio/aac',
38
+ '.flac': 'audio/flac',
39
+ '.webm': 'audio/webm',
40
+ '.wma': 'audio/x-ms-wma',
41
+ '.aiff': 'audio/aiff',
42
+ '.aif': 'audio/aiff',
43
+ '.3gp': 'audio/3gpp',
44
+ '.amr': 'audio/amr'
45
+ }
46
+ return mimeTypes[ext] || 'audio/mpeg'
47
+ }
48
+
49
+ async function loadAudioMetadata(audioElement: HTMLAudioElement): Promise<{ duration: number }> {
50
+ return new Promise((resolve, reject) => {
51
+ const timeout = setTimeout(() => {
52
+ reject(new Error('Timeout loading audio metadata'))
53
+ }, 10000)
54
+
55
+ audioElement.onloadedmetadata = () => {
56
+ clearTimeout(timeout)
57
+ resolve({
58
+ duration: audioElement.duration
59
+ })
60
+ }
61
+
62
+ audioElement.onerror = () => {
63
+ clearTimeout(timeout)
64
+ reject(new Error('Failed to load audio metadata'))
65
+ }
66
+ })
67
+ }
68
+
69
+ function formatDuration(seconds: number): string {
70
+ if (!isFinite(seconds) || isNaN(seconds)) return '?:??'
71
+ const hours = Math.floor(seconds / 3600)
72
+ const minutes = Math.floor((seconds % 3600) / 60)
73
+ const secs = Math.floor(seconds % 60)
74
+
75
+ if (hours > 0) {
76
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
77
+ }
78
+ return `${minutes}:${secs.toString().padStart(2, '0')}`
79
+ }
80
+
81
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
82
+ return new TerminalCommand({
83
+ command: 'play',
84
+ description: 'Play an audio file',
85
+ kernel,
86
+ shell,
87
+ terminal,
88
+ run: async (pid: number, argv: string[]) => {
89
+ const process = kernel.processes.get(pid) as Process | undefined
90
+
91
+ if (!process) return 1
92
+
93
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
94
+ printUsage(process, terminal)
95
+ return 0
96
+ }
97
+
98
+ // Parse options
99
+ const options: {
100
+ autoplay: boolean
101
+ loop: boolean
102
+ muted: boolean
103
+ volume: number
104
+ quiet: boolean
105
+ } = {
106
+ autoplay: true,
107
+ loop: false,
108
+ muted: false,
109
+ volume: 100,
110
+ quiet: false
111
+ }
112
+
113
+ const files: string[] = []
114
+
115
+ for (let i = 0; i < argv.length; i++) {
116
+ const arg = argv[i]
117
+ if (arg === '--no-autoplay') {
118
+ options.autoplay = false
119
+ } else if (arg === '--loop') {
120
+ options.loop = true
121
+ } else if (arg === '--muted') {
122
+ options.muted = true
123
+ } else if (arg === '--quiet') {
124
+ options.quiet = true
125
+ } else if (arg === '--volume' && i + 1 < argv.length) {
126
+ const volumeArg = argv[i + 1]
127
+ if (!volumeArg) {
128
+ await writelnStderr(process, terminal, chalk.red(`play: missing volume value`))
129
+ return 1
130
+ }
131
+ const volume = parseFloat(volumeArg)
132
+ if (isNaN(volume) || volume < 0 || volume > 100) {
133
+ await writelnStderr(process, terminal, chalk.red(`play: invalid volume: ${volumeArg} (must be 0-100)`))
134
+ return 1
135
+ }
136
+ options.volume = volume
137
+ i++ // Skip next argument
138
+ } else if (arg && !arg.startsWith('--')) {
139
+ files.push(arg)
140
+ }
141
+ }
142
+
143
+ if (files.length === 0) {
144
+ await writelnStderr(process, terminal, `play: missing file argument`)
145
+ await writelnStderr(process, terminal, `Try 'play --help' for more information.`)
146
+ return 1
147
+ }
148
+
149
+ // Process each audio file
150
+ for (const file of files) {
151
+ const fullPath = path.resolve(shell.cwd, file)
152
+
153
+ try {
154
+ // Check if file exists
155
+ if (!(await shell.context.fs.promises.exists(fullPath))) {
156
+ await writelnStderr(process, terminal, chalk.red(`play: file not found: ${fullPath}`))
157
+ continue
158
+ }
159
+
160
+ // Read file
161
+ await writelnStdout(process, terminal, chalk.blue(`Loading audio: ${file}...`))
162
+ const fileData = await shell.context.fs.promises.readFile(fullPath)
163
+ const mimeType = getMimeType(fullPath)
164
+ const blob = new Blob([new Uint8Array(fileData)], { type: mimeType })
165
+ const url = URL.createObjectURL(blob)
166
+
167
+ // Load audio metadata
168
+ const audioElement = document.createElement('audio')
169
+ audioElement.src = url
170
+ audioElement.preload = 'metadata'
171
+
172
+ let duration = 0
173
+
174
+ try {
175
+ const metadata = await loadAudioMetadata(audioElement)
176
+ duration = metadata.duration
177
+ } catch (error) {
178
+ await writelnStderr(process, terminal, chalk.yellow(`play: warning: could not load metadata for ${file}`))
179
+ }
180
+
181
+ // Set audio properties
182
+ audioElement.volume = options.volume / 100
183
+ if (options.autoplay) audioElement.autoplay = true
184
+ if (options.loop) audioElement.loop = true
185
+ if (options.muted) audioElement.muted = true
186
+
187
+ if (options.quiet) {
188
+ // Background playback - no window
189
+ audioElement.play().catch((error) => {
190
+ // Autoplay may be blocked by browser, but that's okay for quiet mode
191
+ console.warn('Autoplay blocked:', error)
192
+ })
193
+
194
+ if (duration > 0) {
195
+ const durationStr = formatDuration(duration)
196
+ await writelnStdout(process, terminal, chalk.green(`Playing in background: ${file} (${durationStr})`))
197
+ } else {
198
+ await writelnStdout(process, terminal, chalk.green(`Playing in background: ${file}`))
199
+ }
200
+ } else {
201
+ // Create a simple audio player window
202
+ const windowTitle = files.length > 1
203
+ ? `${path.basename(file)} (${files.indexOf(file) + 1}/${files.length})`
204
+ : path.basename(file)
205
+
206
+ // Create audio player HTML with controls
207
+ const audioId = `audio-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
208
+ const audioHtml = `
209
+ <div style="width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#1e1e1e;color:#fff;font-family:monospace;padding:20px;box-sizing:border-box;">
210
+ <div style="font-size:18px;margin-bottom:20px;text-align:center;word-break:break-word;">${path.basename(file)}</div>
211
+ <audio id="${audioId}" src="${url}" ${options.autoplay ? 'autoplay' : ''} ${options.loop ? 'loop' : ''} ${options.muted ? 'muted' : ''} controls style="width:100%;max-width:600px;"></audio>
212
+ </div>
213
+ <script>
214
+ (function() {
215
+ const audio = document.getElementById('${audioId}');
216
+ if (audio) {
217
+ audio.volume = ${options.volume / 100};
218
+ audio.addEventListener('play', () => console.log('Playing: ${file}'));
219
+ audio.addEventListener('ended', () => console.log('Finished: ${file}'));
220
+ }
221
+ })();
222
+ </script>
223
+ `
224
+
225
+ kernel.windows.create({
226
+ title: windowTitle,
227
+ html: audioHtml,
228
+ width: 500,
229
+ height: 200,
230
+ max: false
231
+ })
232
+
233
+ if (duration > 0) {
234
+ const durationStr = formatDuration(duration)
235
+ await writelnStdout(process, terminal, chalk.green(`Playing: ${file} (${durationStr})`))
236
+ } else {
237
+ await writelnStdout(process, terminal, chalk.green(`Playing: ${file}`))
238
+ }
239
+ }
240
+ } catch (error) {
241
+ await writelnStderr(process, terminal, chalk.red(`play: error playing ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`))
242
+ return 1
243
+ }
244
+ }
245
+
246
+ return 0
247
+ }
248
+ })
249
+ }
@@ -89,27 +89,70 @@ function parseArgs(argv: string[]): { options: TarOptions; files: string[] } {
89
89
  i++
90
90
  } else if (arg.startsWith('-')) {
91
91
  // Handle combined flags like -czf
92
- const flags = arg.slice(1).split('')
93
- for (const flag of flags) {
94
- if (flag === 'c') options.create = true
95
- else if (flag === 'x') options.extract = true
96
- else if (flag === 't') options.list = true
97
- else if (flag === 'f') {
98
- // -f needs to be followed by filename, check if next arg exists
99
- const nextArg = argv[i + 1]
100
- if (i + 1 < argv.length && typeof nextArg === 'string' && !nextArg.startsWith('-')) {
101
- i++
102
- options.file = nextArg
92
+ const flagString = arg.slice(1)
93
+ let flagIndex = 0
94
+ while (flagIndex < flagString.length) {
95
+ const flag = flagString[flagIndex]
96
+ if (flag === 'c') {
97
+ options.create = true
98
+ flagIndex++
99
+ } else if (flag === 'x') {
100
+ options.extract = true
101
+ flagIndex++
102
+ } else if (flag === 't') {
103
+ options.list = true
104
+ flagIndex++
105
+ } else if (flag === 'f') {
106
+ // -f needs to be followed by filename
107
+ // Check if there's a path in the same string after 'f'
108
+ const remaining = flagString.slice(flagIndex + 1)
109
+ if (remaining.length > 0 && !remaining.startsWith('-')) {
110
+ // Path is in the same string
111
+ options.file = remaining
112
+ flagIndex = flagString.length // Done processing this arg
113
+ } else if (i + 1 < argv.length) {
114
+ // Check next argument
115
+ const nextArg = argv[i + 1]
116
+ if (typeof nextArg === 'string' && !nextArg.startsWith('-')) {
117
+ i++
118
+ options.file = nextArg
119
+ flagIndex++
120
+ } else {
121
+ flagIndex++
122
+ }
123
+ } else {
124
+ flagIndex++
103
125
  }
104
- } else if (flag === 'z') options.gzip = true
105
- else if (flag === 'v') options.verbose = true
106
- else if (flag === 'C') {
107
- // -C needs to be followed by directory, check if next arg exists
108
- const nextArg = argv[i + 1]
109
- if (i + 1 < argv.length && typeof nextArg === 'string' && !nextArg.startsWith('-')) {
110
- i++
111
- options.directory = nextArg
126
+ } else if (flag === 'z') {
127
+ options.gzip = true
128
+ flagIndex++
129
+ } else if (flag === 'v') {
130
+ options.verbose = true
131
+ flagIndex++
132
+ } else if (flag === 'C') {
133
+ // -C needs to be followed by directory
134
+ // Check if there's a path in the same string after 'C'
135
+ const remaining = flagString.slice(flagIndex + 1)
136
+ if (remaining.length > 0 && !remaining.startsWith('-')) {
137
+ // Path is in the same string (e.g., -xz-C/tmp/dir)
138
+ options.directory = remaining
139
+ flagIndex = flagString.length // Done processing this arg
140
+ } else if (i + 1 < argv.length) {
141
+ // Check next argument
142
+ const nextArg = argv[i + 1]
143
+ if (typeof nextArg === 'string' && !nextArg.startsWith('-')) {
144
+ i++
145
+ options.directory = nextArg
146
+ flagIndex++
147
+ } else {
148
+ flagIndex++
149
+ }
150
+ } else {
151
+ flagIndex++
112
152
  }
153
+ } else {
154
+ // Unknown flag, skip it
155
+ flagIndex++
113
156
  }
114
157
  }
115
158
  i++
@@ -0,0 +1,117 @@
1
+ import path from 'path'
2
+ import chalk from 'chalk'
3
+ import { mounts } from '@zenfs/core'
4
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
5
+ import { TerminalCommand } from '../shared/terminal-command.js'
6
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js'
7
+
8
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
9
+ const usage = `Usage: umount [OPTIONS] TARGET
10
+ umount [-a|--all]
11
+
12
+ Unmount a filesystem.
13
+
14
+ Options:
15
+ -a, --all unmount all filesystems (except root)
16
+ --help display this help and exit
17
+
18
+ Examples:
19
+ umount /mnt/tmp unmount filesystem at /mnt/tmp
20
+ umount -a unmount all filesystems`
21
+ writelnStderr(process, terminal, usage)
22
+ }
23
+
24
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
25
+ return new TerminalCommand({
26
+ command: 'umount',
27
+ description: 'Unmount a filesystem',
28
+ kernel,
29
+ shell,
30
+ terminal,
31
+ run: async (pid: number, argv: string[]) => {
32
+ const process = kernel.processes.get(pid) as Process | undefined
33
+
34
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
35
+ printUsage(process, terminal)
36
+ return 0
37
+ }
38
+
39
+ let allMode = false
40
+ const positionalArgs: string[] = []
41
+
42
+ for (let i = 0; i < argv.length; i++) {
43
+ const arg = argv[i]
44
+ if (arg === '-a' || arg === '--all') {
45
+ allMode = true
46
+ } else if (arg && !arg.startsWith('-')) {
47
+ positionalArgs.push(arg)
48
+ }
49
+ }
50
+
51
+
52
+ if (allMode) {
53
+ const mountList = Array.from(mounts.keys())
54
+ let unmountedCount = 0
55
+ let errorCount = 0
56
+
57
+ for (const target of mountList) {
58
+ if (target === '/') continue
59
+
60
+ try {
61
+ kernel.filesystem.fsSync.umount(target)
62
+ unmountedCount++
63
+ await writelnStdout(process, terminal, chalk.green(`Unmounted ${target}`))
64
+ } catch (error) {
65
+ errorCount++
66
+ await writelnStderr(process, terminal, chalk.red(`umount: failed to unmount ${target}: ${error instanceof Error ? error.message : 'Unknown error'}`))
67
+ }
68
+ }
69
+
70
+ if (unmountedCount === 0 && errorCount === 0) {
71
+ await writelnStdout(process, terminal, 'No filesystems to unmount.')
72
+ }
73
+
74
+ return errorCount > 0 ? 1 : 0
75
+ }
76
+
77
+ if (positionalArgs.length === 0) {
78
+ await writelnStderr(process, terminal, chalk.red('umount: missing target argument'))
79
+ await writelnStderr(process, terminal, 'Try \'umount --help\' for more information.')
80
+ return 1
81
+ }
82
+
83
+ if (positionalArgs.length > 1) {
84
+ await writelnStderr(process, terminal, chalk.red('umount: too many arguments'))
85
+ await writelnStderr(process, terminal, 'Try \'umount --help\' for more information.')
86
+ return 1
87
+ }
88
+
89
+ const targetArg = positionalArgs[0]
90
+ if (!targetArg) {
91
+ await writelnStderr(process, terminal, chalk.red('umount: missing target argument'))
92
+ return 1
93
+ }
94
+ const target = path.resolve(shell.cwd, targetArg)
95
+
96
+ if (target === '/') {
97
+ await writelnStderr(process, terminal, chalk.red('umount: cannot unmount root filesystem'))
98
+ return 1
99
+ }
100
+
101
+ const mountList = Array.from(mounts.keys())
102
+ if (!mountList.includes(target)) {
103
+ await writelnStderr(process, terminal, chalk.red(`umount: ${target} is not mounted`))
104
+ return 1
105
+ }
106
+
107
+ try {
108
+ kernel.filesystem.fsSync.umount(target)
109
+ await writelnStdout(process, terminal, chalk.green(`Unmounted ${target}`))
110
+ return 0
111
+ } catch (error) {
112
+ await writelnStderr(process, terminal, chalk.red(`umount: failed to unmount ${target}: ${error instanceof Error ? error.message : 'Unknown error'}`))
113
+ return 1
114
+ }
115
+ }
116
+ })
117
+ }