@ecmaos/coreutils 0.5.1 → 0.5.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.
@@ -0,0 +1,547 @@
1
+ import path from 'path'
2
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
3
+ import { TerminalCommand } from '../shared/terminal-command.js'
4
+ import { writelnStderr } from '../shared/helpers.js'
5
+
6
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
7
+ const usage = `Usage: dd [OPERAND]...
8
+ Copy a file, converting and formatting according to the operands.
9
+
10
+ Operands:
11
+
12
+ if=FILE read from FILE instead of stdin
13
+ of=FILE write to FILE instead of stdout
14
+ bs=BYTES read and write up to BYTES bytes at a time
15
+ ibs=BYTES read up to BYTES bytes at a time (default: 512)
16
+ obs=BYTES write BYTES bytes at a time (default: 512)
17
+ count=N copy only N input blocks
18
+ skip=N skip N input blocks before copying
19
+ seek=N skip N output blocks before copying
20
+ conv=CONVS convert the file as per the comma separated symbol list:
21
+ ucase convert to uppercase
22
+ lcase convert to lowercase
23
+ swab swap every pair of input bytes
24
+ noerror continue after read errors
25
+ notrunc do not truncate the output file
26
+ sync pad every input block to ibs
27
+
28
+ status=LEVEL
29
+ The LEVEL of information to print to stderr:
30
+ 'none' suppress all output
31
+ 'noxfer' suppress final transfer statistics
32
+ 'progress' show periodic transfer statistics
33
+
34
+ --help display this help and exit`
35
+ writelnStderr(process, terminal, usage)
36
+ }
37
+
38
+ function parseBytes(value: string): number {
39
+ const match = value.match(/^([0-9]+)([kmgKMG]?)$/)
40
+ if (!match?.[1]) return NaN
41
+
42
+ const num = parseInt(match[1], 10)
43
+ if (isNaN(num)) return NaN
44
+
45
+ const suffix = (match[2] || '').toLowerCase()
46
+ switch (suffix) {
47
+ case 'k':
48
+ return num * 1024
49
+ case 'm':
50
+ return num * 1024 * 1024
51
+ case 'g':
52
+ return num * 1024 * 1024 * 1024
53
+ default:
54
+ return num
55
+ }
56
+ }
57
+
58
+ function parseBlocks(value: string): number {
59
+ const num = parseInt(value, 10)
60
+ return isNaN(num) ? NaN : num
61
+ }
62
+
63
+ function applyConversions(data: Uint8Array, conversions: string[]): Uint8Array {
64
+ let result = new Uint8Array(data)
65
+
66
+ for (const conv of conversions) {
67
+ switch (conv) {
68
+ case 'ucase': {
69
+ const text = new TextDecoder().decode(result)
70
+ result = new TextEncoder().encode(text.toUpperCase())
71
+ break
72
+ }
73
+ case 'lcase': {
74
+ const text = new TextDecoder().decode(result)
75
+ result = new TextEncoder().encode(text.toLowerCase())
76
+ break
77
+ }
78
+ case 'swab': {
79
+ const swapped = new Uint8Array(result.length)
80
+ for (let i = 0; i < result.length - 1; i += 2) {
81
+ const a = result[i]
82
+ const b = result[i + 1]
83
+ if (a !== undefined && b !== undefined) {
84
+ swapped[i] = b
85
+ swapped[i + 1] = a
86
+ }
87
+ }
88
+ if (result.length % 2 === 1) {
89
+ const last = result[result.length - 1]
90
+ if (last !== undefined) {
91
+ swapped[result.length - 1] = last
92
+ }
93
+ }
94
+ result = swapped
95
+ break
96
+ }
97
+ }
98
+ }
99
+
100
+ return result
101
+ }
102
+
103
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
104
+ return new TerminalCommand({
105
+ command: 'dd',
106
+ description: 'Copy and convert files with block-level operations',
107
+ kernel,
108
+ shell,
109
+ terminal,
110
+ run: async (pid: number, argv: string[]) => {
111
+ const process = kernel.processes.get(pid) as Process | undefined
112
+
113
+ if (!process) return 1
114
+
115
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
116
+ printUsage(process, terminal)
117
+ return 0
118
+ }
119
+
120
+ let inputFile: string | undefined
121
+ let outputFile: string | undefined
122
+ let blockSize: number | undefined
123
+ let inputBlockSize = 512
124
+ let outputBlockSize = 512
125
+ let count: number | undefined
126
+ let skip = 0
127
+ let seek = 0
128
+ const conversions: string[] = []
129
+ let status: 'none' | 'noxfer' | 'progress' = 'noxfer'
130
+ let noError = false
131
+ let noTrunc = false
132
+ let sync = false
133
+
134
+ for (const arg of argv) {
135
+ if (!arg) continue
136
+
137
+ if (arg === '--help' || arg === '-h') {
138
+ printUsage(process, terminal)
139
+ return 0
140
+ } else if (arg.startsWith('if=')) {
141
+ inputFile = arg.slice(3)
142
+ } else if (arg.startsWith('of=')) {
143
+ outputFile = arg.slice(3)
144
+ } else if (arg.startsWith('bs=')) {
145
+ const bytes = parseBytes(arg.slice(3))
146
+ if (isNaN(bytes)) {
147
+ await writelnStderr(process, terminal, `dd: invalid block size: ${arg.slice(3)}`)
148
+ return 1
149
+ }
150
+ blockSize = bytes
151
+ inputBlockSize = bytes
152
+ outputBlockSize = bytes
153
+ } else if (arg.startsWith('ibs=')) {
154
+ const bytes = parseBytes(arg.slice(4))
155
+ if (isNaN(bytes)) {
156
+ await writelnStderr(process, terminal, `dd: invalid input block size: ${arg.slice(4)}`)
157
+ return 1
158
+ }
159
+ inputBlockSize = bytes
160
+ } else if (arg.startsWith('obs=')) {
161
+ const bytes = parseBytes(arg.slice(4))
162
+ if (isNaN(bytes)) {
163
+ await writelnStderr(process, terminal, `dd: invalid output block size: ${arg.slice(4)}`)
164
+ return 1
165
+ }
166
+ outputBlockSize = bytes
167
+ } else if (arg.startsWith('count=')) {
168
+ const blocks = parseBlocks(arg.slice(6))
169
+ if (isNaN(blocks)) {
170
+ await writelnStderr(process, terminal, `dd: invalid count: ${arg.slice(6)}`)
171
+ return 1
172
+ }
173
+ count = blocks
174
+ } else if (arg.startsWith('skip=')) {
175
+ const blocks = parseBlocks(arg.slice(5))
176
+ if (isNaN(blocks)) {
177
+ await writelnStderr(process, terminal, `dd: invalid skip: ${arg.slice(5)}`)
178
+ return 1
179
+ }
180
+ skip = blocks
181
+ } else if (arg.startsWith('seek=')) {
182
+ const blocks = parseBlocks(arg.slice(5))
183
+ if (isNaN(blocks)) {
184
+ await writelnStderr(process, terminal, `dd: invalid seek: ${arg.slice(5)}`)
185
+ return 1
186
+ }
187
+ seek = blocks
188
+ } else if (arg.startsWith('conv=')) {
189
+ const convs = arg.slice(5).split(',').map(c => c.trim())
190
+ for (const conv of convs) {
191
+ if (['ucase', 'lcase', 'swab', 'noerror', 'notrunc', 'sync'].includes(conv)) {
192
+ if (conv === 'noerror') noError = true
193
+ else if (conv === 'notrunc') noTrunc = true
194
+ else if (conv === 'sync') sync = true
195
+ else conversions.push(conv)
196
+ } else {
197
+ await writelnStderr(process, terminal, `dd: invalid conversion: ${conv}`)
198
+ return 1
199
+ }
200
+ }
201
+ } else if (arg.startsWith('status=')) {
202
+ const level = arg.slice(7)
203
+ if (['none', 'noxfer', 'progress'].includes(level)) {
204
+ status = level as 'none' | 'noxfer' | 'progress'
205
+ } else {
206
+ await writelnStderr(process, terminal, `dd: invalid status level: ${level}`)
207
+ return 1
208
+ }
209
+ } else {
210
+ await writelnStderr(process, terminal, `dd: invalid operand: ${arg}`)
211
+ await writelnStderr(process, terminal, "Try 'dd --help' for more information.")
212
+ return 1
213
+ }
214
+ }
215
+
216
+ if (blockSize !== undefined) {
217
+ inputBlockSize = blockSize
218
+ outputBlockSize = blockSize
219
+ }
220
+
221
+ let inputFileHandle: Awaited<ReturnType<typeof shell.context.fs.promises.open>> | undefined
222
+ let inputReader: ReadableStreamDefaultReader<Uint8Array> | undefined
223
+ let outputFileHandle: Awaited<ReturnType<typeof shell.context.fs.promises.open>> | undefined
224
+ let outputWriter: WritableStreamDefaultWriter<Uint8Array> | { write: (chunk: Uint8Array) => Promise<void>, releaseLock: () => Promise<void> } | undefined
225
+
226
+ try {
227
+ if (inputFile) {
228
+ const inputPath = path.resolve(shell.cwd, inputFile)
229
+ const isDevice = inputPath.startsWith('/dev')
230
+ inputFileHandle = await shell.context.fs.promises.open(inputPath, isDevice ? undefined : 'r')
231
+ } else {
232
+ if (!process.stdin) {
233
+ await writelnStderr(process, terminal, 'dd: stdin not available')
234
+ return 1
235
+ }
236
+ inputReader = process.stdin.getReader()
237
+ }
238
+
239
+ if (outputFile) {
240
+ const outputPath = path.resolve(shell.cwd, outputFile)
241
+ const flags = noTrunc ? 'r+' : 'w'
242
+ outputFileHandle = await shell.context.fs.promises.open(outputPath, flags).catch(async () => {
243
+ if (noTrunc) {
244
+ return await shell.context.fs.promises.open(outputPath, 'w')
245
+ }
246
+ throw new Error(`Cannot open ${outputFile}`)
247
+ })
248
+
249
+ let outputPosition = 0
250
+ if (seek > 0) {
251
+ outputPosition = seek * outputBlockSize
252
+ const zeros = new Uint8Array(outputPosition)
253
+ await outputFileHandle.write(zeros)
254
+ }
255
+
256
+ outputWriter = {
257
+ write: async (chunk: Uint8Array) => {
258
+ await outputFileHandle!.write(chunk)
259
+ },
260
+ releaseLock: async () => {
261
+ await outputFileHandle!.close()
262
+ }
263
+ }
264
+ } else {
265
+ if (!process.stdout) {
266
+ await writelnStderr(process, terminal, 'dd: stdout not available')
267
+ return 1
268
+ }
269
+ outputWriter = process.stdout.getWriter()
270
+
271
+ if (seek > 0 && outputWriter) {
272
+ const seekBytes = seek * outputBlockSize
273
+ const zeros = new Uint8Array(seekBytes)
274
+ await outputWriter.write(zeros)
275
+ }
276
+ }
277
+
278
+ if (!outputWriter) {
279
+ await writelnStderr(process, terminal, 'dd: no output writer available')
280
+ return 1
281
+ }
282
+
283
+ let totalBytesRead = 0
284
+ let totalBytesWritten = 0
285
+ let blocksRead = 0
286
+ let blocksWritten = 0
287
+
288
+ if (inputFileHandle && inputFile) {
289
+ const inputPath = path.resolve(shell.cwd, inputFile)
290
+ const isDevice = inputPath.startsWith('/dev')
291
+
292
+ if (isDevice) {
293
+ const buffer = new Uint8Array(inputBlockSize)
294
+ let skipBytes = skip * inputBlockSize
295
+ let skipped = 0
296
+
297
+ while (true) {
298
+ if (count !== undefined && blocksRead >= count) {
299
+ break
300
+ }
301
+
302
+ const result = await inputFileHandle.read(buffer)
303
+ const bytesRead = result.bytesRead
304
+
305
+ if (bytesRead === 0) {
306
+ if (sync && blocksRead > 0) {
307
+ const padded = new Uint8Array(inputBlockSize)
308
+ let data: Uint8Array = padded
309
+ if (conversions.length > 0) {
310
+ data = applyConversions(data, conversions)
311
+ }
312
+ await outputWriter.write(data)
313
+ totalBytesWritten += data.length
314
+ blocksWritten++
315
+ }
316
+ break
317
+ }
318
+
319
+ let data = buffer.subarray(0, bytesRead)
320
+
321
+ if (skipBytes > 0) {
322
+ const toSkip = Math.min(data.length, skipBytes - skipped)
323
+ skipped += toSkip
324
+ if (toSkip < data.length) {
325
+ data = data.subarray(toSkip)
326
+ } else {
327
+ continue
328
+ }
329
+ }
330
+
331
+ totalBytesRead += data.length
332
+ blocksRead++
333
+
334
+ if (data.length < inputBlockSize && sync) {
335
+ const padded = new Uint8Array(inputBlockSize)
336
+ padded.set(data)
337
+ data = padded
338
+ }
339
+
340
+ if (conversions.length > 0) {
341
+ const converted = applyConversions(data, conversions)
342
+ data = new Uint8Array(converted)
343
+ }
344
+
345
+ if (data.length > outputBlockSize) {
346
+ let offset = 0
347
+ while (offset < data.length) {
348
+ const chunk = data.slice(offset, offset + outputBlockSize)
349
+ await outputWriter.write(chunk)
350
+ totalBytesWritten += chunk.length
351
+ blocksWritten++
352
+ offset += outputBlockSize
353
+ }
354
+ } else {
355
+ await outputWriter.write(data)
356
+ totalBytesWritten += data.length
357
+ blocksWritten++
358
+ }
359
+
360
+ if (status === 'progress' && blocksRead % 100 === 0) {
361
+ await writelnStderr(process, terminal, `dd: ${blocksRead} blocks read, ${blocksWritten} blocks written`)
362
+ }
363
+ }
364
+ } else {
365
+ const stat = await shell.context.fs.promises.stat(inputPath)
366
+ const fileSize = stat.size
367
+ let inputPosition = 0
368
+
369
+ if (skip > 0) {
370
+ inputPosition = skip * inputBlockSize
371
+ if (inputPosition > fileSize) {
372
+ inputPosition = fileSize
373
+ }
374
+ }
375
+
376
+ const buffer = new Uint8Array(inputBlockSize)
377
+
378
+ while (true) {
379
+ if (count !== undefined && blocksRead >= count) {
380
+ break
381
+ }
382
+
383
+ if (inputPosition >= fileSize) {
384
+ if (sync && blocksRead > 0) {
385
+ const padded = new Uint8Array(inputBlockSize)
386
+ let data: Uint8Array = padded
387
+ if (conversions.length > 0) {
388
+ data = applyConversions(data, conversions)
389
+ }
390
+ await outputWriter.write(data)
391
+ totalBytesWritten += data.length
392
+ blocksWritten++
393
+ }
394
+ break
395
+ }
396
+
397
+ const bytesToRead = Math.min(inputBlockSize, fileSize - inputPosition)
398
+ await inputFileHandle.read(buffer, 0, bytesToRead, inputPosition)
399
+ let data = new Uint8Array(buffer.buffer, buffer.byteOffset, bytesToRead)
400
+
401
+ totalBytesRead += data.length
402
+ blocksRead++
403
+ inputPosition += bytesToRead
404
+
405
+ if (data.length < inputBlockSize && sync) {
406
+ const padded = new Uint8Array(inputBlockSize)
407
+ padded.set(data)
408
+ data = padded
409
+ }
410
+
411
+ if (conversions.length > 0) {
412
+ const converted = applyConversions(data, conversions)
413
+ data = new Uint8Array(converted)
414
+ }
415
+
416
+ if (data.length > outputBlockSize) {
417
+ let offset = 0
418
+ while (offset < data.length) {
419
+ const chunk = data.slice(offset, offset + outputBlockSize)
420
+ await outputWriter.write(chunk)
421
+ totalBytesWritten += chunk.length
422
+ blocksWritten++
423
+ offset += outputBlockSize
424
+ }
425
+ } else {
426
+ await outputWriter.write(data)
427
+ totalBytesWritten += data.length
428
+ blocksWritten++
429
+ }
430
+
431
+ if (status === 'progress' && blocksRead % 100 === 0) {
432
+ await writelnStderr(process, terminal, `dd: ${blocksRead} blocks read, ${blocksWritten} blocks written`)
433
+ }
434
+ }
435
+ }
436
+ } else if (inputReader) {
437
+ if (skip > 0) {
438
+ const skipBytes = skip * inputBlockSize
439
+ let skipped = 0
440
+ while (skipped < skipBytes) {
441
+ const { done, value } = await inputReader.read()
442
+ if (done) break
443
+ if (value) {
444
+ skipped += value.length
445
+ }
446
+ }
447
+ }
448
+
449
+ let partialBlock: Uint8Array | undefined
450
+
451
+ while (true) {
452
+ if (count !== undefined && blocksRead >= count) {
453
+ break
454
+ }
455
+
456
+ let data: Uint8Array | undefined
457
+
458
+ if (partialBlock) {
459
+ data = partialBlock
460
+ partialBlock = undefined
461
+ } else {
462
+ const result = await inputReader.read()
463
+ if (result.done) {
464
+ if (sync && blocksRead > 0) {
465
+ const padded = new Uint8Array(inputBlockSize)
466
+ data = padded
467
+ } else {
468
+ break
469
+ }
470
+ } else {
471
+ data = result.value
472
+ }
473
+ }
474
+
475
+ if (!data || data.length === 0) {
476
+ if (sync && blocksRead > 0) {
477
+ data = new Uint8Array(inputBlockSize)
478
+ } else {
479
+ break
480
+ }
481
+ }
482
+
483
+ totalBytesRead += data.length
484
+ blocksRead++
485
+
486
+ if (data.length < inputBlockSize && sync) {
487
+ const padded = new Uint8Array(inputBlockSize)
488
+ padded.set(data)
489
+ data = padded
490
+ }
491
+
492
+ if (conversions.length > 0) {
493
+ data = applyConversions(data, conversions)
494
+ }
495
+
496
+ if (!data) {
497
+ break
498
+ }
499
+
500
+ if (data.length > outputBlockSize) {
501
+ let offset = 0
502
+ while (offset < data.length) {
503
+ const chunk = data.slice(offset, offset + outputBlockSize)
504
+ await outputWriter.write(chunk)
505
+ totalBytesWritten += chunk.length
506
+ blocksWritten++
507
+ offset += outputBlockSize
508
+ }
509
+ } else {
510
+ await outputWriter.write(data)
511
+ totalBytesWritten += data.length
512
+ blocksWritten++
513
+ }
514
+
515
+ if (status === 'progress' && blocksRead % 100 === 0) {
516
+ await writelnStderr(process, terminal, `dd: ${blocksRead} blocks read, ${blocksWritten} blocks written`)
517
+ }
518
+ }
519
+ }
520
+
521
+ if (status !== 'none') {
522
+ await writelnStderr(process, terminal, `${blocksRead}+${Math.floor((totalBytesRead % inputBlockSize) / (inputBlockSize || 1))} records in`)
523
+ await writelnStderr(process, terminal, `${blocksWritten}+${Math.floor((totalBytesWritten % outputBlockSize) / (outputBlockSize || 1))} records out`)
524
+ await writelnStderr(process, terminal, `${totalBytesWritten} bytes copied`)
525
+ }
526
+
527
+ return 0
528
+ } catch (error) {
529
+ if (!noError) {
530
+ await writelnStderr(process, terminal, `dd: ${error instanceof Error ? error.message : 'Unknown error'}`)
531
+ return 1
532
+ }
533
+ return 0
534
+ } finally {
535
+ if (inputFileHandle) {
536
+ await inputFileHandle.close()
537
+ }
538
+ if (inputReader) {
539
+ inputReader.releaseLock()
540
+ }
541
+ if (outputWriter && 'releaseLock' in outputWriter) {
542
+ await outputWriter.releaseLock()
543
+ }
544
+ }
545
+ }
546
+ })
547
+ }
@@ -5,7 +5,6 @@ import humanFormat from 'human-format'
5
5
  import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
