@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.
Files changed (123) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/LICENSE +1 -1
  4. package/dist/commands/cal.js +2 -2
  5. package/dist/commands/cal.js.map +1 -1
  6. package/dist/commands/cat.js +2 -2
  7. package/dist/commands/cd.js +2 -2
  8. package/dist/commands/chmod.d.ts.map +1 -1
  9. package/dist/commands/chmod.js +16 -11
  10. package/dist/commands/chmod.js.map +1 -1
  11. package/dist/commands/cp.js +2 -2
  12. package/dist/commands/cp.js.map +1 -1
  13. package/dist/commands/date.js +2 -2
  14. package/dist/commands/date.js.map +1 -1
  15. package/dist/commands/echo.js +2 -2
  16. package/dist/commands/echo.js.map +1 -1
  17. package/dist/commands/false.js +2 -2
  18. package/dist/commands/fetch.d.ts +4 -0
  19. package/dist/commands/fetch.d.ts.map +1 -0
  20. package/dist/commands/fetch.js +210 -0
  21. package/dist/commands/fetch.js.map +1 -0
  22. package/dist/commands/format.d.ts +4 -0
  23. package/dist/commands/format.d.ts.map +1 -0
  24. package/dist/commands/format.js +178 -0
  25. package/dist/commands/format.js.map +1 -0
  26. package/dist/commands/hash.d.ts +4 -0
  27. package/dist/commands/hash.d.ts.map +1 -0
  28. package/dist/commands/hash.js +200 -0
  29. package/dist/commands/hash.js.map +1 -0
  30. package/dist/commands/head.js +2 -2
  31. package/dist/commands/id.js +2 -2
  32. package/dist/commands/id.js.map +1 -1
  33. package/dist/commands/less.d.ts.map +1 -1
  34. package/dist/commands/less.js +53 -2
  35. package/dist/commands/less.js.map +1 -1
  36. package/dist/commands/ls.d.ts.map +1 -1
  37. package/dist/commands/ls.js +120 -97
  38. package/dist/commands/ls.js.map +1 -1
  39. package/dist/commands/man.d.ts +4 -0
  40. package/dist/commands/man.d.ts.map +1 -0
  41. package/dist/commands/man.js +554 -0
  42. package/dist/commands/man.js.map +1 -0
  43. package/dist/commands/mkdir.js +2 -2
  44. package/dist/commands/mkdir.js.map +1 -1
  45. package/dist/commands/mktemp.d.ts +4 -0
  46. package/dist/commands/mktemp.d.ts.map +1 -0
  47. package/dist/commands/mktemp.js +229 -0
  48. package/dist/commands/mktemp.js.map +1 -0
  49. package/dist/commands/nc.js +2 -2
  50. package/dist/commands/nc.js.map +1 -1
  51. package/dist/commands/passkey.js +3 -3
  52. package/dist/commands/pwd.js +2 -2
  53. package/dist/commands/pwd.js.map +1 -1
  54. package/dist/commands/rm.d.ts.map +1 -1
  55. package/dist/commands/rm.js +57 -12
  56. package/dist/commands/rm.js.map +1 -1
  57. package/dist/commands/rmdir.js +2 -2
  58. package/dist/commands/rmdir.js.map +1 -1
  59. package/dist/commands/sockets.js +1 -1
  60. package/dist/commands/stat.d.ts.map +1 -1
  61. package/dist/commands/stat.js +37 -15
  62. package/dist/commands/stat.js.map +1 -1
  63. package/dist/commands/tail.js +2 -2
  64. package/dist/commands/tar.d.ts +4 -0
  65. package/dist/commands/tar.d.ts.map +1 -0
  66. package/dist/commands/tar.js +693 -0
  67. package/dist/commands/tar.js.map +1 -0
  68. package/dist/commands/touch.js +2 -2
  69. package/dist/commands/touch.js.map +1 -1
  70. package/dist/commands/true.js +2 -2
  71. package/dist/commands/unzip.d.ts +4 -0
  72. package/dist/commands/unzip.d.ts.map +1 -0
  73. package/dist/commands/unzip.js +443 -0
  74. package/dist/commands/unzip.js.map +1 -0
  75. package/dist/commands/user.d.ts +4 -0
  76. package/dist/commands/user.d.ts.map +1 -0
  77. package/dist/commands/user.js +427 -0
  78. package/dist/commands/user.js.map +1 -0
  79. package/dist/commands/whoami.js +2 -2
  80. package/dist/commands/whoami.js.map +1 -1
  81. package/dist/commands/zip.d.ts +4 -0
  82. package/dist/commands/zip.d.ts.map +1 -0
  83. package/dist/commands/zip.js +264 -0
  84. package/dist/commands/zip.js.map +1 -0
  85. package/dist/index.d.ts +9 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +28 -1
  88. package/dist/index.js.map +1 -1
  89. package/package.json +6 -4
  90. package/src/commands/cal.ts +2 -2
  91. package/src/commands/cat.ts +2 -2
  92. package/src/commands/cd.ts +2 -2
  93. package/src/commands/chmod.ts +19 -11
  94. package/src/commands/cp.ts +2 -2
  95. package/src/commands/date.ts +2 -2
  96. package/src/commands/echo.ts +2 -2
  97. package/src/commands/false.ts +2 -2
  98. package/src/commands/fetch.ts +205 -0
  99. package/src/commands/format.ts +204 -0
  100. package/src/commands/hash.ts +215 -0
  101. package/src/commands/head.ts +2 -2
  102. package/src/commands/id.ts +2 -2
  103. package/src/commands/less.ts +50 -2
  104. package/src/commands/ls.ts +131 -91
  105. package/src/commands/man.ts +643 -0
  106. package/src/commands/mkdir.ts +2 -2
  107. package/src/commands/mktemp.ts +235 -0
  108. package/src/commands/nc.ts +2 -2
  109. package/src/commands/passkey.ts +3 -3
  110. package/src/commands/pwd.ts +2 -2
  111. package/src/commands/rm.ts +54 -12
  112. package/src/commands/rmdir.ts +2 -2
  113. package/src/commands/sockets.ts +1 -1
  114. package/src/commands/stat.ts +44 -16
  115. package/src/commands/tail.ts +2 -2
  116. package/src/commands/tar.ts +737 -0
  117. package/src/commands/touch.ts +2 -2
  118. package/src/commands/true.ts +2 -2
  119. package/src/commands/unzip.ts +517 -0
  120. package/src/commands/user.ts +436 -0
  121. package/src/commands/whoami.ts +2 -2
  122. package/src/commands/zip.ts +319 -0
  123. 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
