@ecmaos/coreutils 0.2.1 → 0.3.1

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 (150) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +37 -0
  3. package/LICENSE +1 -1
  4. package/dist/commands/cal.js +2 -2
  5. package/dist/commands/cal.js.map +1 -1
  6. package/dist/commands/cat.js +2 -2
  7. package/dist/commands/cd.js +2 -2
  8. package/dist/commands/chmod.d.ts.map +1 -1
  9. package/dist/commands/chmod.js +16 -11
  10. package/dist/commands/chmod.js.map +1 -1
  11. package/dist/commands/cp.js +2 -2
  12. package/dist/commands/cp.js.map +1 -1
  13. package/dist/commands/cron.d.ts +4 -0
  14. package/dist/commands/cron.d.ts.map +1 -0
  15. package/dist/commands/cron.js +439 -0
  16. package/dist/commands/cron.js.map +1 -0
  17. package/dist/commands/date.js +2 -2
  18. package/dist/commands/date.js.map +1 -1
  19. package/dist/commands/echo.js +2 -2
  20. package/dist/commands/echo.js.map +1 -1
  21. package/dist/commands/false.js +2 -2
  22. package/dist/commands/fetch.d.ts +4 -0
  23. package/dist/commands/fetch.d.ts.map +1 -0
  24. package/dist/commands/fetch.js +210 -0
  25. package/dist/commands/fetch.js.map +1 -0
  26. package/dist/commands/format.d.ts +4 -0
  27. package/dist/commands/format.d.ts.map +1 -0
  28. package/dist/commands/format.js +178 -0
  29. package/dist/commands/format.js.map +1 -0
  30. package/dist/commands/hash.d.ts +4 -0
  31. package/dist/commands/hash.d.ts.map +1 -0
  32. package/dist/commands/hash.js +200 -0
  33. package/dist/commands/hash.js.map +1 -0
  34. package/dist/commands/head.js +2 -2
  35. package/dist/commands/id.js +2 -2
  36. package/dist/commands/id.js.map +1 -1
  37. package/dist/commands/less.d.ts.map +1 -1
  38. package/dist/commands/less.js +53 -2
  39. package/dist/commands/less.js.map +1 -1
  40. package/dist/commands/ls.d.ts.map +1 -1
  41. package/dist/commands/ls.js +41 -15
  42. package/dist/commands/ls.js.map +1 -1
  43. package/dist/commands/man.d.ts +4 -0
  44. package/dist/commands/man.d.ts.map +1 -0
  45. package/dist/commands/man.js +564 -0
  46. package/dist/commands/man.js.map +1 -0
  47. package/dist/commands/mkdir.js +2 -2
  48. package/dist/commands/mkdir.js.map +1 -1
  49. package/dist/commands/mktemp.d.ts +4 -0
  50. package/dist/commands/mktemp.d.ts.map +1 -0
  51. package/dist/commands/mktemp.js +229 -0
  52. package/dist/commands/mktemp.js.map +1 -0
  53. package/dist/commands/nc.js +2 -2
  54. package/dist/commands/nc.js.map +1 -1
  55. package/dist/commands/open.d.ts +4 -0
  56. package/dist/commands/open.d.ts.map +1 -0
  57. package/dist/commands/open.js +74 -0
  58. package/dist/commands/open.js.map +1 -0
  59. package/dist/commands/passkey.js +3 -3
  60. package/dist/commands/play.d.ts +4 -0
  61. package/dist/commands/play.d.ts.map +1 -0
  62. package/dist/commands/play.js +231 -0
  63. package/dist/commands/play.js.map +1 -0
  64. package/dist/commands/pwd.js +2 -2
  65. package/dist/commands/pwd.js.map +1 -1
  66. package/dist/commands/rm.d.ts.map +1 -1
  67. package/dist/commands/rm.js +57 -12
  68. package/dist/commands/rm.js.map +1 -1
  69. package/dist/commands/rmdir.js +2 -2
  70. package/dist/commands/rmdir.js.map +1 -1
  71. package/dist/commands/sockets.js +1 -1
  72. package/dist/commands/stat.d.ts.map +1 -1
  73. package/dist/commands/stat.js +37 -15
  74. package/dist/commands/stat.js.map +1 -1
  75. package/dist/commands/tail.js +2 -2
  76. package/dist/commands/tar.d.ts +4 -0
  77. package/dist/commands/tar.d.ts.map +1 -0
  78. package/dist/commands/tar.js +743 -0
  79. package/dist/commands/tar.js.map +1 -0
  80. package/dist/commands/touch.js +2 -2
  81. package/dist/commands/touch.js.map +1 -1
  82. package/dist/commands/true.js +2 -2
  83. package/dist/commands/unzip.d.ts +4 -0
  84. package/dist/commands/unzip.d.ts.map +1 -0
  85. package/dist/commands/unzip.js +443 -0
  86. package/dist/commands/unzip.js.map +1 -0
  87. package/dist/commands/user.js +1 -1
  88. package/dist/commands/video.d.ts +4 -0
  89. package/dist/commands/video.d.ts.map +1 -0
  90. package/dist/commands/video.js +250 -0
  91. package/dist/commands/video.js.map +1 -0
  92. package/dist/commands/view.d.ts +4 -0
  93. package/dist/commands/view.d.ts.map +1 -0
  94. package/dist/commands/view.js +488 -0
  95. package/dist/commands/view.js.map +1 -0
  96. package/dist/commands/web.d.ts +4 -0
  97. package/dist/commands/web.d.ts.map +1 -0
  98. package/dist/commands/web.js +348 -0
  99. package/dist/commands/web.js.map +1 -0
  100. package/dist/commands/whoami.js +2 -2
  101. package/dist/commands/whoami.js.map +1 -1
  102. package/dist/commands/zip.d.ts +4 -0
  103. package/dist/commands/zip.d.ts.map +1 -0
  104. package/dist/commands/zip.js +264 -0
  105. package/dist/commands/zip.js.map +1 -0
  106. package/dist/index.d.ts +14 -0
  107. package/dist/index.d.ts.map +1 -1
  108. package/dist/index.js +44 -2
  109. package/dist/index.js.map +1 -1
  110. package/package.json +7 -4
  111. package/src/commands/cal.ts +2 -2
  112. package/src/commands/cat.ts +2 -2
  113. package/src/commands/cd.ts +2 -2
  114. package/src/commands/chmod.ts +19 -11
  115. package/src/commands/cp.ts +2 -2
  116. package/src/commands/cron.ts +499 -0
  117. package/src/commands/date.ts +2 -2
  118. package/src/commands/echo.ts +2 -2
  119. package/src/commands/false.ts +2 -2
  120. package/src/commands/fetch.ts +205 -0
  121. package/src/commands/format.ts +204 -0
  122. package/src/commands/hash.ts +215 -0
  123. package/src/commands/head.ts +2 -2
  124. package/src/commands/id.ts +2 -2
  125. package/src/commands/less.ts +50 -2
  126. package/src/commands/ls.ts +40 -14
  127. package/src/commands/man.ts +651 -0
  128. package/src/commands/mkdir.ts +2 -2
  129. package/src/commands/mktemp.ts +235 -0
  130. package/src/commands/nc.ts +2 -2
  131. package/src/commands/open.ts +84 -0
  132. package/src/commands/passkey.ts +3 -3
  133. package/src/commands/play.ts +249 -0
  134. package/src/commands/pwd.ts +2 -2
  135. package/src/commands/rm.ts +54 -12
  136. package/src/commands/rmdir.ts +2 -2
  137. package/src/commands/sockets.ts +1 -1
  138. package/src/commands/stat.ts +44 -16
  139. package/src/commands/tail.ts +2 -2
  140. package/src/commands/tar.ts +780 -0
  141. package/src/commands/touch.ts +2 -2
  142. package/src/commands/true.ts +2 -2
  143. package/src/commands/unzip.ts +517 -0
  144. package/src/commands/user.ts +1 -1
  145. package/src/commands/video.ts +267 -0
  146. package/src/commands/view.ts +526 -0
  147. package/src/commands/web.ts +377 -0
  148. package/src/commands/whoami.ts +2 -2
  149. package/src/commands/zip.ts +319 -0
  150. package/src/index.ts +44 -2
