@ecmaos/coreutils 0.4.0 → 0.4.2

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 (171) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +6 -0
  3. package/dist/commands/awk.d.ts +4 -0
  4. package/dist/commands/awk.d.ts.map +1 -0
  5. package/dist/commands/awk.js +324 -0
  6. package/dist/commands/awk.js.map +1 -0
  7. package/dist/commands/chgrp.d.ts +4 -0
  8. package/dist/commands/chgrp.d.ts.map +1 -0
  9. package/dist/commands/chgrp.js +187 -0
  10. package/dist/commands/chgrp.js.map +1 -0
  11. package/dist/commands/chmod.d.ts.map +1 -1
  12. package/dist/commands/chmod.js +139 -2
  13. package/dist/commands/chmod.js.map +1 -1
  14. package/dist/commands/chown.d.ts +4 -0
  15. package/dist/commands/chown.d.ts.map +1 -0
  16. package/dist/commands/chown.js +257 -0
  17. package/dist/commands/chown.js.map +1 -0
  18. package/dist/commands/cksum.d.ts +4 -0
  19. package/dist/commands/cksum.d.ts.map +1 -0
  20. package/dist/commands/cksum.js +124 -0
  21. package/dist/commands/cksum.js.map +1 -0
  22. package/dist/commands/cmp.d.ts +4 -0
  23. package/dist/commands/cmp.d.ts.map +1 -0
  24. package/dist/commands/cmp.js +120 -0
  25. package/dist/commands/cmp.js.map +1 -0
  26. package/dist/commands/column.d.ts +4 -0
  27. package/dist/commands/column.d.ts.map +1 -0
  28. package/dist/commands/column.js +274 -0
  29. package/dist/commands/column.js.map +1 -0
  30. package/dist/commands/cp.d.ts.map +1 -1
  31. package/dist/commands/cp.js +81 -4
  32. package/dist/commands/cp.js.map +1 -1
  33. package/dist/commands/curl.d.ts +4 -0
  34. package/dist/commands/curl.d.ts.map +1 -0
  35. package/dist/commands/curl.js +238 -0
  36. package/dist/commands/curl.js.map +1 -0
  37. package/dist/commands/du.d.ts +4 -0
  38. package/dist/commands/du.d.ts.map +1 -0
  39. package/dist/commands/du.js +168 -0
  40. package/dist/commands/du.js.map +1 -0
  41. package/dist/commands/echo.d.ts.map +1 -1
  42. package/dist/commands/echo.js +125 -2
  43. package/dist/commands/echo.js.map +1 -1
  44. package/dist/commands/expand.d.ts +4 -0
  45. package/dist/commands/expand.d.ts.map +1 -0
  46. package/dist/commands/expand.js +197 -0
  47. package/dist/commands/expand.js.map +1 -0
  48. package/dist/commands/factor.d.ts +4 -0
  49. package/dist/commands/factor.d.ts.map +1 -0
  50. package/dist/commands/factor.js +141 -0
  51. package/dist/commands/factor.js.map +1 -0
  52. package/dist/commands/fmt.d.ts +4 -0
  53. package/dist/commands/fmt.d.ts.map +1 -0
  54. package/dist/commands/fmt.js +278 -0
  55. package/dist/commands/fmt.js.map +1 -0
  56. package/dist/commands/fold.d.ts +4 -0
  57. package/dist/commands/fold.d.ts.map +1 -0
  58. package/dist/commands/fold.js +253 -0
  59. package/dist/commands/fold.js.map +1 -0
  60. package/dist/commands/groups.d.ts +4 -0
  61. package/dist/commands/groups.d.ts.map +1 -0
  62. package/dist/commands/groups.js +61 -0
  63. package/dist/commands/groups.js.map +1 -0
  64. package/dist/commands/hostname.d.ts +4 -0
  65. package/dist/commands/hostname.d.ts.map +1 -0
  66. package/dist/commands/hostname.js +80 -0
  67. package/dist/commands/hostname.js.map +1 -0
  68. package/dist/commands/mount.d.ts.map +1 -1
  69. package/dist/commands/mount.js +192 -97
  70. package/dist/commands/mount.js.map +1 -1
  71. package/dist/commands/od.d.ts +4 -0
  72. package/dist/commands/od.d.ts.map +1 -0
  73. package/dist/commands/od.js +342 -0
  74. package/dist/commands/od.js.map +1 -0
  75. package/dist/commands/pr.d.ts +4 -0
  76. package/dist/commands/pr.d.ts.map +1 -0
  77. package/dist/commands/pr.js +298 -0
  78. package/dist/commands/pr.js.map +1 -0
  79. package/dist/commands/printf.d.ts +4 -0
  80. package/dist/commands/printf.d.ts.map +1 -0
  81. package/dist/commands/printf.js +271 -0
  82. package/dist/commands/printf.js.map +1 -0
  83. package/dist/commands/readlink.d.ts +4 -0
  84. package/dist/commands/readlink.d.ts.map +1 -0
  85. package/dist/commands/readlink.js +104 -0
  86. package/dist/commands/readlink.js.map +1 -0
  87. package/dist/commands/realpath.d.ts +4 -0
  88. package/dist/commands/realpath.d.ts.map +1 -0
  89. package/dist/commands/realpath.js +111 -0
  90. package/dist/commands/realpath.js.map +1 -0
  91. package/dist/commands/rev.d.ts +4 -0
  92. package/dist/commands/rev.d.ts.map +1 -0
  93. package/dist/commands/rev.js +134 -0
  94. package/dist/commands/rev.js.map +1 -0
  95. package/dist/commands/shuf.d.ts +4 -0
  96. package/dist/commands/shuf.d.ts.map +1 -0
  97. package/dist/commands/shuf.js +221 -0
  98. package/dist/commands/shuf.js.map +1 -0
  99. package/dist/commands/sleep.d.ts +4 -0
  100. package/dist/commands/sleep.d.ts.map +1 -0
  101. package/dist/commands/sleep.js +102 -0
  102. package/dist/commands/sleep.js.map +1 -0
  103. package/dist/commands/strings.d.ts +4 -0
  104. package/dist/commands/strings.d.ts.map +1 -0
  105. package/dist/commands/strings.js +170 -0
  106. package/dist/commands/strings.js.map +1 -0
  107. package/dist/commands/tac.d.ts +4 -0
  108. package/dist/commands/tac.d.ts.map +1 -0
  109. package/dist/commands/tac.js +130 -0
  110. package/dist/commands/tac.js.map +1 -0
  111. package/dist/commands/time.d.ts +4 -0
  112. package/dist/commands/time.d.ts.map +1 -0
  113. package/dist/commands/time.js +126 -0
  114. package/dist/commands/time.js.map +1 -0
  115. package/dist/commands/umount.d.ts.map +1 -1
  116. package/dist/commands/umount.js +2 -3
  117. package/dist/commands/umount.js.map +1 -1
  118. package/dist/commands/uname.d.ts +4 -0
  119. package/dist/commands/uname.d.ts.map +1 -0
  120. package/dist/commands/uname.js +149 -0
  121. package/dist/commands/uname.js.map +1 -0
  122. package/dist/commands/unexpand.d.ts +4 -0
  123. package/dist/commands/unexpand.d.ts.map +1 -0
  124. package/dist/commands/unexpand.js +286 -0
  125. package/dist/commands/unexpand.js.map +1 -0
  126. package/dist/commands/uptime.d.ts +4 -0
  127. package/dist/commands/uptime.d.ts.map +1 -0
  128. package/dist/commands/uptime.js +62 -0
  129. package/dist/commands/uptime.js.map +1 -0
  130. package/dist/commands/yes.d.ts +4 -0
  131. package/dist/commands/yes.d.ts.map +1 -0
  132. package/dist/commands/yes.js +58 -0
  133. package/dist/commands/yes.js.map +1 -0
  134. package/dist/index.d.ts +21 -0
  135. package/dist/index.d.ts.map +1 -1
  136. package/dist/index.js +73 -0
  137. package/dist/index.js.map +1 -1
  138. package/package.json +3 -2
  139. package/src/commands/awk.ts +340 -0
  140. package/src/commands/chmod.ts +141 -2
  141. package/src/commands/chown.ts +321 -0
  142. package/src/commands/cksum.ts +133 -0
  143. package/src/commands/cmp.ts +126 -0
  144. package/src/commands/column.ts +273 -0
  145. package/src/commands/cp.ts +93 -4
  146. package/src/commands/curl.ts +231 -0
  147. package/src/commands/echo.ts +122 -2
  148. package/src/commands/expand.ts +207 -0
  149. package/src/commands/factor.ts +151 -0
  150. package/src/commands/fmt.ts +293 -0
  151. package/src/commands/fold.ts +257 -0
  152. package/src/commands/groups.ts +72 -0
  153. package/src/commands/hostname.ts +81 -0
  154. package/src/commands/mount.ts +208 -99
  155. package/src/commands/od.ts +327 -0
  156. package/src/commands/pr.ts +291 -0
  157. package/src/commands/printf.ts +271 -0
  158. package/src/commands/readlink.ts +102 -0
  159. package/src/commands/realpath.ts +126 -0
  160. package/src/commands/rev.ts +143 -0
  161. package/src/commands/shuf.ts +218 -0
  162. package/src/commands/sleep.ts +109 -0
  163. package/src/commands/strings.ts +176 -0
  164. package/src/commands/tac.ts +138 -0
  165. package/src/commands/time.ts +144 -0
  166. package/src/commands/umount.ts +2 -3
  167. package/src/commands/uname.ts +130 -0
  168. package/src/commands/unexpand.ts +305 -0
  169. package/src/commands/uptime.ts +73 -0
  170. package/src/index.ts +73 -0
  171. package/tsconfig.json +4 -0