6
6
  import { TerminalCommand } from '../shared/terminal-command.js'
7
7
  import { writelnStdout, writelnStderr } from '../shared/helpers.js'
8
- import { CoreutilsDescriptions } from '../index.js'
9
8
 
10
9
  function printUsage(process: Process | undefined, terminal: Terminal): void {
11
10
  const usage = `Usage: ls [OPTION]... [FILE]...
@@ -64,8 +63,6 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
64
63
 
65
64
  if (targets.length === 0) targets.push(shell.cwd)
66
65
 
67
- const descriptions = kernel.filesystem.descriptions(kernel.i18n.t)
68
-
69
66
  // Process each target and collect all entries
70
67
  // We'll determine if each entry is a directory when we stat it later
71
68
  const allEntries: Array<{ fullPath: string, entry: string }> = []
@@ -138,6 +135,8 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
138
135
  return ''
139
136
  }
140
137
 
138
+ const descriptions = kernel.filesystem.descriptions(kernel.i18n.ns.filesystem)
139
+
141
140
  const filesMap = await Promise.all(allEntries
142
141
  .map(async ({ fullPath: entryFullPath, entry }) => {
143
142
  const target = path.resolve(entryFullPath, entry)
@@ -209,7 +208,9 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
209
208
 
210
209
  // Check if any entry is in /dev directory
211
210
  const isDevDirectory = allEntries.some(e => e.fullPath.startsWith('/dev'))
212
- const columns = isDevDirectory ? ['Name', 'Mode', 'Owner', 'Info'] : ['Name', 'Size', 'Modified', 'Mode', 'Owner', 'Info']
211
+ const columns = isDevDirectory
212
+ ? [kernel.i18n.ns.common('Name'), kernel.i18n.ns.common('Mode'), kernel.i18n.ns.common('Owner'), kernel.i18n.ns.common('Info')]
213
+ : [kernel.i18n.ns.common('Name'), kernel.i18n.ns.common('Size'), kernel.i18n.ns.common('Modified'), kernel.i18n.ns.common('Mode'), kernel.i18n.ns.common('Owner'), kernel.i18n.ns.common('Info')]
213
214
 
214
215
  const directoryRows = directories.sort((a, b) => a.name.localeCompare(b.name)).map(directory => {
215
216
  const displayName = directory.linkTarget
@@ -229,15 +230,15 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
229
230
  : chalk.green(displayName)
230
231
 
231
232
  const row: Record<string, string> = {
232
- Name: coloredName,
233
- Mode: chalk.gray(modeString),
234
- Owner: directory.stats ? chalk.gray(getOwnerString(directory.stats)) : '',
235
- Info: truncateInfo(chalk.gray(linkInfo))
233
+ [kernel.i18n.ns.common('Name')]: coloredName,
234
+ [kernel.i18n.ns.common('Mode')]: chalk.gray(modeString),
235
+ [kernel.i18n.ns.common('Owner')]: directory.stats ? chalk.gray(getOwnerString(directory.stats)) : '',
236
+ [kernel.i18n.ns.common('Info')]: truncateInfo(chalk.gray(linkInfo))
236
237
  }
237
238
 
238
239
  if (!isDevDirectory) {
239
- row.Size = ''
240
- row.Modified = directory.stats ? chalk.gray(getTimestampString(directory.stats.mtime)) : ''
240
+ row[kernel.i18n.ns.common('Size')] = ''
241
+ row[kernel.i18n.ns.common('Modified')] = directory.stats ? chalk.gray(getTimestampString(directory.stats.mtime)) : ''
241
242
  }
242
243
 
243
244
  return row
@@ -263,11 +264,13 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
263
264
  const linkInfo = getLinkInfo(file.linkTarget, file.linkStats, file.stats)
264
265
  if (linkInfo) return linkInfo
265
266
 
266
- // Check if this is a command in /bin/ and use CoreutilsDescriptions
267
+ // Check if this is a command in /bin/ and use coreutils translations
267
268
  if (file.target.startsWith('/bin/')) {
268
269
  const commandName = path.basename(file.target)
269
- if (commandName in CoreutilsDescriptions) {
270
- return CoreutilsDescriptions[commandName as keyof typeof CoreutilsDescriptions]
270
+ const translatedDescription = kernel.i18n.ns.coreutils(commandName)
271
+ // Only use translation if it exists (i18next returns the key if translation is missing)
272
+ if (translatedDescription !== commandName) {
273
+ return translatedDescription
271
274
  }
272
275
  }
273
276
 
@@ -278,22 +281,25 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
278
281
  }
279
282
  if (!file.stats) return ''
280
283
  if (file.stats.isBlockDevice() || file.stats.isCharacterDevice()) {
281
- // TODO: zenfs `fs.mounts` is deprecated - use a better way of getting device info
284
+ const devicePackage = kernel.devices.get(path.basename(file.target))
285
+ const description = devicePackage?.device?.pkg?.description || ''
286
+ const hasCLI = devicePackage?.device?.cli !== undefined
287
+ return `${description}${hasCLI ? ' ' + `${chalk.bold(`(${kernel.i18n.t('CLI')})`)}${chalk.reset()}` : ''}`
282
288
  }
283
289
 
284
290
  return ''
285
291
  })()
286
292
 
287
293
  const row: Record<string, string> = {
288
- Name: coloredName,
289
- Mode: chalk.gray(modeString),
290
- Owner: file.stats ? chalk.gray(getOwnerString(file.stats)) : '',
291
- Info: truncateInfo(chalk.gray(info))
294
+ [kernel.i18n.ns.common('Name')]: coloredName,
295
+ [kernel.i18n.ns.common('Mode')]: chalk.gray(modeString),
296
+ [kernel.i18n.ns.common('Owner')]: file.stats ? chalk.gray(getOwnerString(file.stats)) : '',
297
+ [kernel.i18n.ns.common('Info')]: truncateInfo(chalk.gray(info))
292
298
  }
293
299
 
294
300
  if (!isDevDirectory) {
295
- row.Size = file.stats ? chalk.gray(humanFormat(file.stats.size)) : ''
296
- row.Modified = file.stats ? chalk.gray(getTimestampString(file.stats.mtime)) : ''
301
+ row[kernel.i18n.ns.common('Size')] = file.stats ? chalk.gray(humanFormat(file.stats.size)) : ''
302
+ row[kernel.i18n.ns.common('Modified')] = file.stats ? chalk.gray(getTimestampString(file.stats.mtime)) : ''
297
303
  }
298
304
 
299
305
  return row