@ecmaos/coreutils 0.3.1 → 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 (199) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +48 -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/cron.d.ts.map +1 -1
  34. package/dist/commands/cron.js +116 -23
  35. package/dist/commands/cron.js.map +1 -1
  36. package/dist/commands/curl.d.ts +4 -0
  37. package/dist/commands/curl.d.ts.map +1 -0
  38. package/dist/commands/curl.js +238 -0
  39. package/dist/commands/curl.js.map +1 -0
  40. package/dist/commands/du.d.ts +4 -0
  41. package/dist/commands/du.d.ts.map +1 -0
  42. package/dist/commands/du.js +168 -0
  43. package/dist/commands/du.js.map +1 -0
  44. package/dist/commands/echo.d.ts.map +1 -1
  45. package/dist/commands/echo.js +125 -2
  46. package/dist/commands/echo.js.map +1 -1
  47. package/dist/commands/env.d.ts +4 -0
  48. package/dist/commands/env.d.ts.map +1 -0
  49. package/dist/commands/env.js +129 -0
  50. package/dist/commands/env.js.map +1 -0
  51. package/dist/commands/expand.d.ts +4 -0
  52. package/dist/commands/expand.d.ts.map +1 -0
  53. package/dist/commands/expand.js +197 -0
  54. package/dist/commands/expand.js.map +1 -0
  55. package/dist/commands/factor.d.ts +4 -0
  56. package/dist/commands/factor.d.ts.map +1 -0
  57. package/dist/commands/factor.js +141 -0
  58. package/dist/commands/factor.js.map +1 -0
  59. package/dist/commands/fmt.d.ts +4 -0
  60. package/dist/commands/fmt.d.ts.map +1 -0
  61. package/dist/commands/fmt.js +278 -0
  62. package/dist/commands/fmt.js.map +1 -0
  63. package/dist/commands/fold.d.ts +4 -0
  64. package/dist/commands/fold.d.ts.map +1 -0
  65. package/dist/commands/fold.js +253 -0
  66. package/dist/commands/fold.js.map +1 -0
  67. package/dist/commands/groups.d.ts +4 -0
  68. package/dist/commands/groups.d.ts.map +1 -0
  69. package/dist/commands/groups.js +61 -0
  70. package/dist/commands/groups.js.map +1 -0
  71. package/dist/commands/head.d.ts.map +1 -1
  72. package/dist/commands/head.js +184 -77
  73. package/dist/commands/head.js.map +1 -1
  74. package/dist/commands/hostname.d.ts +4 -0
  75. package/dist/commands/hostname.d.ts.map +1 -0
  76. package/dist/commands/hostname.js +80 -0
  77. package/dist/commands/hostname.js.map +1 -0
  78. package/dist/commands/less.d.ts.map +1 -1
  79. package/dist/commands/less.js +1 -0
  80. package/dist/commands/less.js.map +1 -1
  81. package/dist/commands/man.d.ts.map +1 -1
  82. package/dist/commands/man.js +3 -1
  83. package/dist/commands/man.js.map +1 -1
  84. package/dist/commands/mount.d.ts +4 -0
  85. package/dist/commands/mount.d.ts.map +1 -0
  86. package/dist/commands/mount.js +1136 -0
  87. package/dist/commands/mount.js.map +1 -0
  88. package/dist/commands/od.d.ts +4 -0
  89. package/dist/commands/od.d.ts.map +1 -0
  90. package/dist/commands/od.js +342 -0
  91. package/dist/commands/od.js.map +1 -0
  92. package/dist/commands/pr.d.ts +4 -0
  93. package/dist/commands/pr.d.ts.map +1 -0
  94. package/dist/commands/pr.js +298 -0
  95. package/dist/commands/pr.js.map +1 -0
  96. package/dist/commands/printf.d.ts +4 -0
  97. package/dist/commands/printf.d.ts.map +1 -0
  98. package/dist/commands/printf.js +271 -0
  99. package/dist/commands/printf.js.map +1 -0
  100. package/dist/commands/readlink.d.ts +4 -0
  101. package/dist/commands/readlink.d.ts.map +1 -0
  102. package/dist/commands/readlink.js +104 -0
  103. package/dist/commands/readlink.js.map +1 -0
  104. package/dist/commands/realpath.d.ts +4 -0
  105. package/dist/commands/realpath.d.ts.map +1 -0
  106. package/dist/commands/realpath.js +111 -0
  107. package/dist/commands/realpath.js.map +1 -0
  108. package/dist/commands/rev.d.ts +4 -0
  109. package/dist/commands/rev.d.ts.map +1 -0
  110. package/dist/commands/rev.js +134 -0
  111. package/dist/commands/rev.js.map +1 -0
  112. package/dist/commands/shuf.d.ts +4 -0
  113. package/dist/commands/shuf.d.ts.map +1 -0
  114. package/dist/commands/shuf.js +221 -0
  115. package/dist/commands/shuf.js.map +1 -0
  116. package/dist/commands/sleep.d.ts +4 -0
  117. package/dist/commands/sleep.d.ts.map +1 -0
  118. package/dist/commands/sleep.js +102 -0
  119. package/dist/commands/sleep.js.map +1 -0
  120. package/dist/commands/strings.d.ts +4 -0
  121. package/dist/commands/strings.d.ts.map +1 -0
  122. package/dist/commands/strings.js +170 -0
  123. package/dist/commands/strings.js.map +1 -0
  124. package/dist/commands/tac.d.ts +4 -0
  125. package/dist/commands/tac.d.ts.map +1 -0
  126. package/dist/commands/tac.js +130 -0
  127. package/dist/commands/tac.js.map +1 -0
  128. package/dist/commands/time.d.ts +4 -0
  129. package/dist/commands/time.d.ts.map +1 -0
  130. package/dist/commands/time.js +126 -0
  131. package/dist/commands/time.js.map +1 -0
  132. package/dist/commands/umount.d.ts +4 -0
  133. package/dist/commands/umount.d.ts.map +1 -0
  134. package/dist/commands/umount.js +103 -0
  135. package/dist/commands/umount.js.map +1 -0
  136. package/dist/commands/uname.d.ts +4 -0
  137. package/dist/commands/uname.d.ts.map +1 -0
  138. package/dist/commands/uname.js +149 -0
  139. package/dist/commands/uname.js.map +1 -0
  140. package/dist/commands/unexpand.d.ts +4 -0
  141. package/dist/commands/unexpand.d.ts.map +1 -0
  142. package/dist/commands/unexpand.js +286 -0
  143. package/dist/commands/unexpand.js.map +1 -0
  144. package/dist/commands/uptime.d.ts +4 -0
  145. package/dist/commands/uptime.d.ts.map +1 -0
  146. package/dist/commands/uptime.js +62 -0
  147. package/dist/commands/uptime.js.map +1 -0
  148. package/dist/commands/view.d.ts +1 -0
  149. package/dist/commands/view.d.ts.map +1 -1
  150. package/dist/commands/view.js +408 -66
  151. package/dist/commands/view.js.map +1 -1
  152. package/dist/commands/yes.d.ts +4 -0
  153. package/dist/commands/yes.d.ts.map +1 -0
  154. package/dist/commands/yes.js +58 -0
  155. package/dist/commands/yes.js.map +1 -0
  156. package/dist/index.d.ts +24 -0
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +82 -0
  159. package/dist/index.js.map +1 -1
  160. package/package.json +12 -3
  161. package/src/commands/awk.ts +340 -0
  162. package/src/commands/chmod.ts +141 -2
  163. package/src/commands/chown.ts +321 -0
  164. package/src/commands/cksum.ts +133 -0
  165. package/src/commands/cmp.ts +126 -0
  166. package/src/commands/column.ts +273 -0
  167. package/src/commands/cp.ts +93 -4
  168. package/src/commands/cron.ts +115 -23
  169. package/src/commands/curl.ts +231 -0
  170. package/src/commands/echo.ts +122 -2
  171. package/src/commands/env.ts +143 -0
  172. package/src/commands/expand.ts +207 -0
  173. package/src/commands/factor.ts +151 -0
  174. package/src/commands/fmt.ts +293 -0
  175. package/src/commands/fold.ts +257 -0
  176. package/src/commands/groups.ts +72 -0
  177. package/src/commands/head.ts +176 -77
  178. package/src/commands/hostname.ts +81 -0
  179. package/src/commands/less.ts +1 -0
  180. package/src/commands/man.ts +4 -1
  181. package/src/commands/mount.ts +1302 -0
  182. package/src/commands/od.ts +327 -0
  183. package/src/commands/pr.ts +291 -0
  184. package/src/commands/printf.ts +271 -0
  185. package/src/commands/readlink.ts +102 -0
  186. package/src/commands/realpath.ts +126 -0
  187. package/src/commands/rev.ts +143 -0
  188. package/src/commands/shuf.ts +218 -0
  189. package/src/commands/sleep.ts +109 -0
  190. package/src/commands/strings.ts +176 -0
  191. package/src/commands/tac.ts +138 -0
  192. package/src/commands/time.ts +144 -0
  193. package/src/commands/umount.ts +116 -0
  194. package/src/commands/uname.ts +130 -0
  195. package/src/commands/unexpand.ts +305 -0
  196. package/src/commands/uptime.ts +73 -0
  197. package/src/commands/view.ts +463 -73
  198. package/src/index.ts +82 -0
  199. 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}`)
@@ -3,6 +3,7 @@ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
3
3
  import { TerminalCommand } from '../shared/terminal-command.js'
4
4
  import { writelnStdout, writelnStderr } from '../shared/helpers.js'
5
5
  import { parseCronExpression } from 'cron-schedule'
6
+ import cronstrue from 'cronstrue'
6
7
 
7
8
  interface CrontabEntry {
8
9
  expression: string
@@ -37,44 +38,46 @@ function parseCrontabLine(line: string): { expression: string, command: string }
37
38
  let expression: string
38
39
  let command: string
39
40
 
40
- // If we have exactly 6 parts, it must be 5-field format (5 cron fields + 1 command word)
41
+ // If we have exactly 6 parts, assume 5-field format (5 cron fields + 1 command word)
42
+ // This is the most common case
41
43
  if (parts.length === 6) {
42
44
  // 5-field format: minute hour day month weekday command
43
45
  expression = parts.slice(0, 5).join(' ')
44
46
  command = parts[5] || ''
45
47
  } else {
46
48
  // 7+ parts: could be 5-field with multi-word command or 6-field format
47
- // Try parsing as 5-field first (more common), then 6-field if that fails
49
+ // Try 6-field format FIRST (if user wrote 6 fields, they probably meant 6 fields)
50
+ // Then fall back to 5-field if 6-field is invalid
48
51
  const potential5Field = parts.slice(0, 5).join(' ')
49
52
  const potential6Field = parts.slice(0, 6).join(' ')
50
53
 
51
54
  let valid5Field = false
52
55
  let valid6Field = false
53
56
 
54
- // Try to validate 5-field format
57
+ // Try to validate 6-field format first
55
58
  try {
56
- parseCronExpression(potential5Field)
57
- valid5Field = true
59
+ parseCronExpression(potential6Field)
60
+ valid6Field = true
58
61
  } catch {
59
- // Not a valid 5-field expression
62
+ // Not a valid 6-field expression
60
63
  }
61
64
 
62
- // Try to validate 6-field format
65
+ // Try to validate 5-field format
63
66
  try {
64
- parseCronExpression(potential6Field)
65
- valid6Field = true
67
+ parseCronExpression(potential5Field)
68
+ valid5Field = true
66
69
  } catch {
67
- // Not a valid 6-field expression
70
+ // Not a valid 5-field expression
68
71
  }
69
72
 
70
- // Prefer 5-field format if both are valid (more common)
71
- // Only use 6-field if 5-field is invalid but 6-field is valid
72
- if (valid5Field) {
73
- expression = potential5Field
74
- command = parts.slice(5).join(' ')
75
- } else if (valid6Field) {
73
+ // Prefer 6-field format if valid (user wrote 6 fields, so use them)
74
+ // Only use 5-field if 6-field is invalid
75
+ if (valid6Field) {
76
76
  expression = potential6Field
77
77
  command = parts.slice(6).join(' ')
78
+ } else if (valid5Field) {
79
+ expression = potential5Field
80
+ command = parts.slice(5).join(' ')
78
81
  } else {
79
82
  // Neither format is valid, return null
80
83
  return null
@@ -118,11 +121,30 @@ function parseCrontabFile(content: string): CrontabEntry[] {
118
121
  return entries
119
122
  }
120
123
 
124
+ /**
125
+ * Get human-readable description of a cron expression
126
+ * @param expression - The cron expression
127
+ * @returns Human-readable description or null if parsing fails
128
+ */
129
+ function getHumanReadableDescription(expression: string): string | null {
130
+ try {
131
+ return cronstrue.toString(expression, {
132
+ throwExceptionOnParseError: false,
133
+ verbose: false
134
+ })
135
+ } catch {
136
+ return null
137
+ }
138
+ }
139
+
121
140
  function printUsage(process: Process | undefined, terminal: Terminal): void {
122
141
  const usage = `Usage: cron [COMMAND] [OPTIONS]