@@ -0,0 +1,273 @@
1
+ import path from 'path'
2
+ import columnify from 'columnify'
3
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
4
+ import { TerminalEvents } from '@ecmaos/types'
5
+ import { TerminalCommand } from '../shared/terminal-command.js'
6
+ import { writelnStderr } from '../shared/helpers.js'
7
+
8
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
9
+ const usage = `Usage: column [OPTION]... [FILE]...
10
+ Format input into columns.
11
+
12
+ -t, --table create a table
13
+ -s, --separator=SEP specify column separator (default: whitespace)
14
+ -c, --columns=COLS specify number of columns
15
+ --help display this help and exit`
16
+ writelnStderr(process, terminal, usage)
17
+ }
18
+
19
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
20
+ return new TerminalCommand({
21
+ command: 'column',
22
+ description: 'Format input into columns',
23
+ kernel,
24
+ shell,
25
+ terminal,
26
+ run: async (pid: number, argv: string[]) => {
27
+ const process = kernel.processes.get(pid) as Process | undefined
28
+
29
+ if (!process) return 1
30
+
31
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
32
+ printUsage(process, terminal)
33
+ return 0
34
+ }
35
+
36
+ let table = false
37
+ let separator: string | undefined
38
+ let columns: number | undefined
39
+ const files: string[] = []
40
+
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const arg = argv[i]
43
+ if (!arg) continue
44
+
45
+ if (arg === '--help' || arg === '-h') {
46
+ printUsage(process, terminal)
47
+ return 0
48
+ } else if (arg === '-t' || arg === '--table') {
49
+ table = true
50
+ } else if (arg === '-s' || arg === '--separator') {
51
+ if (i + 1 < argv.length) {
52
+ separator = argv[++i]
53
+ }
54
+ } else if (arg.startsWith('--separator=')) {
55
+ separator = arg.slice(12)
56
+ } else if (arg.startsWith('-s')) {
57
+ separator = arg.slice(2) || undefined
58
+ } else if (arg === '-c' || arg === '--columns') {
59
+ if (i + 1 < argv.length) {
60
+ const colsStr = argv[++i]
61
+ if (colsStr !== undefined) {
62
+ const parsed = parseInt(colsStr, 10)
63
+ if (!isNaN(parsed) && parsed > 0) {
64
+ columns = parsed
65
+ } else {
66
+ await writelnStderr(process, terminal, `column: invalid column count: ${colsStr}`)
67
+ return 1
68
+ }
69
+ }
70
+ }
71
+ } else if (arg.startsWith('--columns=')) {
72
+ const colsStr = arg.slice(11)
73
+ const parsed = parseInt(colsStr, 10)
74
+ if (!isNaN(parsed) && parsed > 0) {
75
+ columns = parsed
76
+ } else {
77
+ await writelnStderr(process, terminal, `column: invalid column count: ${colsStr}`)
78
+ return 1
79
+ }
80
+ } else if (arg.startsWith('-c')) {
81
+ const colsStr = arg.slice(2)
82
+ if (colsStr) {
83
+ const parsed = parseInt(colsStr, 10)
84
+ if (!isNaN(parsed) && parsed > 0) {
85
+ columns = parsed
86
+ } else {
87
+ await writelnStderr(process, terminal, `column: invalid column count: ${colsStr}`)
88
+ return 1
89
+ }
90
+ }
91
+ } else if (!arg.startsWith('-')) {
92
+ files.push(arg)
93
+ } else {
94
+ await writelnStderr(process, terminal, `column: invalid option -- '${arg.slice(1)}'`)
95
+ await writelnStderr(process, terminal, "Try 'column --help' for more information.")
96
+ return 1
97
+ }
98
+ }
99
+
100
+ const writer = process.stdout.getWriter()
101
+
102
+ try {
103
+ let lines: string[] = []
104
+
105
+ if (files.length === 0) {
106
+ if (!process.stdin) {
107
+ return 0
108
+ }
109
+
110
+ const reader = process.stdin.getReader()
111
+ const decoder = new TextDecoder()
112
+ let buffer = ''
113
+
114
+ try {
115
+ while (true) {
116
+ const { done, value } = await reader.read()
117
+ if (done) break
118
+ if (value) {
119
+ buffer += decoder.decode(value, { stream: true })
120
+ const newLines = buffer.split('\n')
121
+ buffer = newLines.pop() || ''
122
+ lines.push(...newLines)
123
+ }
124
+ }
125
+ if (buffer) {
126
+ lines.push(buffer)
127
+ }
128
+ } finally {
129
+ reader.releaseLock()
130
+ }
131
+ } else {
132
+ for (const file of files) {
133
+ const fullPath = path.resolve(shell.cwd, file)
134
+
135
+ let interrupted = false
136
+ const interruptHandler = () => { interrupted = true }
137
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
138
+
139
+ try {
140
+ if (fullPath.startsWith('/dev')) {
141
+ await writelnStderr(process, terminal, `column: ${file}: cannot process device files`)
142
+ continue
143
+ }
144
+
145
+ const handle = await shell.context.fs.promises.open(fullPath, 'r')
146
+ const stat = await shell.context.fs.promises.stat(fullPath)
147
+
148
+ const decoder = new TextDecoder()
149
+ let content = ''
150
+ let bytesRead = 0
151
+ const chunkSize = 1024
152
+
153
+ while (bytesRead < stat.size) {
154
+ if (interrupted) break
155
+ const data = new Uint8Array(chunkSize)
156
+ const readSize = Math.min(chunkSize, stat.size - bytesRead)
157
+ await handle.read(data, 0, readSize, bytesRead)
158
+ const chunk = data.subarray(0, readSize)
159
+ content += decoder.decode(chunk, { stream: true })
160
+ bytesRead += readSize
161
+ }
162
+
163
+ const fileLines = content.split('\n')
164
+ if (fileLines[fileLines.length - 1] === '') {
165
+ fileLines.pop()
166
+ }
167
+ lines.push(...fileLines)
168
+ } catch (error) {
169
+ await writelnStderr(process, terminal, `column: ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`)
170
+ } finally {
171
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
172
+ }
173
+ }
174
+ }
175
+
176
+ if (table && separator) {
177
+ const data: Array<Record<string, string>> = []
178
+ const headers = new Set<string>()
179
+
180
+ for (const line of lines) {
181
+ if (!line.trim()) continue
182
+ const parts = line.split(separator)
183
+ const row: Record<string, string> = {}
184
+ parts.forEach((part, idx) => {
185
+ const header = `col${idx + 1}`
186
+ headers.add(header)
187
+ row[header] = part.trim()
188
+ })
189
+ data.push(row)
190
+ }
191
+
192
+ if (data.length > 0) {
193
+ const tableOutput = columnify(data, {
194
+ columns: Array.from(headers),
195
+ columnSplitter: ' ',
196
+ showHeaders: true
197
+ })
198
+ await writer.write(new TextEncoder().encode(tableOutput))
199
+ }
200
+ } else if (table) {
201
+ const data: Array<Record<string, string>> = []
202
+ const headers = new Set<string>()
203
+
204
+ for (const line of lines) {
205
+ if (!line.trim()) continue
206
+ const parts = line.trim().split(/\s+/)
207
+ const row: Record<string, string> = {}
208
+ parts.forEach((part, idx) => {
209
+ const header = `col${idx + 1}`
210
+ headers.add(header)
211
+ row[header] = part
212
+ })
213
+ data.push(row)
214
+ }
215
+
216
+ if (data.length > 0) {
217
+ const tableOutput = columnify(data, {
218
+ columns: Array.from(headers),
219
+ columnSplitter: ' ',
220
+ showHeaders: true
221
+ })
222
+ await writer.write(new TextEncoder().encode(tableOutput))
223
+ }
224
+ } else {
225
+ const words: string[] = []
226
+ for (const line of lines) {
227
+ if (separator) {
228
+ words.push(...line.split(separator).map(w => w.trim()).filter(w => w))
229
+ } else {
230
+ words.push(...line.trim().split(/\s+/).filter(w => w))
231
+ }
232
+ }
233
+
234
+ if (columns && columns > 0) {
235
+ const rows: string[][] = []
236
+ for (let i = 0; i < words.length; i += columns) {
237
+ rows.push(words.slice(i, i + columns))
238
+ }
239
+
240
+ const data: Array<Record<string, string>> = []
241
+ for (const row of rows) {
242
+ const rowObj: Record<string, string> = {}
243
+ for (let i = 0; i < columns; i++) {
244
+ rowObj[`col${i + 1}`] = row[i] || ''
245
+ }
246
+ data.push(rowObj)
247
+ }
248
+
249
+ if (data.length > 0) {
250
+ const tableOutput = columnify(data, {
251
+ columns: Array.from({ length: columns }, (_, i) => `col${i + 1}`),
252
+ columnSplitter: ' ',
253
+ showHeaders: false
254
+ })
255
+ await writer.write(new TextEncoder().encode(tableOutput))
256
+ }
257
+ } else {
258
+ for (const word of words) {
259
+ await writer.write(new TextEncoder().encode(word + '\n'))
260
+ }
261
+ }
262
+ }
263
+
264
+ return 0
265
+ } catch (error) {
266
+ await writelnStderr(process, terminal, `column: ${error instanceof Error ? error.message : 'Unknown error'}`)
267
+ return 1
268
+ } finally {
269
+ writer.releaseLock()
270
+ }
271
+ }
272
+ })
273
+ }
@@ -1,16 +1,52 @@
1
1
  import path from 'path'