@@ -0,0 +1,235 @@
1
+ import path from 'path'
2
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
3
+ import { TerminalCommand } from '../shared/terminal-command.js'
4
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js'
5
+
6
+ /**
7
+ * Generate random alphanumeric characters for template replacement
8
+ */
9
+ function generateRandomChars(count: number): string {
10
+ const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
11
+ let result = ''
12
+ const cryptoApi = globalThis.crypto
13
+ if (!cryptoApi) {
14
+ throw new Error('Crypto API not available')
15
+ }
16
+ const randomValues = cryptoApi.getRandomValues(new Uint8Array(count))
17
+ for (let i = 0; i < count; i++) {
18
+ const randomValue = randomValues[i]
19
+ if (randomValue !== undefined) {
20
+ result += chars[randomValue % chars.length]
21
+ }
22
+ }
23
+ return result
24
+ }
25
+
26
+ /**
27
+ * Replace X's in template with random characters
28
+ */
29
+ function replaceTemplate(template: string): string {
30
+ const xCount = (template.match(/X/g) || []).length
31
+ if (xCount === 0) {
32
+ // If no X's, append random suffix
33
+ return template + '.' + generateRandomChars(6)
34
+ }
35
+
36
+ let result = template
37
+ const randomChars = generateRandomChars(xCount)
38
+ let charIndex = 0
39
+
40
+ for (let i = 0; i < template.length; i++) {
41
+ if (template[i] === 'X') {
42
+ result = result.substring(0, i) + randomChars[charIndex] + result.substring(i + 1)
43
+ charIndex++
44
+ }
45
+ }
46
+
47
+ return result
48
+ }
49
+
50
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
51
+ const usage = `Usage: mktemp [OPTION]... [TEMPLATE]
52
+ Create a temporary file or directory, safely, and print its name.
53
+
54
+ -d, --directory create a directory, not a file
55
+ -q, --quiet suppress error messages
56
+ -u, --dry-run do not create anything; merely print a name (unsafe)
57
+ -p DIR, --tmpdir=DIR interpret TEMPLATE relative to DIR; if DIR is not
58
+ specified, use \$TMPDIR if set, else /tmp. With
59
+ this option, TEMPLATE must not be an absolute name;
60
+ unlike with -t, TEMPLATE may contain slashes, but
61
+ mktemp creates only the final component
62
+ -t interpret TEMPLATE relative to the directory specified by
63
+ -p, or \$TMPDIR if -p is not given; if neither is
64
+ specified, use /tmp [deprecated]
65
+ --help display this help and exit
66
+
67
+ The TEMPLATE must contain at least 3 consecutive 'X's in last component.
68
+ If TEMPLATE is not specified, use tmp.XXXXXX, and --tmpdir implies -t.
69
+
70
+ Examples:
71
+ mktemp create a temp file in /tmp
72
+ mktemp -d create a temp directory in /tmp
73
+ mktemp /tmp/file.XXXXXX create a temp file with template
74
+ mktemp -d /tmp/dir.XXXXXX create a temp directory with template`
75
+ writelnStderr(process, terminal, usage)
76
+ }
77
+
78
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
79
+ return new TerminalCommand({
80
+ command: 'mktemp',
81
+ description: 'Create a temporary file or directory',
82
+ kernel,
83
+ shell,
84
+ terminal,
85
+ run: async (pid: number, argv: string[]) => {
86
+ const process = kernel.processes.get(pid) as Process | undefined
87
+
88
+ if (!process) return 1
89
+
90
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
91
+ printUsage(process, terminal)
92
+ return 0
93
+ }
94
+
95
+ let createDirectory = false
96
+ let quiet = false
97
+ let dryRun = false
98
+ let tmpdir: string | undefined
99
+ let useTmpdir = false
100
+ let template: string | undefined
101
+
102
+ // Parse arguments
103
+ for (let i = 0; i < argv.length; i++) {
104
+ const arg = argv[i]
105
+ if (!arg) continue
106
+
107
+ if (arg === '--help' || arg === '-h') {
108
+ printUsage(process, terminal)
109
+ return 0
110
+ } else if (arg === '-d' || arg === '--directory') {
111
+ createDirectory = true
112
+ } else if (arg === '-q' || arg === '--quiet') {
113
+ quiet = true
114
+ } else if (arg === '-u' || arg === '--dry-run') {
115
+ dryRun = true
116
+ } else if (arg === '-t') {
117
+ useTmpdir = true
118
+ } else if (arg === '-p' || arg === '--tmpdir') {
119
+ useTmpdir = true
120
+ const dirArg = argv[i + 1]
121
+ if (dirArg && !dirArg.startsWith('-')) {
122
+ tmpdir = dirArg
123
+ i++ // Skip the next argument as it's the directory value
124
+ }
125
+ } else if (arg.startsWith('--tmpdir=')) {
126
+ useTmpdir = true
127
+ const dirValue = arg.split('=')[1]
128
+ if (dirValue) {
129
+ tmpdir = dirValue
130
+ }
131
+ } else if (!arg.startsWith('-')) {
132
+ // Positional argument - should be the template
133
+ if (!template) {
134
+ template = arg
135
+ } else {
136
+ if (!quiet) {
137
+ await writelnStderr(process, terminal, `mktemp: too many arguments`)
138
+ }
139
+ return 1
140
+ }
141
+ } else {
142
+ if (!quiet) {
143
+ await writelnStderr(process, terminal, `mktemp: invalid option -- '${arg.replace(/^-+/, '')}'`)
144
+ await writelnStderr(process, terminal, `Try 'mktemp --help' for more information.`)
145
+ }
146
+ return 1
147
+ }
148
+ }
149
+
150
+ // Determine the temp directory
151
+ let baseDir = '/tmp'
152
+ if (useTmpdir) {
153
+ if (tmpdir) {
154
+ baseDir = tmpdir
155
+ } else {
156
+ // Check TMPDIR environment variable
157
+ const envTmpdir = shell.env.get('TMPDIR')
158
+ if (envTmpdir) {
159
+ baseDir = envTmpdir
160
+ }
161
+ }
162
+ }
163
+
164
+ // Resolve base directory
165
+ const resolvedBaseDir = path.isAbsolute(baseDir) ? baseDir : path.resolve(shell.cwd, baseDir)
166
+
167
+ // Determine template
168
+ if (!template) {
169
+ template = 'tmp.XXXXXX'
170
+ }
171
+
172
+ // If template is absolute and useTmpdir is set, that's an error
173
+ if (useTmpdir && path.isAbsolute(template)) {
174
+ if (!quiet) {
175
+ await writelnStderr(process, terminal, `mktemp: with -p/--tmpdir, TEMPLATE must not be an absolute name`)
176
+ }
177
+ return 1
178
+ }
179
+
180
+ // Build the full path
181
+ let fullPath: string
182
+ if (path.isAbsolute(template)) {
183
+ fullPath = template
184
+ } else {
185
+ fullPath = path.join(resolvedBaseDir, template)
186
+ }
187
+
188
+ // Check that template has at least 3 X's in the last component
189
+ const basename = path.basename(fullPath)
190
+ const xCount = (basename.match(/X/g) || []).length
191
+ if (xCount < 3) {
192
+ if (!quiet) {
193
+ await writelnStderr(process, terminal, `mktemp: too few X's in template ${template}`)
194
+ }
195
+ return 1
196
+ }
197
+
198
+ // Replace template with random characters
199
+ const finalPath = replaceTemplate(fullPath)
200
+
201
+ // If dry-run, just print the name
202
+ if (dryRun) {
203
+ await writelnStdout(process, terminal, finalPath)
204
+ return 0
205
+ }
206
+
207
+ // Create the file or directory
208
+ try {
209
+ if (createDirectory) {
210
+ await shell.context.fs.promises.mkdir(finalPath, { recursive: true })
211
+ } else {
212
+ // Create parent directory if needed
213
+ const parentDir = path.dirname(finalPath)
214
+ try {
215
+ await shell.context.fs.promises.mkdir(parentDir, { recursive: true })
216
+ } catch {
217
+ // Parent might already exist, ignore
218
+ }
219
+ // Create empty file
220
+ await shell.context.fs.promises.writeFile(finalPath, '')
221
+ }
222
+
223
+ // Print the created path
224
+ await writelnStdout(process, terminal, finalPath)
225
+ return 0
226
+ } catch (error) {
227
+ if (!quiet) {
228
+ const errorMessage = error instanceof Error ? error.message : String(error)
229
+ await writelnStderr(process, terminal, `mktemp: ${errorMessage}`)
230
+ }
231
+ return 1
232
+ }
233
+ }
234
+ })
235
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
2
2
  import { TerminalEvents } from '@ecmaos/types'