123
-
142
+
124
143
  Manage scheduled tasks (crontabs).
125
144
 
145
+ Options:
146
+ --help display this help and exit
147
+
126
148
  Commands:
127
149
  list List all active cron jobs
128
150
  add <schedule> <cmd> Add a new cron job to user crontab
@@ -136,6 +158,7 @@ Commands:
136
158
  Examples:
137
159
  cron list List all cron jobs
138
160
  cron add "*/5 * * * *" "echo hello" Add job to run every 5 minutes
161
+ cron add "* * * * * *" "echo hello" Add job to run every second (6-field)
139
162
  cron add "0 0 * * *" "echo daily" Add daily job at midnight
140
163
  cron remove cron:user:1 Remove user cron job #1
141
164
  cron validate "*/5 * * * *" Validate cron expression
@@ -143,10 +166,14 @@ Examples:
143
166
  cron test "*/5 * * * *" Test if expression matches now
144
167
 
145
168
  Crontab format:
146
- Standard: minute hour day month weekday command
147
- Extended: second minute hour day month weekday command
148
-
149
- --help display this help and exit`
169
+ Both 5-field and 6-field cron expressions are supported:
170
+
171
+ 5-field (standard): minute hour day month weekday command
172
+ Example: "*/5 * * * *" runs every 5 minutes
173
+
174
+ 6-field (extended): second minute hour day month weekday command
175
+ Example: "* * * * * *" runs every second
176
+ Example: "0 */5 * * * *" runs every 5 minutes at :00 seconds`
150
177
  writelnStderr(process, terminal, usage)
