@ecmaos/coreutils 0.5.0 → 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.
@@ -14,6 +14,33 @@ List information about the FILEs (the current directory by default).
14
14
  writelnStderr(process, terminal, usage)
15
15
  }
16
16
 
17
+ function truncateInfo(text: string, maxWidth: number = 40): string {
18
+ if (!text) return text
19
+
20
+ // Strip ANSI codes to get visible length
21
+ const ansiRegex = /\x1b\[[0-9;]*m/g
22
+ const plainText = text.replace(ansiRegex, '')
23
+
24
+ if (plainText.length <= maxWidth) return text
25
+
26
+ // Extract all ANSI codes from the original text
27
+ const codes: string[] = []
28
+ let match
29
+ const codeRegex = /\x1b\[[0-9;]*m/g
30
+ while ((match = codeRegex.exec(text)) !== null) {
31
+ codes.push(match[0])
32
+ }
33
+
34
+ // Truncate plain text and add ellipsis
35
+ const truncated = plainText.substring(0, maxWidth - 3)
36
+
37
+ // Reconstruct with original color codes at the start
38
+ const prefixCodes = codes.length > 0 ? codes[0] : ''
39
+ const resetCode = '\x1b[0m'
40
+
41
+ return `${prefixCodes}${truncated}...${resetCode}`
42
+ }
43
+
17
44
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
18
45
  return new TerminalCommand({
19
46
  command: 'ls',
@@ -36,8 +63,6 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
36
63
 
37
64
  if (targets.length === 0) targets.push(shell.cwd)
38
65
 
39
- const descriptions = kernel.filesystem.descriptions(kernel.i18n.t)
40
-
41
66
  // Process each target and collect all entries
42
67
  // We'll determine if each entry is a directory when we stat it later
43
68
  const allEntries: Array<{ fullPath: string, entry: string }> = []
@@ -110,6 +135,8 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
110
135
  return ''
111
136
  }
112
137
 
138
+ const descriptions = kernel.filesystem.descriptions(kernel.i18n.ns.filesystem)
139
+
113
140
  const filesMap = await Promise.all(allEntries
114
141
  .map(async ({ fullPath: entryFullPath, entry }) => {
115
142
  const target = path.resolve(entryFullPath, entry)
@@ -181,7 +208,9 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
181
208
 
182
209
  // Check if any entry is in /dev directory
183
210
  const isDevDirectory = allEntries.some(e => e.fullPath.startsWith('/dev'))
184
- 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')]
185
214
 
186
215
  const directoryRows = directories.sort((a, b) => a.name.localeCompare(b.name)).map(directory => {
187
216
  const displayName = directory.linkTarget
@@ -201,15 +230,15 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
201
230
  : chalk.green(displayName)
202
231
 
203
232
  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)
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))
208
237
  }
209
238
 
210
239
  if (!isDevDirectory) {
211
- row.Size = ''
212
- 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)) : ''
213
242
  }
214
243
 
215
244
  return row
@@ -235,6 +264,16 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
235
264
  const linkInfo = getLinkInfo(file.linkTarget, file.linkStats, file.stats)
236
265
  if (linkInfo) return linkInfo
237
266
 
267
+ // Check if this is a command in /bin/ and use coreutils translations
268
+ if (file.target.startsWith('/bin/')) {
269
+ const commandName = path.basename(file.target)
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
274
+ }
275
+ }
276
+
238
277
  if (descriptions.has(file.target)) return descriptions.get(file.target) || ''
239
278
  if (file.name.includes('.')) {
240
279
  const ext = file.name.split('.').pop()
@@ -242,22 +281,25 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
242
281
  }
243
282
  if (!file.stats) return ''
244
283
  if (file.stats.isBlockDevice() || file.stats.isCharacterDevice()) {
245
- // 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()}` : ''}`
246
288
  }
247
289
 
248
290
  return ''
249
291
  })()
