@ecmaos/coreutils 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +25 -0
- package/LICENSE +1 -1
- package/dist/commands/cal.js +2 -2
- package/dist/commands/cal.js.map +1 -1
- package/dist/commands/cat.js +2 -2
- package/dist/commands/cd.js +2 -2
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +16 -11
- package/dist/commands/chmod.js.map +1 -1
- package/dist/commands/cp.js +2 -2
- package/dist/commands/cp.js.map +1 -1
- package/dist/commands/date.js +2 -2
- package/dist/commands/date.js.map +1 -1
- package/dist/commands/echo.js +2 -2
- package/dist/commands/echo.js.map +1 -1
- package/dist/commands/false.js +2 -2
- package/dist/commands/fetch.d.ts +4 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +210 -0
- package/dist/commands/fetch.js.map +1 -0
- package/dist/commands/format.d.ts +4 -0
- package/dist/commands/format.d.ts.map +1 -0
- package/dist/commands/format.js +178 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/hash.d.ts +4 -0
- package/dist/commands/hash.d.ts.map +1 -0
- package/dist/commands/hash.js +200 -0
- package/dist/commands/hash.js.map +1 -0
- package/dist/commands/head.js +2 -2
- package/dist/commands/id.js +2 -2
- package/dist/commands/id.js.map +1 -1
- package/dist/commands/less.d.ts.map +1 -1
- package/dist/commands/less.js +53 -2
- package/dist/commands/less.js.map +1 -1
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +120 -97
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/man.d.ts +4 -0
- package/dist/commands/man.d.ts.map +1 -0
- package/dist/commands/man.js +554 -0
- package/dist/commands/man.js.map +1 -0
- package/dist/commands/mkdir.js +2 -2
- package/dist/commands/mkdir.js.map +1 -1
- package/dist/commands/mktemp.d.ts +4 -0
- package/dist/commands/mktemp.d.ts.map +1 -0
- package/dist/commands/mktemp.js +229 -0
- package/dist/commands/mktemp.js.map +1 -0
- package/dist/commands/nc.js +2 -2
- package/dist/commands/nc.js.map +1 -1
- package/dist/commands/passkey.js +3 -3
- package/dist/commands/pwd.js +2 -2
- package/dist/commands/pwd.js.map +1 -1
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +57 -12
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/rmdir.js +2 -2
- package/dist/commands/rmdir.js.map +1 -1
- package/dist/commands/sockets.js +1 -1
- package/dist/commands/stat.d.ts.map +1 -1
- package/dist/commands/stat.js +37 -15
- package/dist/commands/stat.js.map +1 -1
- package/dist/commands/tail.js +2 -2
- package/dist/commands/tar.d.ts +4 -0
- package/dist/commands/tar.d.ts.map +1 -0
- package/dist/commands/tar.js +693 -0
- package/dist/commands/tar.js.map +1 -0
- package/dist/commands/touch.js +2 -2
- package/dist/commands/touch.js.map +1 -1
- package/dist/commands/true.js +2 -2
- package/dist/commands/unzip.d.ts +4 -0
- package/dist/commands/unzip.d.ts.map +1 -0
- package/dist/commands/unzip.js +443 -0
- package/dist/commands/unzip.js.map +1 -0
- package/dist/commands/user.d.ts +4 -0
- package/dist/commands/user.d.ts.map +1 -0
- package/dist/commands/user.js +427 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/commands/whoami.js +2 -2
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/zip.d.ts +4 -0
- package/dist/commands/zip.d.ts.map +1 -0
- package/dist/commands/zip.js +264 -0
- package/dist/commands/zip.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/commands/cal.ts +2 -2
- package/src/commands/cat.ts +2 -2
- package/src/commands/cd.ts +2 -2
- package/src/commands/chmod.ts +19 -11
- package/src/commands/cp.ts +2 -2
- package/src/commands/date.ts +2 -2
- package/src/commands/echo.ts +2 -2
- package/src/commands/false.ts +2 -2
- package/src/commands/fetch.ts +205 -0
- package/src/commands/format.ts +204 -0
- package/src/commands/hash.ts +215 -0
- package/src/commands/head.ts +2 -2
- package/src/commands/id.ts +2 -2
- package/src/commands/less.ts +50 -2
- package/src/commands/ls.ts +131 -91
- package/src/commands/man.ts +643 -0
- package/src/commands/mkdir.ts +2 -2
- package/src/commands/mktemp.ts +235 -0
- package/src/commands/nc.ts +2 -2
- package/src/commands/passkey.ts +3 -3
- package/src/commands/pwd.ts +2 -2
- package/src/commands/rm.ts +54 -12
- package/src/commands/rmdir.ts +2 -2
- package/src/commands/sockets.ts +1 -1
- package/src/commands/stat.ts +44 -16
- package/src/commands/tail.ts +2 -2
- package/src/commands/tar.ts +737 -0
- package/src/commands/touch.ts +2 -2
- package/src/commands/true.ts +2 -2
- package/src/commands/unzip.ts +517 -0
- package/src/commands/user.ts +436 -0
- package/src/commands/whoami.ts +2 -2
- package/src/commands/zip.ts +319 -0
- package/src/index.ts +28 -1
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
|
|
3
|
+
import { TerminalEvents } from '@ecmaos/types'
|
|
4
|
+
import { TerminalCommand } from '../shared/terminal-command.js'
|
|
5
|
+
import { writelnStderr } from '../shared/helpers.js'
|
|
6
|
+
|
|
7
|
+
type HashAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512'
|
|
8
|
+
|
|
9
|
+
const SUPPORTED_ALGORITHMS: Record<string, HashAlgorithm> = {
|
|
10
|
+
'sha1': 'SHA-1',
|
|
11
|
+
'sha-1': 'SHA-1',
|
|
12
|
+
'sha256': 'SHA-256',
|
|
13
|
+
'sha-256': 'SHA-256',
|
|
14
|
+
'sha384': 'SHA-384',
|
|
15
|
+
'sha-384': 'SHA-384',
|
|
16
|
+
'sha512': 'SHA-512',
|
|
17
|
+
'sha-512': 'SHA-512'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printUsage(process: Process | undefined, terminal: Terminal): void {
|
|
21
|
+
const usage = `Usage: hash [OPTION]... [FILE]...
|
|
22
|
+
Compute and display hash values for files or standard input.
|
|
23
|
+
|
|
24
|
+
-a, --algorithm=ALGORITHM hash algorithm to use (sha1, sha256, sha384, sha512)
|
|
25
|
+
default: sha256
|
|
26
|
+
--help display this help and exit
|
|
27
|
+
|
|
28
|
+
Supported algorithms:
|
|
29
|
+
sha1, sha-1 SHA-1 (160 bits)
|
|
30
|
+
sha256, sha-256 SHA-256 (256 bits) [default]
|
|
31
|
+
sha384, sha-384 SHA-384 (384 bits)
|
|
32
|
+
sha512, sha-512 SHA-512 (512 bits)
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
hash file.txt compute SHA-256 hash of file.txt
|
|
36
|
+
hash -a sha512 file.txt compute SHA-512 hash of file.txt
|
|
37
|
+
echo "hello" | hash compute SHA-256 hash of stdin`
|
|
38
|
+
writelnStderr(process, terminal, usage)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function hashData(data: Uint8Array, algorithm: HashAlgorithm): Promise<string> {
|
|
42
|
+
// Create a new Uint8Array with a proper ArrayBuffer to ensure compatibility
|
|
43
|
+
const dataCopy = new Uint8Array(data)
|
|
44
|
+
const hashBuffer = await crypto.subtle.digest(algorithm, dataCopy)
|
|
45
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
46
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readStreamToUint8Array(reader: ReadableStreamDefaultReader<Uint8Array>): Promise<Uint8Array> {
|
|
50
|
+
const chunks: Uint8Array[] = []
|
|
51
|
+
try {
|
|
52
|
+
while (true) {
|
|
53
|
+
const { done, value } = await reader.read()
|
|
54
|
+
if (done) break
|
|
55
|
+
if (value) {
|
|
56
|
+
chunks.push(value)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
reader.releaseLock()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Calculate total length
|
|
64
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
|
|
65
|
+
|
|
66
|
+
// Concatenate all chunks into a single Uint8Array
|
|
67
|
+
const result = new Uint8Array(totalLength)
|
|
68
|
+
let offset = 0
|
|
69
|
+
for (const chunk of chunks) {
|
|
70
|
+
result.set(chunk, offset)
|
|
71
|
+
offset += chunk.length
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
|
|
78
|
+
return new TerminalCommand({
|
|
79
|
+
command: 'hash',
|
|
80
|
+
description: 'Compute and display hash values for files or standard input',
|
|
81
|
+
kernel,
|
|
82
|
+
shell,
|
|
83
|
+
terminal,
|
|
84
|
+
run: async (pid: number, argv: string[]) => {
|
|
85
|
+
const process = kernel.processes.get(pid) as Process | undefined
|
|
86
|
+
|
|
87
|
+
if (!process) return 1
|
|
88
|
+
|
|
89
|
+
if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
90
|
+
printUsage(process, terminal)
|
|
91
|
+
return 0
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let algorithm: HashAlgorithm = 'SHA-256'
|
|
95
|
+
const files: string[] = []
|
|
96
|
+
|
|
97
|
+
// Parse arguments
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
const arg = argv[i]
|
|
100
|
+
if (arg === undefined) continue
|
|
101
|
+
|
|
102
|
+
if (arg === '--help' || arg === '-h') {
|
|
103
|
+
printUsage(process, terminal)
|
|
104
|
+
return 0
|
|
105
|
+
} else if (arg === '-a' || arg === '--algorithm') {
|
|
106
|
+
const algoArg = argv[i + 1]
|
|
107
|
+
if (!algoArg) {
|
|
108
|
+
await writelnStderr(process, terminal, `hash: option requires an argument -- '${arg === '-a' ? 'a' : 'algorithm'}'`)
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
const algoLower = algoArg.toLowerCase()
|
|
112
|
+
const selectedAlgorithm = SUPPORTED_ALGORITHMS[algoLower]
|
|
113
|
+
if (!selectedAlgorithm) {
|
|
114
|
+
await writelnStderr(process, terminal, `hash: unsupported algorithm '${algoArg}'\nSupported algorithms: ${Object.keys(SUPPORTED_ALGORITHMS).join(', ')}`)
|
|
115
|
+
return 1
|
|
116
|
+
}
|
|
117
|
+
algorithm = selectedAlgorithm
|
|
118
|
+
i++ // Skip the next argument as it's the algorithm value
|
|
119
|
+
} else if (arg.startsWith('--algorithm=')) {
|
|
120
|
+
const algoArg = arg.split('=')[1]
|
|
121
|
+
if (!algoArg) {
|
|
122
|
+
await writelnStderr(process, terminal, `hash: option requires an argument -- 'algorithm'`)
|
|
123
|
+
return 1
|
|
124
|
+
}
|
|
125
|
+
const algoLower = algoArg.toLowerCase()
|
|
126
|
+
const selectedAlgorithm = SUPPORTED_ALGORITHMS[algoLower]
|
|
127
|
+
if (!selectedAlgorithm) {
|
|
128
|
+
await writelnStderr(process, terminal, `hash: unsupported algorithm '${algoArg}'\nSupported algorithms: ${Object.keys(SUPPORTED_ALGORITHMS).join(', ')}`)
|
|
129
|
+
return 1
|
|
130
|
+
}
|
|
131
|
+
algorithm = selectedAlgorithm
|
|
132
|
+
} else if (!arg.startsWith('-')) {
|
|
133
|
+
files.push(arg)
|
|
134
|
+
} else {
|
|
135
|
+
await writelnStderr(process, terminal, `hash: invalid option -- '${arg.replace(/^-+/, '')}'`)
|
|
136
|
+
await writelnStderr(process, terminal, `Try 'hash --help' for more information.`)
|
|
137
|
+
return 1
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const writer = process.stdout.getWriter()
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// If no files specified, read from stdin
|
|
145
|
+
if (files.length === 0) {
|
|
146
|
+
if (!process.stdin) {
|
|
147
|
+
await writelnStderr(process, terminal, 'hash: no input specified')
|
|
148
|
+
return 1
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const reader = process.stdin.getReader()
|
|
152
|
+
const data = await readStreamToUint8Array(reader)
|
|
153
|
+
const hash = await hashData(data, algorithm)
|
|
154
|
+
await writer.write(new TextEncoder().encode(hash + '\n'))
|
|
155
|
+
return 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Process each file
|
|
159
|
+
let hasError = false
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const fullPath = path.resolve(shell.cwd, file)
|
|
162
|
+
|
|
163
|
+
let interrupted = false
|
|
164
|
+
const interruptHandler = () => { interrupted = true }
|
|
165
|
+
kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (fullPath.startsWith('/dev')) {
|
|
169
|
+
await writelnStderr(process, terminal, `hash: ${file}: cannot hash device files`)
|
|
170
|
+
hasError = true
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handle = await shell.context.fs.promises.open(fullPath, 'r')
|
|
175
|
+
const stat = await shell.context.fs.promises.stat(fullPath)
|
|
176
|
+
|
|
177
|
+
const chunks: Uint8Array[] = []
|
|
178
|
+
let bytesRead = 0
|
|
179
|
+
const chunkSize = 64 * 1024 // 64KB chunks for better performance
|
|
180
|
+
|
|
181
|
+
while (bytesRead < stat.size) {
|
|
182
|
+
if (interrupted) break
|
|
183
|
+
const data = new Uint8Array(chunkSize)
|
|
184
|
+
const readSize = Math.min(chunkSize, stat.size - bytesRead)
|
|
185
|
+
await handle.read(data, 0, readSize, bytesRead)
|
|
186
|
+
chunks.push(data.subarray(0, readSize))
|
|
187
|
+
bytesRead += readSize
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Concatenate all chunks
|
|
191
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0)
|
|
192
|
+
const fileData = new Uint8Array(totalLength)
|
|
193
|
+
let offset = 0
|
|
194
|
+
for (const chunk of chunks) {
|
|
195
|
+
fileData.set(chunk, offset)
|
|
196
|
+
offset += chunk.length
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hash = await hashData(fileData, algorithm)
|
|
200
|
+
await writer.write(new TextEncoder().encode(`${hash} ${file}\n`))
|
|
201
|
+
} catch (error) {
|
|
202
|
+
await writelnStderr(process, terminal, `hash: ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
203
|
+
hasError = true
|
|
204
|
+
} finally {
|
|
205
|
+
kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return hasError ? 1 : 0
|
|
210
|
+
} finally {
|
|
211
|
+
writer.releaseLock()
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
}
|
package/src/commands/head.ts
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path'
|
|
|
2
2
|
import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
|
|
3
3
|
import { TerminalEvents } from '@ecmaos/types'
|
|
4
4
|
import { TerminalCommand } from '../shared/terminal-command.js'
|
|
5
|
-
import {
|
|
5
|
+
import { writelnStderr } from '../shared/helpers.js'
|
|
6
6
|
|
|
7
7
|
function printUsage(process: Process | undefined, terminal: Terminal): void {
|
|
8
8
|
const usage = `Usage: head [OPTION]... [FILE]...
|
|
@@ -10,7 +10,7 @@ Print the first 10 lines of each FILE to standard output.
|
|
|
10
10
|
|
|
11
11
|
-n, -nNUMBER print the first NUMBER lines instead of 10
|
|
12
12
|
--help display this help and exit`
|
|
13
|
-
|
|
13
|
+
writelnStderr(process, terminal, usage)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
|
package/src/commands/id.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
|
|
2
2
|
import { TerminalCommand } from '../shared/terminal-command.js'
|
|
3
|
-
import { writelnStdout } from '../shared/helpers.js'
|
|
3
|
+
import { writelnStdout, writelnStderr } from '../shared/helpers.js'
|
|
4
4
|
|
|
5
5
|
function printUsage(process: Process | undefined, terminal: Terminal): void {
|
|
6
6
|
const usage = `Usage: id [OPTION]...
|
|
@@ -11,7 +11,7 @@ Print user and group IDs.
|
|
|
11
11
|
-G, --groups print all group IDs
|
|
12
12
|
-n, --name print names instead of numeric IDs
|
|
13
13
|
--help display this help and exit`
|
|
14
|
-
|
|
14
|
+
writelnStderr(process, terminal, usage)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
|
package/src/commands/less.ts
CHANGED
|
@@ -33,6 +33,7 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
33
33
|
|
|
34
34
|
let lines: string[] = []
|
|
35
35
|
let currentLine = 0
|
|
36
|
+
let horizontalOffset = 0
|
|
36
37
|
let keyListener: IDisposable | null = null
|
|
37
38
|
let linesRendered = 0
|
|
38
39
|
|
|
@@ -94,6 +95,40 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
94
95
|
const displayRows = rows - 1
|
|
95
96
|
|
|
96
97
|
const render = () => {
|
|
98
|
+
const cols = terminal.cols
|
|
99
|
+
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
100
|
+
|
|
101
|
+
const getVisibleSlice = (line: string, offset: number): string => {
|
|
102
|
+
const visibleLen = stripAnsi(line).length
|
|
103
|
+
|
|
104
|
+
if (offset < 0) offset = 0
|
|
105
|
+
if (offset > visibleLen) offset = visibleLen
|
|
106
|
+
|
|
107
|
+
let visible = 0
|
|
108
|
+
let result = ''
|
|
109
|
+
let inEscape = false
|
|
110
|
+
let charsSkipped = 0
|
|
111
|
+
|
|
112
|
+
for (const char of line) {
|
|
113
|
+
if (char === '\x1b') inEscape = true
|
|
114
|
+
if (inEscape) {
|
|
115
|
+
if (charsSkipped >= offset) {
|
|
116
|
+
result += char
|
|
117
|
+
}
|
|
118
|
+
if (/[a-zA-Z]/.test(char)) inEscape = false
|
|
119
|
+
} else {
|
|
120
|
+
if (charsSkipped < offset) {
|
|
121
|
+
charsSkipped++
|
|
122
|
+
} else {
|
|
123
|
+
if (visible >= cols) break
|
|
124
|
+
result += char
|
|
125
|
+
visible++
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result
|
|
130
|
+
}
|
|
131
|
+
|
|
97
132
|
const maxLine = Math.max(0, lines.length - displayRows)
|
|
98
133
|
if (currentLine > maxLine) {
|
|
99
134
|
currentLine = maxLine
|
|
@@ -102,6 +137,11 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
102
137
|
currentLine = 0
|
|
103
138
|
}
|
|
104
139
|
|
|
140
|
+
const maxLineLength = Math.max(...lines.map(l => stripAnsi(l).length), 0)
|
|
141
|
+
const maxHorizontalOffset = Math.max(0, maxLineLength - cols)
|
|
142
|
+
if (horizontalOffset > maxHorizontalOffset) horizontalOffset = maxHorizontalOffset
|
|
143
|
+
if (horizontalOffset < 0) horizontalOffset = 0
|
|
144
|
+
|
|
105
145
|
if (linesRendered > 0) {
|
|
106
146
|
terminal.write(ansi.cursor.up(linesRendered))
|
|
107
147
|
}
|
|
@@ -111,7 +151,7 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
111
151
|
|
|
112
152
|
for (let i = currentLine; i < endLine; i++) {
|
|
113
153
|
terminal.write(ansi.erase.inLine(2))
|
|
114
|
-
const line = lines[i] || ''
|
|
154
|
+
const line = getVisibleSlice(lines[i] || '', horizontalOffset)
|
|
115
155
|
terminal.write(line)
|
|
116
156
|
linesRendered++
|
|
117
157
|
if (i < endLine - 1) {
|
|
@@ -129,7 +169,7 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
129
169
|
const statusLine = `-- ${currentLine + 1}-${endLine} / ${lines.length} (${percentage}%)`
|
|
130
170
|
terminal.write('\n')
|
|
131
171
|
terminal.write(ansi.erase.inLine(2))
|
|
132
|
-
terminal.write(statusLine)
|
|
172
|
+
terminal.write(getVisibleSlice(statusLine, 0))
|
|
133
173
|
linesRendered++
|
|
134
174
|
}
|
|
135
175
|
|
|
@@ -163,6 +203,14 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
163
203
|
currentLine++
|
|
164
204
|
render()
|
|
165
205
|
break
|
|
206
|
+
case 'ArrowLeft':
|
|
207
|
+
horizontalOffset = Math.max(0, horizontalOffset - Math.floor(terminal.cols / 2))
|
|
208
|
+
render()
|
|
209
|
+
break
|
|
210
|
+
case 'ArrowRight':
|
|
211
|
+
horizontalOffset += Math.floor(terminal.cols / 2)
|
|
212
|
+
render()
|
|
213
|
+
break
|
|
166
214
|
case 'PageDown':
|
|
167
215
|
case ' ':
|
|
168
216
|
currentLine = Math.min(currentLine + displayRows, Math.max(0, lines.length - displayRows))
|
package/src/commands/ls.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
|
+
import columnify from 'columnify'
|
|
3
4
|
import humanFormat from 'human-format'
|
|
4
5
|
import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
|
|
5
6
|
import { TerminalCommand } from '../shared/terminal-command.js'
|
|
6
|
-
import { writelnStdout } from '../shared/helpers.js'
|
|
7
|
+
import { writelnStdout, writelnStderr } from '../shared/helpers.js'
|
|
7
8
|
|
|
8
9
|
function printUsage(process: Process | undefined, terminal: Terminal): void {
|
|
9
10
|
const usage = `Usage: ls [OPTION]... [FILE]...
|
|
10
11
|
List information about the FILEs (the current directory by default).
|
|
11
12
|
|
|
12
13
|
--help display this help and exit`
|
|
13
|
-
|
|
14
|
+
writelnStderr(process, terminal, usage)
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
|
|
@@ -28,12 +29,37 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
28
29
|
return 0
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// Filter out options/flags and get target paths
|
|
33
|
+
const targets = argv.length > 0
|
|
34
|
+
? argv.filter(arg => !arg.startsWith('-'))
|
|
35
|
+
: [shell.cwd]
|
|
36
|
+
|
|
37
|
+
if (targets.length === 0) targets.push(shell.cwd)
|
|
38
|
+
|
|
35
39
|
const descriptions = kernel.filesystem.descriptions(kernel.i18n.t)
|
|
36
40
|
|
|
41
|
+
// Process each target and collect all entries
|
|
42
|
+
// We'll determine if each entry is a directory when we stat it later
|
|
43
|
+
const allEntries: Array<{ fullPath: string, entry: string }> = []
|
|
44
|
+
|
|
45
|
+
for (const target of targets) {
|
|
46
|
+
const fullPath = path.resolve(shell.cwd, target === '' ? '.' : target)
|
|
47
|
+
try {
|
|
48
|
+
const stats = await shell.context.fs.promises.stat(fullPath)
|
|
49
|
+
if (stats.isDirectory()) {
|
|
50
|
+
// For directories, list all contents
|
|
51
|
+
const dirEntries = await shell.context.fs.promises.readdir(fullPath)
|
|
52
|
+
for (const entry of dirEntries) allEntries.push({ fullPath, entry })
|
|
53
|
+
} else {
|
|
54
|
+
// For files, add the file itself
|
|
55
|
+
allEntries.push({ fullPath: path.dirname(fullPath), entry: path.basename(fullPath) })
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// If target doesn't exist, skip it (standard ls behavior)
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
37
63
|
const getModeType = (stats: Awaited<ReturnType<typeof shell.context.fs.promises.stat>>) => {
|
|
38
64
|
let type = '-'
|
|
39
65
|
if (stats.isDirectory()) type = 'd'
|
|
@@ -84,9 +110,9 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
84
110
|
return ''
|
|
85
111
|
}
|
|
86
112
|
|
|
87
|
-
const filesMap = await Promise.all(
|
|
88
|
-
.map(async entry => {
|
|
89
|
-
const target = path.resolve(
|
|
113
|
+
const filesMap = await Promise.all(allEntries
|
|
114
|
+
.map(async ({ fullPath: entryFullPath, entry }) => {
|
|
115
|
+
const target = path.resolve(entryFullPath, entry)
|
|
90
116
|
try {
|
|
91
117
|
let linkTarget: string | null = null
|
|
92
118
|
let linkStats = null
|
|
@@ -118,9 +144,9 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
118
144
|
.filter(entry => entry && entry.stats && !entry.stats.isDirectory())
|
|
119
145
|
.filter((entry): entry is NonNullable<typeof entry> => entry !== null && entry !== undefined)
|
|
120
146
|
|
|
121
|
-
const directoryMap = await Promise.all(
|
|
122
|
-
.map(async entry => {
|
|
123
|
-
const target = path.resolve(
|
|
147
|
+
const directoryMap = await Promise.all(allEntries
|
|
148
|
+
.map(async ({ fullPath: entryFullPath, entry }) => {
|
|
149
|
+
const target = path.resolve(entryFullPath, entry)
|
|
124
150
|
try {
|
|
125
151
|
let linkTarget: string | null = null
|
|
126
152
|
let linkStats = null
|
|
@@ -153,87 +179,101 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
|
|
|
153
179
|
.filter((entry, index, self) => self.findIndex(e => e?.name === entry?.name) === index)
|
|
154
180
|
.filter((entry): entry is NonNullable<typeof entry> => entry !== null && entry !== undefined)
|
|
155
181
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
file.stats ? getTimestampString(file.stats.mtime) : '',
|
|
192
|
-
modeString,
|
|
193
|
-
file.stats ? getOwnerString(file.stats) : '',
|
|
194
|
-
(() => {
|
|
195
|
-
const linkInfo = getLinkInfo(file.linkTarget, file.linkStats, file.stats)
|
|
196
|
-
if (linkInfo) return linkInfo
|
|
197
|
-
|
|
198
|
-
if (descriptions.has(path.resolve(fullPath, file.name))) return descriptions.get(path.resolve(fullPath, file.name))
|
|
199
|
-
if (file.name.includes('.')) {
|
|
200
|
-
const ext = file.name.split('.').pop()
|
|
201
|
-
if (ext && descriptions.has('.' + ext)) return descriptions.get('.' + ext)
|
|
202
|
-
}
|
|
203
|
-
if (!file.stats) return ''
|
|
204
|
-
if (file.stats.isBlockDevice() || file.stats.isCharacterDevice()) {
|
|
205
|
-
// TODO: zenfs `fs.mounts` is deprecated - use a better way of getting device info
|
|
206
|
-
}
|
|
182
|
+
// Check if any entry is in /dev directory
|
|
183
|
+
const isDevDirectory = allEntries.some(e => e.fullPath.startsWith('/dev'))
|
|
184
|
+
const columns = isDevDirectory ? ['Name', 'Mode', 'Owner', 'Info'] : ['Name', 'Size', 'Modified', 'Mode', 'Owner', 'Info']
|
|
185
|
+
|
|
186
|
+
const directoryRows = directories.sort((a, b) => a.name.localeCompare(b.name)).map(directory => {
|
|
187
|
+
const displayName = directory.linkTarget
|
|
188
|
+
? `${directory.name} ${chalk.cyan('⟶')} ${directory.linkTarget}`
|
|
189
|
+
: directory.name
|
|
190
|
+
const modeStats = directory.linkStats && directory.linkStats.isSymbolicLink()
|
|
191
|
+
? directory.linkStats
|
|
192
|
+
: directory.stats
|
|
193
|
+
const modeString = modeStats
|
|
194
|
+
? getModeString(modeStats, directory.linkStats?.isSymbolicLink() ? directory.stats : undefined)
|
|
195
|
+
: ''
|
|
196
|
+
const linkInfo = getLinkInfo(directory.linkTarget, directory.linkStats, directory.stats)
|
|
197
|
+
|
|
198
|
+
const modeType = modeString?.charAt(0) || ''
|
|
199
|
+
const coloredName = modeType === 'd' ? chalk.blue(displayName)
|
|
200
|
+
: modeType === 'l' ? chalk.cyan(displayName)
|
|
201
|
+
: chalk.green(displayName)
|
|
202
|
+
|
|
203
|
+
const row: Record<string, string> = {
|
|
204
|
+
Name: coloredName,
|
|
205
|
+
Mode: chalk.gray(modeString),
|
|
206
|
+
Owner: directory.stats ? chalk.gray(getOwnerString(directory.stats)) : '',
|
|
207
|
+
Info: chalk.gray(linkInfo)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!isDevDirectory) {
|
|
211
|
+
row.Size = ''
|
|
212
|
+
row.Modified = directory.stats ? chalk.gray(getTimestampString(directory.stats.mtime)) : ''
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return row
|
|
216
|
+
})
|
|
207
217
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
const fileRows = files.sort((a, b) => a.name.localeCompare(b.name)).map(file => {
|
|
219
|
+
const displayName = file.linkTarget
|
|
220
|
+
? `${file.name} ${chalk.cyan('⟶')} ${file.linkTarget}`
|
|
221
|
+
: file.name
|
|
222
|
+
const modeStats = file.linkStats && file.linkStats.isSymbolicLink()
|
|
223
|
+
? file.linkStats
|
|
224
|
+
: file.stats
|
|
225
|
+
const modeString = modeStats
|
|
226
|
+
? getModeString(modeStats, file.linkStats?.isSymbolicLink() ? file.stats : undefined)
|
|
227
|
+
: ''
|
|
228
|
+
|
|
229
|
+
const modeType = modeString?.charAt(0) || ''
|
|
230
|
+
const coloredName = modeType === 'd' ? chalk.blue(displayName)
|
|
231
|
+
: modeType === 'l' ? chalk.cyan(displayName)
|
|
232
|
+
: chalk.green(displayName)
|
|
233
|
+
|
|
234
|
+
const info = (() => {
|
|
235
|
+
const linkInfo = getLinkInfo(file.linkTarget, file.linkStats, file.stats)
|
|
236
|
+
if (linkInfo) return linkInfo
|
|
237
|
+
|
|
238
|
+
if (descriptions.has(file.target)) return descriptions.get(file.target) || ''
|
|
239
|
+
if (file.name.includes('.')) {
|
|
240
|
+
const ext = file.name.split('.').pop()
|
|
241
|
+
if (ext && descriptions.has('.' + ext)) return descriptions.get('.' + ext) || ''
|
|
242
|
+
}
|
|
243
|
+
if (!file.stats) return ''
|
|
244
|
+
if (file.stats.isBlockDevice() || file.stats.isCharacterDevice()) {
|
|
245
|
+
// TODO: zenfs `fs.mounts` is deprecated - use a better way of getting device info
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return ''
|
|
249
|
+
})()
|
|
250
|
+
|
|
251
|
+
const row: Record<string, string> = {
|
|
252
|
+
Name: coloredName,
|
|
253
|
+
Mode: chalk.gray(modeString),
|
|
254
|
+
Owner: file.stats ? chalk.gray(getOwnerString(file.stats)) : '',
|
|
255
|
+
Info: chalk.gray(info)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!isDevDirectory) {
|
|
259
|
+
row.Size = file.stats ? chalk.gray(humanFormat(file.stats.size)) : ''
|
|
260
|
+
row.Modified = file.stats ? chalk.gray(getTimestampString(file.stats.mtime)) : ''
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return row
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const data = [...directoryRows, ...fileRows]
|
|
267
|
+
|
|
268
|
+
if (data.length > 0) {
|
|
269
|
+
const table = columnify(data, {
|
|
270
|
+
columns,
|
|
271
|
+
columnSplitter: ' ',
|
|
272
|
+
showHeaders: true,
|
|
273
|
+
headingTransform: (heading: string) => chalk.bold(heading)
|
|
211
274
|
})
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// Special output for certain directories
|
|
215
|
-
if (fullPath.startsWith('/dev')) data.forEach(row => row.splice(1, 2)) // remove size and modified columns
|
|
216
|
-
|
|
217
|
-
const columnWidths = data[0]?.map((_, colIndex) => Math.max(...data.map(row => {
|
|
218
|
-
// Remove ANSI escape sequences before calculating length
|
|
219
|
-
const cleanedCell = row[colIndex]?.replace(/\u001b\[.*?m/g, '')
|
|
220
|
-
// count all emojis as two characters
|
|
221
|
-
return cleanedCell?.length || 0
|
|
222
|
-
})))
|
|
223
|
-
|
|
224
|
-
for (const [rowIndex, row] of data.entries()) {
|
|
225
|
-
const line = row
|
|
226
|
-
.map((cell, index) => {
|
|
227
|
-
const paddedCell = cell.padEnd(columnWidths?.[index] ?? 0)
|
|
228
|
-
if (index === 0 && rowIndex > 0) {
|
|
229
|
-
if (row[3]?.startsWith('d')) return chalk.blue(paddedCell)
|
|
230
|
-
else if (row[3]?.startsWith('l')) return chalk.cyan(paddedCell)
|
|
231
|
-
else return chalk.green(paddedCell)
|
|
232
|
-
} else return rowIndex === 0 ? chalk.bold(paddedCell) : chalk.gray(paddedCell)
|
|
233
|
-
})
|
|
234
|
-
.join(' ')
|
|
235
|
-
|
|
236
|
-
if (data.length > 1) await writelnStdout(process, terminal, line)
|
|
275
|
+
|
|
276
|
+
await writelnStdout(process, terminal, table)
|
|
237
277
|
}
|
|
238
278
|
|
|
239
279
|
return 0
|