151
178
  }
152
179
 
@@ -176,9 +203,57 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
176
203
  return 0
177
204
  }
178
205
 
206
+ // Build a map of job names to expressions by parsing crontab files
207
+ const jobExpressions = new Map<string, { expression: string, command: string }>()
208
+
209
+ // Load system crontab entries
210
+ try {
211
+ const systemCrontabPath = '/etc/crontab'
212
+ if (await kernel.filesystem.fs.exists(systemCrontabPath)) {
213
+ const content = await kernel.filesystem.fs.readFile(systemCrontabPath, 'utf-8')
214
+ const entries = parseCrontabFile(content)
215
+ for (const entry of entries) {
216
+ const jobName = `cron:system:${entry.lineNumber}`
217
+ jobExpressions.set(jobName, { expression: entry.expression, command: entry.command })
218
+ }
219
+ }
220
+ } catch {
221
+ // Ignore errors loading system crontab
222
+ }
223
+
224
+ // Load user crontab entries
225
+ try {
226
+ const home = shell.env.get('HOME') ?? '/root'
227
+ const userCrontabPath = path.join(home, '.config', 'crontab')
228
+ if (await shell.context.fs.promises.exists(userCrontabPath)) {
229
+ const content = await shell.context.fs.promises.readFile(userCrontabPath, 'utf-8')
230
+ const entries = parseCrontabFile(content)
231
+ for (const entry of entries) {
232
+ const jobName = `cron:user:${entry.lineNumber}`
233
+ jobExpressions.set(jobName, { expression: entry.expression, command: entry.command })
234
+ }
235
+ }
236
+ } catch {
237
+ // Ignore errors loading user crontab
238
+ }
239
+
179
240
  await writelnStdout(process, terminal, 'Active cron jobs:')