250
292
 
251
293
  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)
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))
256
298
  }
257
299
 
258
300
  if (!isDevDirectory) {
259
- row.Size = file.stats ? chalk.gray(humanFormat(file.stats.size)) : ''
260
- 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)) : ''
261
303
  }
262
304
 
263
305
  return row
@@ -1,16 +1,31 @@
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: mkdir [OPTION]... DIRECTORY...
8
8
  Create the DIRECTORY(ies), if they do not already exist.
9
9
 
10
- --help display this help and exit`
10
+ Mandatory arguments to long options are mandatory for short options too.
11
+ -m, --mode=MODE set file mode (as in chmod), not a=rwx - umask
12
+ -p, --parents no error if existing, make parent directories as needed,
13
+ with their file modes unaffected by any -m option.
14
+ -v, --verbose print a message for each created directory
15
+ --help display this help and exit`
11
16
  writelnStderr(process, terminal, usage)
12
17
  }
13
18
 
19
+ function parseNumericMode(mode: string): number | null {
20
+ if (/^0?[0-7]{1,4}$/.test(mode)) {
21
+ return parseInt(mode, 8)
22
+ }
23
+ if (/^0o[0-7]{1,4}$/i.test(mode)) {
24
+ return parseInt(mode.slice(2), 8)
25
+ }
26
+ return null
27
+ }
28
+
14
29
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
15
30
  return new TerminalCommand({
16
31
  command: 'mkdir',
@@ -26,7 +41,104 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
26
41
  return 0
27
42
  }
28
43
 
29
- if (argv.length === 0) {
44
+ let parents = false
45
+ let verbose = false
46
+ let mode: number | undefined = undefined
47
+ const directories: string[] = []
48
+
49
+ let i = 0
50
+ while (i < argv.length) {
51
+ const arg = argv[i]
52
+ if (!arg) {
53
+ i++
54
+ continue
55
+ }
56
+
57
+ if (arg === '--') {
58
+ i++
59
+ while (i < argv.length) {
60
+ const dirArg = argv[i]
61
+ if (dirArg) {
62
+ directories.push(dirArg)
63
+ }
64
+ i++
65
+ }
66
+ break
67
+ }
68
+
69
+ if (arg.startsWith('--')) {
70
+ if (arg === '--parents') {
71
+ parents = true
72
+ } else if (arg === '--verbose') {
73
+ verbose = true
74
+ } else if (arg.startsWith('--mode=')) {
75
+ const modeStr = arg.slice(7)
76
+ const parsedMode = parseNumericMode(modeStr)
77
+ if (parsedMode === null) {
78
+ await writelnStderr(process, terminal, `mkdir: invalid mode '${modeStr}'`)
79
+ return 1
80
+ }
81
+ mode = parsedMode
82
+ } else if (arg === '--help' || arg === '-h') {
83
+ printUsage(process, terminal)
84
+ return 0
85
+ } else {
86
+ await writelnStderr(process, terminal, `mkdir: unrecognized option '${arg}'`)
87
+ await writelnStderr(process, terminal, "Try 'mkdir --help' for more information.")
88
+ return 1
89
+ }
90
+ } else if (arg.startsWith('-') && arg.length > 1) {
91
+ for (let j = 1; j < arg.length; j++) {
92
+ const flag = arg[j]
93
+ if (!flag) continue
94
+
95
+ if (flag === 'p') {
96
+ parents = true
97
+ } else if (flag === 'v') {
98
+ verbose = true
99
+ } else if (flag === 'm') {
100
+ if (j + 1 < arg.length) {
101
+ const modeStr = arg.slice(j + 1)
102
+ const parsedMode = parseNumericMode(modeStr)
103
+ if (parsedMode === null) {
104
+ await writelnStderr(process, terminal, `mkdir: invalid mode '${modeStr}'`)
105
+ return 1
106
+ }
107
+ mode = parsedMode
108
+ break
109
+ } else if (i + 1 < argv.length) {
110
+ const modeStr = argv[i + 1]
111
+ if (!modeStr) {
112
+ await writelnStderr(process, terminal, "mkdir: option requires an argument -- 'm'")
113
+ await writelnStderr(process, terminal, "Try 'mkdir --help' for more information.")
114
+ return 1
115
+ }
116
+ const parsedMode = parseNumericMode(modeStr)
117
+ if (parsedMode === null) {
118
+ await writelnStderr(process, terminal, `mkdir: invalid mode '${modeStr}'`)
119
+ return 1
120
+ }
121
+ mode = parsedMode
122
+ i++
123
+ break
124
+ } else {
125
+ await writelnStderr(process, terminal, "mkdir: option requires an argument -- 'm'")
126
+ await writelnStderr(process, terminal, "Try 'mkdir --help' for more information.")
127
+ return 1
128
+ }
129
+ } else {
130
+ await writelnStderr(process, terminal, `mkdir: invalid option -- '${flag}'`)
131
+ await writelnStderr(process, terminal, "Try 'mkdir --help' for more information.")
132
+ return 1
133
+ }
134
+ }
135
+ } else {
136
+ directories.push(arg)
137
+ }
138
+ i++
139
+ }
140
+
141
+ if (directories.length === 0) {
30
142
  await writelnStderr(process, terminal, 'mkdir: missing operand')
31
143
  await writelnStderr(process, terminal, "Try 'mkdir --help' for more information.")
32
144
  return 1
@@ -34,17 +146,45 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
34
146
 
35
147
  let hasError = false
36
148
 
37
- for (const target of argv) {
38
- if (!target || target.startsWith('-')) continue
149
+ for (const target of directories) {
150
+ if (!target) continue
39
151
 
40
- const fullPath = target ? path.resolve(shell.cwd, target) : shell.cwd
152
+ const fullPath = path.resolve(shell.cwd, target)
41
153
 
42
154
  try {
43
- await shell.context.fs.promises.mkdir(fullPath)
155
+ const mkdirOptions: { recursive?: boolean; mode?: number } = {}
156
+ if (parents) {
157
+ mkdirOptions.recursive = true
158
+ }
159
+ if (mode !== undefined) {
160
+ mkdirOptions.mode = mode
161
+ }
162
+
163
+ let existedBefore = false
164
+ if (parents) {
165
+ try {
166
+ await shell.context.fs.promises.stat(fullPath)
167
+ existedBefore = true
168
+ } catch {
169
+ existedBefore = false
170
+ }
171
+ }
172
+
173
+ await shell.context.fs.promises.mkdir(fullPath, mkdirOptions)
174
+
175
+ if (verbose && !existedBefore) {
176
+ const relativePath = path.relative(shell.cwd, fullPath) || target
177
+ await writelnStdout(process, terminal, `mkdir: created directory '${relativePath}'`)
178
+ }
44
179
  } catch (error) {
45
- const errorMessage = error instanceof Error ? error.message : String(error)
46
- await writelnStderr(process, terminal, `mkdir: ${target}: ${errorMessage}`)
47
- hasError = true
180
+ const err = error as { code?: string; message?: string }
181
+ if (parents && err.code === 'EEXIST') {
182
+ continue
183
+ } else {
184
+ const errorMessage = error instanceof Error ? error.message : String(error)
185
+ await writelnStderr(process, terminal, `mkdir: ${target}: ${errorMessage}`)
186
+ hasError = true
187
+ }
48
188
  }
49
189
  }
50
190
 
@@ -236,9 +236,9 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
236
236
  const trimmed = arg.trim()
237
237
  return (
238
238
  trimmed.startsWith('s/') ||
239
- trimmed.startsWith('/') ||
240
- /^\d+[sd]/.test(trimmed) ||
241
- /^\d+,\d*[sd]/.test(trimmed) ||
239
+ /^\/.+?\/[dp]$/.test(trimmed) ||
240
+ /^\d+[sd]$/.test(trimmed) ||
241
+ /^\d+,\d*[sd]$/.test(trimmed) ||
242
242
  /^\d+s\//.test(trimmed) ||
243
243
  /^\d+,\d*s\//.test(trimmed)
244
244
  )
@@ -341,73 +341,6 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
341
341
  return content.split('\n')
342
342
  }
343
343
 
344
- let inputLines: string[] = []
345
-
346
- if (files.length > 0) {
347
- for (const file of files) {
348
- const expandedPath = shell.expandTilde(file)
349
- const fullPath = path.resolve(shell.cwd, expandedPath)
350
- const lines = await processFile(fullPath)
351
- inputLines.push(...lines)
352
- if (lines.length > 0 && inputLines.length > lines.length) {
353
- inputLines.push('')
354
- }
355
- }
356
- } else {
357
- if (!process.stdin) {
358
- await writelnStderr(process, terminal, 'sed: No input provided')
359
- return 1
360
- }
361
-
362
- const reader = process.stdin.getReader()
363
- const decoder = new TextDecoder()
364
- const chunks: string[] = []
365
-
366
- try {
367
- while (true) {
368
- const { done, value } = await reader.read()
369
- if (done) break
370
- chunks.push(decoder.decode(value, { stream: true }))
371
- }
372
- chunks.push(decoder.decode(new Uint8Array(), { stream: false }))
373
- } finally {
374
- reader.releaseLock()
375
- }
376
-
377
- const content = chunks.join('')
378
- inputLines = content.split('\n')
379
- }
380
-
381
- const outputLines: string[] = []
382
- const totalLines = inputLines.length
383
-
384
- for (let i = 0; i < inputLines.length; i++) {
385
- let line = inputLines[i] || ''
386
- let lineNum = i + 1
387
- let shouldPrint = false
388
-
389
- for (const command of commands) {
390
- const { result, shouldPrint: print } = applySedCommand(line, lineNum, totalLines, command)
391
- if (result === null) {
392
- line = null as unknown as string
393
- break
394
- }
395
- line = result
396
- if (print) {
397
- shouldPrint = true
398
- }
399
- }
400
-
401
- if (line !== null) {
402
- outputLines.push(line)
403
- if (shouldPrint && !quiet) {
404
- outputLines.push(line)
405
- }
406
- }
407
- }
408
-
409
- const output = outputLines.join('\n')
410
-
411
344
  if (inplace !== undefined && files.length > 0) {
412
345
  for (const file of files) {
413
346
  const expandedPath = shell.expandTilde(file)
@@ -455,6 +388,72 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
455
388
  await shell.context.fs.promises.writeFile(fullPath, fileOutput)
456
389
  }
457
390
  } else {
391
+ let inputLines: string[] = []
392
+
393
+ if (files.length > 0) {
394
+ for (const file of files) {
395
+ const expandedPath = shell.expandTilde(file)
396
+ const fullPath = path.resolve(shell.cwd, expandedPath)
397
+ const lines = await processFile(fullPath)
398
+ inputLines.push(...lines)
399
+ if (lines.length > 0 && inputLines.length > lines.length) {
400
+ inputLines.push('')
401
+ }
402
+ }
403
+ } else {
404
+ if (!process.stdin) {
405
+ await writelnStderr(process, terminal, 'sed: No input provided')
406
+ return 1
407
+ }
408
+
409
+ const reader = process.stdin.getReader()
410
+ const decoder = new TextDecoder()
411
+ const chunks: string[] = []
412
+
413
+ try {
414
+ while (true) {
415
+ const { done, value } = await reader.read()
416
+ if (done) break
417
+ chunks.push(decoder.decode(value, { stream: true }))
418
+ }
419
+ chunks.push(decoder.decode(new Uint8Array(), { stream: false }))
420
+ } finally {
421
+ reader.releaseLock()
422
+ }
423
+
424
+ const content = chunks.join('')
425
+ inputLines = content.split('\n')
426
+ }
427
+
428
+ const outputLines: string[] = []
429
+ const totalLines = inputLines.length
430
+
431
+ for (let i = 0; i < inputLines.length; i++) {
432
+ let line = inputLines[i] || ''
433
+ let lineNum = i + 1
434
+ let shouldPrint = false
435
+
436
+ for (const command of commands) {
437
+ const { result, shouldPrint: print } = applySedCommand(line, lineNum, totalLines, command)
438
+ if (result === null) {
439
+ line = null as unknown as string
440
+ break
441
+ }
442
+ line = result
443
+ if (print) {
444
+ shouldPrint = true
445
+ }
446
+ }
447
+
448
+ if (line !== null) {
449
+ outputLines.push(line)
450
+ if (shouldPrint && !quiet) {
451
+ outputLines.push(line)
452
+ }
453
+ }
454
+ }
455
+
456
+ const output = outputLines.join('\n')
458
457
  await writer.write(new TextEncoder().encode(output))
459
458
  }
460
459
 
@@ -306,7 +306,6 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
306
306
  }
307
307
 
308
308
  // Read file
309
- await writelnStdout(process, terminal, chalk.blue(`Loading: ${file}...`))
310
309
  const fileData = await shell.context.fs.promises.readFile(fullPath)
311
310
  const fileType = detectFileType(fullPath)
312
311
  const mimeType = getMimeType(fullPath, fileType)
@@ -374,8 +373,6 @@ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal):
374
373
  })