2
2
  import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
3
3
  import { TerminalCommand } from '../shared/terminal-command.js'
4
- import { writelnStderr } from '../shared/helpers.js'
4
+ import { writelnStderr, writelnStdout } from '../shared/helpers.js'
5
5
 
6
6
  function printUsage(process: Process | undefined, terminal: Terminal): void {
7
7
  const usage = `Usage: cp [OPTION]... SOURCE... DEST
8
8
  Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.
9
9
 
10
- --help display this help and exit`
10
+ -r, -R, --recursive copy directories recursively
11
+ -v, --verbose explain what is being done
12
+ --help display this help and exit`
11
13
  writelnStderr(process, terminal, usage)
12
14
  }
13
15
 
16
+ async function copyRecursive(
17
+ fs: typeof import('@zenfs/core').fs.promises,
18
+ sourcePath: string,
19
+ destPath: string,
20
+ verbose: boolean,
21
+ process: Process | undefined,
22
+ terminal: Terminal,
23
+ relativeSource: string,
24
+ relativeDest: string
25
+ ): Promise<void> {
26
+ const stats = await fs.stat(sourcePath)
27
+
28
+ if (stats.isDirectory()) {
29
+ try {
30
+ await fs.mkdir(destPath)
31
+ if (verbose) await writelnStdout(process, terminal, `'${relativeSource}' -> '${relativeDest}'`)
32
+ } catch (error) {
33
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
34
+ }
35
+
36
+ const entries = await fs.readdir(sourcePath)
37
+ for (const entry of entries) {
38
+ const srcEntry = path.join(sourcePath, entry)
39
+ const destEntry = path.join(destPath, entry)
40
+ const srcRelative = path.join(relativeSource, entry)
41
+ const destRelative = path.join(relativeDest, entry)
42
+ await copyRecursive(fs, srcEntry, destEntry, verbose, process, terminal, srcRelative, destRelative)
43
+ }
44
+ } else {
45
+ await fs.copyFile(sourcePath, destPath)
46
+ if (verbose) await writelnStdout(process, terminal, `'${relativeSource}' -> '${relativeDest}'`)
47
+ }
48
+ }
49
+
14
50
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
15
51
  return new TerminalCommand({
16
52
  command: 'cp',
@@ -26,9 +62,31 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
26
62
  return 0
27
63
  }
28
64
 
65
+ let recursive = false
66
+ let verbose = false
29
67
  const args: string[] = []
68
+
30
69
  for (const arg of argv) {
31
- if (arg && !arg.startsWith('-')) {
70
+ if (arg.startsWith('-') && arg !== '--') {
71
+ if (arg === '--recursive') {
72
+ recursive = true
73
+ } else if (arg === '--verbose') {
74
+ verbose = true
75
+ } else if (arg.length > 1) {
76
+ for (let i = 1; i < arg.length; i++) {
77
+ const flag = arg[i]
78
+ if (flag === 'r' || flag === 'R') {
79
+ recursive = true
80
+ } else if (flag === 'v') {
81
+ verbose = true
82
+ } else {
83
+ await writelnStderr(process, terminal, `cp: invalid option -- '${flag}'`)
84
+ await writelnStderr(process, terminal, "Try 'cp --help' for more information.")
85
+ return 1
86
+ }
87
+ }
88
+ }
89
+ } else if (arg && !arg.startsWith('-')) {
32
90
  args.push(arg)
33
91
  }
34
92
  }
@@ -67,7 +125,38 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
67
125
  : path.resolve(shell.cwd, destination)
68
126
 
69
127
  try {
70
- await shell.context.fs.promises.copyFile(sourcePath, finalDestination)
128
+ const sourceStats = await shell.context.fs.promises.stat(sourcePath)
129
+
130
+ if (sourceStats.isDirectory()) {
131
+ if (!recursive) {
132
+ await writelnStderr(process, terminal, `cp: -r not specified; omitting directory '${source}'`)
133
+ hasError = true
134
+ continue
135
+ }
136
+ const relativeSource = source
137
+ const relativeDest = isDestinationDir
138
+ ? path.join(destination, path.basename(source))
139
+ : destination
140
+ await copyRecursive(
141
+ shell.context.fs.promises,
142
+ sourcePath,
143
+ finalDestination,
144
+ verbose,
145
+ process,
146
+ terminal,
147
+ relativeSource,
148
+ relativeDest
149
+ )
150
+ } else {
151
+ await shell.context.fs.promises.copyFile(sourcePath, finalDestination)
152
+ if (verbose) {
153
+ const relativeSource = source
154
+ const relativeDest = isDestinationDir
155
+ ? path.join(destination, path.basename(source))
156
+ : destination
157
+ await writelnStdout(process, terminal, `'${relativeSource}' -> '${relativeDest}'`)
158
+ }
159
+ }
71
160
  } catch (error) {
72
161
  const errorMessage = error instanceof Error ? error.message : String(error)
73
162
  await writelnStderr(process, terminal, `cp: ${source}: ${errorMessage}`)
@@ -0,0 +1,231 @@
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: curl [OPTION]... URL
9
+ Transfer data from or to a server.
10
+
11
+ -o, --output=FILE write output to FILE instead of stdout
12
+ -O, --remote-name write output to a file named like the remote file
13
+ -X, --request=METHOD HTTP method to use (default: GET)
14
+ -d, --data=DATA send data in POST request
15
+ -H, --header=HEADER add custom HTTP header (format: "Name: Value")
16
+ -s, --silent silent mode (don't show progress)
17
+ -v, --verbose verbose mode (show request/response headers)
18
+ --help display this help and exit`
19
+ writelnStderr(process, terminal, usage)
20
+ }
21
+
22
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
23
+ return new TerminalCommand({
24
+ command: 'curl',
25
+ description: 'Transfer data from or to a server',
26
+ kernel,
27
+ shell,
28
+ terminal,
29
+ run: async (pid: number, argv: string[]) => {
30
+ const process = kernel.processes.get(pid) as Process | undefined
31
+
32
+ if (!process) return 1
33
+
34
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
35
+ printUsage(process, terminal)
36
+ return 0
37
+ }
38
+
39
+ let url: string | undefined
40
+ let outputFile: string | undefined
41
+ let remoteName = false
42
+ let method = 'GET'
43
+ let body: string | undefined
44
+ const headers: Record<string, string> = {}
45
+ let silent = false
46
+ let verbose = false
47
+
48
+ for (let i = 0; i < argv.length; i++) {
49
+ const arg = argv[i]
50
+ if (!arg) continue
51
+
52
+ if (arg === '--help' || arg === '-h') {
53
+ printUsage(process, terminal)
54
+ return 0
55
+ } else if (arg === '-o' || arg === '--output') {
56
+ if (i + 1 < argv.length) {
57
+ outputFile = argv[++i]
58
+ }
59
+ } else if (arg.startsWith('--output=')) {
60
+ outputFile = arg.slice(9)
61
+ } else if (arg.startsWith('-o')) {
62
+ outputFile = arg.slice(2) || undefined
63
+ } else if (arg === '-O' || arg === '--remote-name') {
64
+ remoteName = true
65
+ } else if (arg === '-X' || arg === '--request') {
66
+ if (i + 1 < argv.length) {
67
+ method = (argv[++i] || 'GET').toUpperCase()
68
+ }
69
+ } else if (arg.startsWith('--request=')) {
70
+ method = arg.slice(10).toUpperCase()
71
+ } else if (arg.startsWith('-X')) {
72
+ method = (arg.slice(2) || 'GET').toUpperCase()
73
+ } else if (arg === '-d' || arg === '--data') {
74
+ if (i + 1 < argv.length) {
75
+ body = argv[++i]
76
+ }
77
+ } else if (arg.startsWith('--data=')) {
78
+ body = arg.slice(7)
79
+ } else if (arg.startsWith('-d')) {
80
+ body = arg.slice(2) || undefined
81
+ } else if (arg === '-H' || arg === '--header') {
82
+ if (i + 1 < argv.length) {
83
+ const headerArg = argv[++i]
84
+ if (headerArg) {
85
+ const [name, ...valueParts] = headerArg.split(':')
86
+ if (name && valueParts.length > 0) {
87
+ headers[name.trim()] = valueParts.join(':').trim()
88
+ }
89
+ }
90
+ }
91
+ } else if (arg.startsWith('--header=')) {
92
+ const headerValue = arg.slice(9)
93
+ const [name, ...valueParts] = headerValue.split(':')
94
+ if (name && valueParts.length > 0) {
95
+ headers[name.trim()] = valueParts.join(':').trim()
96
+ }
97
+ } else if (arg.startsWith('-H')) {
98
+ const headerValue = arg.slice(2)
99
+ if (headerValue) {
100
+ const [name, ...valueParts] = headerValue.split(':')
101
+ if (name && valueParts.length > 0) {
102
+ headers[name.trim()] = valueParts.join(':').trim()
103
+ }
104
+ }
105
+ } else if (arg === '-s' || arg === '--silent') {
106
+ silent = true
107
+ } else if (arg === '-v' || arg === '--verbose') {
108
+ verbose = true
109
+ } else if (arg.startsWith('-')) {
110
+ const flags = arg.slice(1).split('')
111
+ if (flags.includes('s')) silent = true
112
+ if (flags.includes('v')) verbose = true
113
+ if (flags.includes('O')) remoteName = true
114
+ const invalidFlags = flags.filter(f => !['s', 'v', 'O'].includes(f))
115
+ if (invalidFlags.length > 0) {
116
+ await writelnStderr(process, terminal, `curl: invalid option -- '${invalidFlags[0]}'`)
117
+ await writelnStderr(process, terminal, "Try 'curl --help' for more information.")
118
+ return 1
119
+ }
120
+ } else {
121
+ if (!url) {
122
+ url = arg
123
+ } else {
124
+ await writelnStderr(process, terminal, `curl: unexpected argument: ${arg}`)
125
+ return 1
126
+ }
127
+ }
128
+ }
129
+
130
+ if (!url) {
131
+ await writelnStderr(process, terminal, 'curl: URL is required')
132
+ await writelnStderr(process, terminal, "Try 'curl --help' for more information.")
133
+ return 1
134
+ }
135
+
136
+ if (remoteName && !outputFile) {
137
+ const urlObj = new URL(url)
138
+ const pathname = urlObj.pathname
139
+ outputFile = path.basename(pathname) || 'index.html'
140
+ }
141
+
142
+ try {
143
+ if (verbose && !silent) {
144
+ await writelnStderr(process, terminal, `* Connecting to ${url}`)
145
+ await writelnStderr(process, terminal, `> ${method} ${url} HTTP/1.1`)
146
+ }
147
+
148
+ const fetchOptions: RequestInit = { method }
149
+ if (body) {
150
+ fetchOptions.body = body
151
+ if (!headers['Content-Type']) {
152
+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
153
+ }
154
+ }
155
+ if (Object.keys(headers).length > 0) {
156
+ fetchOptions.headers = headers
157
+ }
158
+
159
+ if (verbose && !silent) {
160
+ for (const [name, value] of Object.entries(headers)) {
161
+ await writelnStderr(process, terminal, `> ${name}: ${value}`)
162
+ }
163
+ }
164
+
165
+ const response = await globalThis.fetch(url, fetchOptions)
166
+
167
+ if (verbose && !silent) {
168
+ await writelnStderr(process, terminal, `< HTTP/${response.status} ${response.status} ${response.statusText}`)
169
+ for (const [name, value] of response.headers.entries()) {
170
+ await writelnStderr(process, terminal, `< ${name}: ${value}`)
171
+ }
172
+ }
173
+
174
+ if (!response.ok && !silent) {
175
+ await writelnStderr(process, terminal, chalk.red(`curl: HTTP error! status: ${response.status}`))
176
+ }
177
+
178
+ const reader = response.body?.getReader()
179
+ if (!reader) {
180
+ if (!silent) {
181
+ await writelnStderr(process, terminal, chalk.red('curl: No response body'))
182
+ }
183
+ return response.ok ? 0 : 1
184
+ }
185
+
186
+ let writer: WritableStreamDefaultWriter<Uint8Array> | { write: (chunk: Uint8Array) => Promise<void>, releaseLock: () => Promise<void> } | undefined
187
+
188
+ if (outputFile) {
189
+ const fullPath = path.resolve(shell.cwd, outputFile)
190
+ const fileHandle = await shell.context.fs.promises.open(fullPath, 'w')
191
+ writer = {
192
+ write: async (chunk: Uint8Array) => {
193
+ await fileHandle.write(chunk)
194
+ },
195
+ releaseLock: async () => {
196
+ await fileHandle.close()
197
+ }
198
+ }
199
+ } else {
200
+ if (!process.stdout) {
201
+ await writelnStderr(process, terminal, chalk.red('curl: No stdout available'))
202
+ return 1
203
+ }
204
+ writer = process.stdout.getWriter()
205
+ }
206
+
207
+ try {
208
+ while (true) {
209
+ const { done, value } = await reader.read()
210
+ if (done) break
211
+ if (value && value.length > 0) {
212
+ await writer.write(value)
213
+ }
214
+ }
215
+ } finally {
216
+ reader.releaseLock()
217
+ if (writer && 'releaseLock' in writer) {
218
+ await writer.releaseLock()
219
+ }
220
+ }
221
+
222
+ return response.ok ? 0 : 1
223
+ } catch (error) {
224
+ if (!silent) {
225
+ await writelnStderr(process, terminal, chalk.red(`curl: ${error instanceof Error ? error.message : 'Unknown error'}`))
226
+ }
227
+ return 1
228
+ }
229
+ }
230
+ })
231
+ }