180
241
  for (const jobName of cronJobs) {
181
- await writelnStdout(process, terminal, ` ${jobName}`)
242
+ const jobInfo = jobExpressions.get(jobName)
243
+ if (jobInfo) {
244
+ const description = getHumanReadableDescription(jobInfo.expression)
245
+ if (description) {
246
+ await writelnStdout(process, terminal, ` ${jobName}`)
247
+ await writelnStdout(process, terminal, ` Schedule: ${jobInfo.expression} (${description})`)
248
+ await writelnStdout(process, terminal, ` Command: ${jobInfo.command}`)
249
+ } else {
250
+ await writelnStdout(process, terminal, ` ${jobName}`)
251
+ await writelnStdout(process, terminal, ` Schedule: ${jobInfo.expression}`)
252
+ await writelnStdout(process, terminal, ` Command: ${jobInfo.command}`)
253
+ }
254
+ } else {
255
+ await writelnStdout(process, terminal, ` ${jobName}`)
256
+ }
182
257
  }
183
258
  return 0
184
259
  }
@@ -199,8 +274,10 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
199
274
  const command = argv.slice(2).join(' ')
200
275
 
201
276
  // Validate the cron expression
277
+ let humanReadable: string | null = null
202
278
  try {
203
279
  parseCronExpression(schedule)
280
+ humanReadable = getHumanReadableDescription(schedule)
204
281
  } catch (error) {
205
282
  await writelnStderr(process, terminal, `cron add: invalid cron expression: ${schedule}`)
206
283
  return 1
@@ -247,6 +324,9 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
247
324
  }
