@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +48 -0
- package/dist/commands/awk.d.ts +4 -0
- package/dist/commands/awk.d.ts.map +1 -0
- package/dist/commands/awk.js +324 -0
- package/dist/commands/awk.js.map +1 -0
- package/dist/commands/chgrp.d.ts +4 -0
- package/dist/commands/chgrp.d.ts.map +1 -0
- package/dist/commands/chgrp.js +187 -0
- package/dist/commands/chgrp.js.map +1 -0
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +139 -2
- package/dist/commands/chmod.js.map +1 -1
- package/dist/commands/chown.d.ts +4 -0
- package/dist/commands/chown.d.ts.map +1 -0
- package/dist/commands/chown.js +257 -0
- package/dist/commands/chown.js.map +1 -0
- package/dist/commands/cksum.d.ts +4 -0
- package/dist/commands/cksum.d.ts.map +1 -0
- package/dist/commands/cksum.js +124 -0
- package/dist/commands/cksum.js.map +1 -0
- package/dist/commands/cmp.d.ts +4 -0
- package/dist/commands/cmp.d.ts.map +1 -0
- package/dist/commands/cmp.js +120 -0
- package/dist/commands/cmp.js.map +1 -0
- package/dist/commands/column.d.ts +4 -0
- package/dist/commands/column.d.ts.map +1 -0
- package/dist/commands/column.js +274 -0
- package/dist/commands/column.js.map +1 -0
- package/dist/commands/cp.d.ts.map +1 -1
- package/dist/commands/cp.js +81 -4
- package/dist/commands/cp.js.map +1 -1
- package/dist/commands/cron.d.ts.map +1 -1
- package/dist/commands/cron.js +116 -23
- package/dist/commands/cron.js.map +1 -1
- package/dist/commands/curl.d.ts +4 -0
- package/dist/commands/curl.d.ts.map +1 -0
- package/dist/commands/curl.js +238 -0
- package/dist/commands/curl.js.map +1 -0
- package/dist/commands/du.d.ts +4 -0
- package/dist/commands/du.d.ts.map +1 -0
- package/dist/commands/du.js +168 -0
- package/dist/commands/du.js.map +1 -0
- package/dist/commands/echo.d.ts.map +1 -1
- package/dist/commands/echo.js +125 -2
- package/dist/commands/echo.js.map +1 -1
- package/dist/commands/env.d.ts +4 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +129 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/expand.d.ts +4 -0
- package/dist/commands/expand.d.ts.map +1 -0
- package/dist/commands/expand.js +197 -0
- package/dist/commands/expand.js.map +1 -0
- package/dist/commands/factor.d.ts +4 -0
- package/dist/commands/factor.d.ts.map +1 -0
- package/dist/commands/factor.js +141 -0
- package/dist/commands/factor.js.map +1 -0
- package/dist/commands/fmt.d.ts +4 -0
- package/dist/commands/fmt.d.ts.map +1 -0
- package/dist/commands/fmt.js +278 -0
- package/dist/commands/fmt.js.map +1 -0
- package/dist/commands/fold.d.ts +4 -0
- package/dist/commands/fold.d.ts.map +1 -0
- package/dist/commands/fold.js +253 -0
- package/dist/commands/fold.js.map +1 -0
- package/dist/commands/groups.d.ts +4 -0
- package/dist/commands/groups.d.ts.map +1 -0
- package/dist/commands/groups.js +61 -0
- package/dist/commands/groups.js.map +1 -0
- package/dist/commands/head.d.ts.map +1 -1
- package/dist/commands/head.js +184 -77
- package/dist/commands/head.js.map +1 -1
- package/dist/commands/hostname.d.ts +4 -0
- package/dist/commands/hostname.d.ts.map +1 -0
- package/dist/commands/hostname.js +80 -0
- package/dist/commands/hostname.js.map +1 -0
- package/dist/commands/less.d.ts.map +1 -1
- package/dist/commands/less.js +1 -0
- package/dist/commands/less.js.map +1 -1
- package/dist/commands/man.d.ts.map +1 -1
- package/dist/commands/man.js +3 -1
- package/dist/commands/man.js.map +1 -1
- package/dist/commands/mount.d.ts +4 -0
- package/dist/commands/mount.d.ts.map +1 -0
- package/dist/commands/mount.js +1136 -0
- package/dist/commands/mount.js.map +1 -0
- package/dist/commands/od.d.ts +4 -0
- package/dist/commands/od.d.ts.map +1 -0
- package/dist/commands/od.js +342 -0
- package/dist/commands/od.js.map +1 -0
- package/dist/commands/pr.d.ts +4 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +298 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/printf.d.ts +4 -0
- package/dist/commands/printf.d.ts.map +1 -0
- package/dist/commands/printf.js +271 -0
- package/dist/commands/printf.js.map +1 -0
- package/dist/commands/readlink.d.ts +4 -0
- package/dist/commands/readlink.d.ts.map +1 -0
- package/dist/commands/readlink.js +104 -0
- package/dist/commands/readlink.js.map +1 -0
- package/dist/commands/realpath.d.ts +4 -0
- package/dist/commands/realpath.d.ts.map +1 -0
- package/dist/commands/realpath.js +111 -0
- package/dist/commands/realpath.js.map +1 -0
- package/dist/commands/rev.d.ts +4 -0
- package/dist/commands/rev.d.ts.map +1 -0
- package/dist/commands/rev.js +134 -0
- package/dist/commands/rev.js.map +1 -0
- package/dist/commands/shuf.d.ts +4 -0
- package/dist/commands/shuf.d.ts.map +1 -0
- package/dist/commands/shuf.js +221 -0
- package/dist/commands/shuf.js.map +1 -0
- package/dist/commands/sleep.d.ts +4 -0
- package/dist/commands/sleep.d.ts.map +1 -0
- package/dist/commands/sleep.js +102 -0
- package/dist/commands/sleep.js.map +1 -0
- package/dist/commands/strings.d.ts +4 -0
- package/dist/commands/strings.d.ts.map +1 -0
- package/dist/commands/strings.js +170 -0
- package/dist/commands/strings.js.map +1 -0
- package/dist/commands/tac.d.ts +4 -0
- package/dist/commands/tac.d.ts.map +1 -0
- package/dist/commands/tac.js +130 -0
- package/dist/commands/tac.js.map +1 -0
- package/dist/commands/time.d.ts +4 -0
- package/dist/commands/time.d.ts.map +1 -0
- package/dist/commands/time.js +126 -0
- package/dist/commands/time.js.map +1 -0
- package/dist/commands/umount.d.ts +4 -0
- package/dist/commands/umount.d.ts.map +1 -0
- package/dist/commands/umount.js +103 -0
- package/dist/commands/umount.js.map +1 -0
- package/dist/commands/uname.d.ts +4 -0
- package/dist/commands/uname.d.ts.map +1 -0
- package/dist/commands/uname.js +149 -0
- package/dist/commands/uname.js.map +1 -0
- package/dist/commands/unexpand.d.ts +4 -0
- package/dist/commands/unexpand.d.ts.map +1 -0
- package/dist/commands/unexpand.js +286 -0
- package/dist/commands/unexpand.js.map +1 -0
- package/dist/commands/uptime.d.ts +4 -0
- package/dist/commands/uptime.d.ts.map +1 -0
- package/dist/commands/uptime.js +62 -0
- package/dist/commands/uptime.js.map +1 -0
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.d.ts.map +1 -1
- package/dist/commands/view.js +408 -66
- package/dist/commands/view.js.map +1 -1
- package/dist/commands/yes.d.ts +4 -0
- package/dist/commands/yes.d.ts.map +1 -0
- package/dist/commands/yes.js +58 -0
- package/dist/commands/yes.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -1
- package/package.json +12 -3
- package/src/commands/awk.ts +340 -0
- package/src/commands/chmod.ts +141 -2
- package/src/commands/chown.ts +321 -0
- package/src/commands/cksum.ts +133 -0
- package/src/commands/cmp.ts +126 -0
- package/src/commands/column.ts +273 -0
- package/src/commands/cp.ts +93 -4
- package/src/commands/cron.ts +115 -23
- package/src/commands/curl.ts +231 -0
- package/src/commands/echo.ts +122 -2
- package/src/commands/env.ts +143 -0
- package/src/commands/expand.ts +207 -0
- package/src/commands/factor.ts +151 -0
- package/src/commands/fmt.ts +293 -0
- package/src/commands/fold.ts +257 -0
- package/src/commands/groups.ts +72 -0
- package/src/commands/head.ts +176 -77
- package/src/commands/hostname.ts +81 -0
- package/src/commands/less.ts +1 -0
- package/src/commands/man.ts +4 -1
- package/src/commands/mount.ts +1302 -0
- package/src/commands/od.ts +327 -0
- package/src/commands/pr.ts +291 -0
- package/src/commands/printf.ts +271 -0
- package/src/commands/readlink.ts +102 -0
- package/src/commands/realpath.ts +126 -0
- package/src/commands/rev.ts +143 -0
- package/src/commands/shuf.ts +218 -0
- package/src/commands/sleep.ts +109 -0
- package/src/commands/strings.ts +176 -0
- package/src/commands/tac.ts +138 -0
- package/src/commands/time.ts +144 -0
- package/src/commands/umount.ts +116 -0
- package/src/commands/uname.ts +130 -0
- package/src/commands/unexpand.ts +305 -0
- package/src/commands/uptime.ts +73 -0
- package/src/commands/view.ts +463 -73
- package/src/index.ts +82 -0
- 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
|
+
}
|
package/src/commands/cp.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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}`)
|
package/src/commands/cron.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
57
|
+
// Try to validate 6-field format first
|
|
55
58
|
try {
|
|
56
|
-
parseCronExpression(
|
|
57
|
-
|
|
59
|
+
parseCronExpression(potential6Field)
|
|
60
|
+
valid6Field = true
|
|
58
61
|
} catch {
|
|
59
|
-
// Not a valid
|
|
62
|
+
// Not a valid 6-field expression
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
// Try to validate
|
|
65
|
+
// Try to validate 5-field format
|
|
63
66
|
try {
|
|
64
|
-
parseCronExpression(
|
|
65
|
-
|
|
67
|
+
parseCronExpression(potential5Field)
|
|
68
|
+
valid5Field = true
|
|
66
69
|
} catch {
|
|
67
|
-
// Not a valid
|
|
70
|
+
// Not a valid 5-field expression
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
// Prefer
|
|
71
|
-
// Only use
|
|
72
|
-
if (
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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/
|
|
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}`)
|