375
374
 
376
375
  win.mount(container)
377
-
378
- await writelnStdout(process, terminal, chalk.green(`Viewing: ${file}`))
379
376
  } else if (fileType === 'json') {
380
377
  // Read and parse JSON file
381
378
  const jsonText = new TextDecoder().decode(fileData)
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ import { createCommand as createColumn } from './commands/column.js'
61
61
  import { createCommand as createComm } from './commands/comm.js'
62
62
  import { createCommand as createCurl } from './commands/curl.js'
63
63
  import { createCommand as createCut } from './commands/cut.js'
64
+ import { createCommand as createDd } from './commands/dd.js'
64
65
  import { createCommand as createDate } from './commands/date.js'
65
66
  import { createCommand as createDiff } from './commands/diff.js'
66
67
  import { createCommand as createDirname } from './commands/dirname.js'
@@ -147,6 +148,7 @@ export { createCommand as createCmp } from './commands/cmp.js'
147
148
  export { createCommand as createColumn } from './commands/column.js'
148
149
  export { createCommand as createCurl } from './commands/curl.js'
149
150
  export { createCommand as createCut } from './commands/cut.js'
151
+ export { createCommand as createDd } from './commands/dd.js'
150
152
  export { createCommand as createDate } from './commands/date.js'
151
153
  export { createCommand as createDiff } from './commands/diff.js'
152
154
  export { createCommand as createDirname } from './commands/dirname.js'
@@ -233,6 +235,7 @@ export function createAllCommands(kernel: Kernel, shell: Shell, terminal: Termin
233
235
  comm: createComm(kernel, shell, terminal),
234
236
  curl: createCurl(kernel, shell, terminal),
235
237
  cut: createCut(kernel, shell, terminal),
238
+ dd: createDd(kernel, shell, terminal),
236
239
  date: createDate(kernel, shell, terminal),
237
240
  diff: createDiff(kernel, shell, terminal),
238
241
  dirname: createDirname(kernel, shell, terminal),
@@ -275,4 +278,3 @@ export function createAllCommands(kernel: Kernel, shell: Shell, terminal: Termin
275
278
 
276
279
  // For backward compatibility, export as TerminalCommands
277
280
  export { createAllCommands as TerminalCommands }
278
-