248
325
 
249
326
  await writelnStdout(process, terminal, `Added cron job: ${schedule} ${command}`)
327
+ if (humanReadable) {
328
+ await writelnStdout(process, terminal, ` Schedule: ${humanReadable}`)
329
+ }
250
330
  await writelnStdout(process, terminal, 'Run "cron reload" to activate the new job.')
251
331
  return 0
252
332
  }
@@ -311,7 +391,7 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
311
391
 
312
392
  // Use view command to edit (or create if doesn't exist)
313
393
  const result = await kernel.execute({
314
- command: '/bin/view',
394
+ command: '/usr/bin/edit',
315
395
  args: [crontabPath],
316
396
  shell,
317
397
  terminal
@@ -338,7 +418,11 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
338
418
  }
339
419
  try {
340
420
  parseCronExpression(expression)
421
+ const description = getHumanReadableDescription(expression)
341
422
  await writelnStdout(process, terminal, `Valid cron expression: ${expression}`)
423
+ if (description) {
424
+ await writelnStdout(process, terminal, ` Description: ${description}`)
425
+ }
342
426
  return 0
343
427
  } catch (error) {
344
428
  await writelnStderr(process, terminal, `Invalid cron expression: ${expression}`)
@@ -370,8 +454,12 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
370
454
  const cron = parseCronExpression(expression)
371
455
  const now = new Date()
372
456
  const dates = cron.getNextDates(count, now)
457
+ const description = getHumanReadableDescription(expression)
373
458
 
374
459
  await writelnStdout(process, terminal, `Next ${count} execution time(s) for "${expression}":`)
460
+ if (description) {
461
+ await writelnStdout(process, terminal, ` Schedule: ${description}`)
462
+ }
375
463
  for (let i = 0; i < dates.length; i++) {
376
464
  const date = dates[i]
377
465
  if (date) {
@@ -402,12 +490,16 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
402
490
  const cron = parseCronExpression(expression)
403
491
  const now = new Date()
404
492
  const matches = cron.matchDate(now)
493
+ const description = getHumanReadableDescription(expression)
405
494
 
406
495
  if (matches) {
407
496
  await writelnStdout(process, terminal, `Expression "${expression}" matches current time: ${now.toISOString()}`)
408
497
  } else {
409
498
  await writelnStdout(process, terminal, `Expression "${expression}" does not match current time: ${now.toISOString()}`)
410
499
  }
500
+ if (description) {
501
+ await writelnStdout(process, terminal, ` Schedule: ${description}`)
502
+ }
411
503
  return 0
412
504
  } catch (error) {
413
505
  await writelnStderr(process, terminal, `Invalid cron expression: ${expression}`)