+ }
@@ -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 { writelnStdout } from '../shared/helpers.js'
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
- writelnStdout(process, terminal, usage)
13
+ writelnStderr(process, terminal, usage)
14
14
  }
15
15
 
16
16
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
@@ -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
- writelnStdout(process, terminal, usage)
14
+ writelnStderr(process, terminal, usage)
15
15
  }
16
16
 
17
17
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
@@ -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))
@@ -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
- writelnStdout(process, terminal, usage)
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
- const target = argv.length > 0 && argv[0] !== undefined && !argv[0].startsWith('-') ? argv[0] : shell.cwd
32
- const fullPath = target ? path.resolve(shell.cwd, target === '' ? '.' : target) : shell.cwd
33
- const stats = await shell.context.fs.promises.stat(fullPath)
34
- const entries: string[] = stats.isDirectory() ? await shell.context.fs.promises.readdir(fullPath) : [fullPath]
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(entries
88
- .map(async entry => {
89
- const target = path.resolve(fullPath, entry)
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(entries
122
- .map(async entry => {
123
- const target = path.resolve(fullPath, entry)
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
- const data = [
157
- ['Name', 'Size', 'Modified', 'Mode', 'Owner', 'Info'],
158
- ...directories.sort((a, b) => a.name.localeCompare(b.name)).map(directory => {
159
- const displayName = directory.linkTarget
160
- ? `${directory.name} ${chalk.cyan('⟶')} ${directory.linkTarget}`
161
- : directory.name
162
- const modeStats = directory.linkStats && directory.linkStats.isSymbolicLink()
163
- ? directory.linkStats
164
- : directory.stats
165
- const modeString = modeStats
166
- ? getModeString(modeStats, directory.linkStats?.isSymbolicLink() ? directory.stats : undefined)
167
- : ''
168
- const linkInfo = getLinkInfo(directory.linkTarget, directory.linkStats, directory.stats)
169
- return [
170
- displayName,
171
- '',
172
- directory.stats ? getTimestampString(directory.stats.mtime) : '',
173
- modeString,
174
- directory.stats ? getOwnerString(directory.stats) : '',
175
- linkInfo
176
- ]
177
- }),
178
- ...files.sort((a, b) => a.name.localeCompare(b.name)).map(file => {
179
- const displayName = file.linkTarget
180
- ? `${file.name} ${chalk.cyan('')} ${file.linkTarget}`
181
- : file.name
182
- const modeStats = file.linkStats && file.linkStats.isSymbolicLink()
183
- ? file.linkStats
184
- : file.stats
185
- const modeString = modeStats
186
- ? getModeString(modeStats, file.linkStats?.isSymbolicLink() ? file.stats : undefined)
187
- : ''
188
- return [
189
- displayName,
190
- file.stats ? humanFormat(file.stats.size) : '',
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
- return ''
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
- ] as string[][]
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