3
3
  import { TerminalCommand } from '../shared/terminal-command.js'
4
- import { writelnStderr, writelnStdout } from '../shared/helpers.js'
4
+ import { writelnStderr } from '../shared/helpers.js'
5
5
 
6
6
  function printUsage(process: Process | undefined, terminal: Terminal): void {
7
7
  const usage = `Usage: nc [OPTIONS] <host> [port]
@@ -19,7 +19,7 @@ Examples:
19
19
  nc -p 443 echo.websocket.org
20
20
  nc -u wss://echo.websocket.org
21
21
  nc -u https://example.com:443`
22
- writelnStdout(process, terminal, usage)
22
+ writelnStderr(process, terminal, usage)
23
23
  }
24
24
 
25
25
  interface ConnectionOptions {
@@ -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
+ }
@@ -13,7 +13,7 @@ Subcommands:
13
13
  remove-all Remove all passkeys
14
14
 
15
15
  --help display this help and exit`
16
- writelnStdout(process, terminal, usage)
16
+ writelnStderr(process, terminal, usage)
17
17
  }
18
18
 
19
19
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
@@ -186,7 +186,7 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
186
186
  case 'remove': {
187
187
  if (!id) {
188
188
  await writelnStderr(process, terminal, chalk.red('Error: --id is required for remove command'))
189
- await writelnStdout(process, terminal, 'Usage: passkey remove --id <id>')
189
+ await writelnStderr(process, terminal, 'Usage: passkey remove --id <id>')
190
190
  return 1
191
191
  }
192
192
 
@@ -223,7 +223,7 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
223
223
 
224
224
  default:
225
225
  await writelnStderr(process, terminal, chalk.red(`Error: Unknown subcommand: ${subcommand}`))
226
- await writelnStdout(process, terminal, 'Run "passkey help" for usage information')
226
+ await writelnStderr(process, terminal, 'Run "passkey help" for usage information')
227
227
  return 1
228
228
  }
229
229
  } catch (error) {
@@ -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
+ }
@@ -1,13 +1,13 @@
1
1
  import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
2
2
  import { TerminalCommand } from '../shared/terminal-command.js'
3
- import { writelnStdout } from '../shared/helpers.js'
3
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js'
4
4
 
5
5
  function printUsage(process: Process | undefined, terminal: Terminal): void {
6
6
  const usage = `Usage: pwd
7
7
  Print the name of the current working directory.
8
8
 
9
9
  --help display this help and exit`
10
- writelnStdout(process, terminal, usage)
10
+ writelnStderr(process, terminal, usage)
11
11
  }
12
12
 
13
13
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {