@ecmaos/coreutils 0.3.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +48 -0
  3. package/dist/commands/awk.d.ts +4 -0
  4. package/dist/commands/awk.d.ts.map +1 -0
  5. package/dist/commands/awk.js +324 -0
  6. package/dist/commands/awk.js.map +1 -0
  7. package/dist/commands/chgrp.d.ts +4 -0
  8. package/dist/commands/chgrp.d.ts.map +1 -0
  9. package/dist/commands/chgrp.js +187 -0
  10. package/dist/commands/chgrp.js.map +1 -0
  11. package/dist/commands/chmod.d.ts.map +1 -1
  12. package/dist/commands/chmod.js +139 -2
  13. package/dist/commands/chmod.js.map +1 -1
  14. package/dist/commands/chown.d.ts +4 -0
  15. package/dist/commands/chown.d.ts.map +1 -0
  16. package/dist/commands/chown.js +257 -0
  17. package/dist/commands/chown.js.map +1 -0
  18. package/dist/commands/cksum.d.ts +4 -0
  19. package/dist/commands/cksum.d.ts.map +1 -0
  20. package/dist/commands/cksum.js +124 -0
  21. package/dist/commands/cksum.js.map +1 -0
  22. package/dist/commands/cmp.d.ts +4 -0
  23. package/dist/commands/cmp.d.ts.map +1 -0
  24. package/dist/commands/cmp.js +120 -0
  25. package/dist/commands/cmp.js.map +1 -0
  26. package/dist/commands/column.d.ts +4 -0
  27. package/dist/commands/column.d.ts.map +1 -0
  28. package/dist/commands/column.js +274 -0
  29. package/dist/commands/column.js.map +1 -0
  30. package/dist/commands/cp.d.ts.map +1 -1
  31. package/dist/commands/cp.js +81 -4
  32. package/dist/commands/cp.js.map +1 -1
  33. package/dist/commands/cron.d.ts.map +1 -1
  34. package/dist/commands/cron.js +116 -23
  35. package/dist/commands/cron.js.map +1 -1
  36. package/dist/commands/curl.d.ts +4 -0
  37. package/dist/commands/curl.d.ts.map +1 -0
  38. package/dist/commands/curl.js +238 -0
  39. package/dist/commands/curl.js.map +1 -0
  40. package/dist/commands/du.d.ts +4 -0
  41. package/dist/commands/du.d.ts.map +1 -0
  42. package/dist/commands/du.js +168 -0
  43. package/dist/commands/du.js.map +1 -0
  44. package/dist/commands/echo.d.ts.map +1 -1
  45. package/dist/commands/echo.js +125 -2
  46. package/dist/commands/echo.js.map +1 -1
  47. package/dist/commands/env.d.ts +4 -0
  48. package/dist/commands/env.d.ts.map +1 -0
  49. package/dist/commands/env.js +129 -0
  50. package/dist/commands/env.js.map +1 -0
  51. package/dist/commands/expand.d.ts +4 -0
  52. package/dist/commands/expand.d.ts.map +1 -0
  53. package/dist/commands/expand.js +197 -0
  54. package/dist/commands/expand.js.map +1 -0
  55. package/dist/commands/factor.d.ts +4 -0
  56. package/dist/commands/factor.d.ts.map +1 -0
  57. package/dist/commands/factor.js +141 -0
  58. package/dist/commands/factor.js.map +1 -0
  59. package/dist/commands/fmt.d.ts +4 -0
  60. package/dist/commands/fmt.d.ts.map +1 -0
  61. package/dist/commands/fmt.js +278 -0
  62. package/dist/commands/fmt.js.map +1 -0
  63. package/dist/commands/fold.d.ts +4 -0
  64. package/dist/commands/fold.d.ts.map +1 -0
  65. package/dist/commands/fold.js +253 -0
  66. package/dist/commands/fold.js.map +1 -0
  67. package/dist/commands/groups.d.ts +4 -0
  68. package/dist/commands/groups.d.ts.map +1 -0
  69. package/dist/commands/groups.js +61 -0
  70. package/dist/commands/groups.js.map +1 -0
  71. package/dist/commands/head.d.ts.map +1 -1
  72. package/dist/commands/head.js +184 -77
  73. package/dist/commands/head.js.map +1 -1
  74. package/dist/commands/hostname.d.ts +4 -0
  75. package/dist/commands/hostname.d.ts.map +1 -0
  76. package/dist/commands/hostname.js +80 -0
  77. package/dist/commands/hostname.js.map +1 -0
  78. package/dist/commands/less.d.ts.map +1 -1
  79. package/dist/commands/less.js +1 -0
  80. package/dist/commands/less.js.map +1 -1
  81. package/dist/commands/man.d.ts.map +1 -1
  82. package/dist/commands/man.js +3 -1
  83. package/dist/commands/man.js.map +1 -1
  84. package/dist/commands/mount.d.ts +4 -0
  85. package/dist/commands/mount.d.ts.map +1 -0
  86. package/dist/commands/mount.js +1136 -0
  87. package/dist/commands/mount.js.map +1 -0
  88. package/dist/commands/od.d.ts +4 -0
  89. package/dist/commands/od.d.ts.map +1 -0
  90. package/dist/commands/od.js +342 -0
  91. package/dist/commands/od.js.map +1 -0
  92. package/dist/commands/pr.d.ts +4 -0
  93. package/dist/commands/pr.d.ts.map +1 -0
  94. package/dist/commands/pr.js +298 -0
  95. package/dist/commands/pr.js.map +1 -0
  96. package/dist/commands/printf.d.ts +4 -0
  97. package/dist/commands/printf.d.ts.map +1 -0
  98. package/dist/commands/printf.js +271 -0
  99. package/dist/commands/printf.js.map +1 -0
  100. package/dist/commands/readlink.d.ts +4 -0
  101. package/dist/commands/readlink.d.ts.map +1 -0
  102. package/dist/commands/readlink.js +104 -0
  103. package/dist/commands/readlink.js.map +1 -0
  104. package/dist/commands/realpath.d.ts +4 -0
  105. package/dist/commands/realpath.d.ts.map +1 -0
  106. package/dist/commands/realpath.js +111 -0
  107. package/dist/commands/realpath.js.map +1 -0
  108. package/dist/commands/rev.d.ts +4 -0
  109. package/dist/commands/rev.d.ts.map +1 -0
  110. package/dist/commands/rev.js +134 -0
  111. package/dist/commands/rev.js.map +1 -0
  112. package/dist/commands/shuf.d.ts +4 -0
  113. package/dist/commands/shuf.d.ts.map +1 -0
  114. package/dist/commands/shuf.js +221 -0
  115. package/dist/commands/shuf.js.map +1 -0
  116. package/dist/commands/sleep.d.ts +4 -0
  117. package/dist/commands/sleep.d.ts.map +1 -0
  118. package/dist/commands/sleep.js +102 -0
  119. package/dist/commands/sleep.js.map +1 -0
  120. package/dist/commands/strings.d.ts +4 -0
  121. package/dist/commands/strings.d.ts.map +1 -0
  122. package/dist/commands/strings.js +170 -0
  123. package/dist/commands/strings.js.map +1 -0
  124. package/dist/commands/tac.d.ts +4 -0
  125. package/dist/commands/tac.d.ts.map +1 -0
  126. package/dist/commands/tac.js +130 -0
  127. package/dist/commands/tac.js.map +1 -0
  128. package/dist/commands/time.d.ts +4 -0
  129. package/dist/commands/time.d.ts.map +1 -0
  130. package/dist/commands/time.js +126 -0
  131. package/dist/commands/time.js.map +1 -0
  132. package/dist/commands/umount.d.ts +4 -0
  133. package/dist/commands/umount.d.ts.map +1 -0
  134. package/dist/commands/umount.js +103 -0
  135. package/dist/commands/umount.js.map +1 -0
  136. package/dist/commands/uname.d.ts +4 -0
  137. package/dist/commands/uname.d.ts.map +1 -0
  138. package/dist/commands/uname.js +149 -0
  139. package/dist/commands/uname.js.map +1 -0
  140. package/dist/commands/unexpand.d.ts +4 -0
  141. package/dist/commands/unexpand.d.ts.map +1 -0
  142. package/dist/commands/unexpand.js +286 -0
  143. package/dist/commands/unexpand.js.map +1 -0
  144. package/dist/commands/uptime.d.ts +4 -0
  145. package/dist/commands/uptime.d.ts.map +1 -0
  146. package/dist/commands/uptime.js +62 -0
  147. package/dist/commands/uptime.js.map +1 -0
  148. package/dist/commands/view.d.ts +1 -0
  149. package/dist/commands/view.d.ts.map +1 -1
  150. package/dist/commands/view.js +408 -66
  151. package/dist/commands/view.js.map +1 -1
  152. package/dist/commands/yes.d.ts +4 -0
  153. package/dist/commands/yes.d.ts.map +1 -0
  154. package/dist/commands/yes.js +58 -0
  155. package/dist/commands/yes.js.map +1 -0
  156. package/dist/index.d.ts +24 -0
  157. package/dist/index.d.ts.map +1 -1
  158. package/dist/index.js +82 -0
  159. package/dist/index.js.map +1 -1
  160. package/package.json +12 -3
  161. package/src/commands/awk.ts +340 -0
  162. package/src/commands/chmod.ts +141 -2
  163. package/src/commands/chown.ts +321 -0
  164. package/src/commands/cksum.ts +133 -0
  165. package/src/commands/cmp.ts +126 -0
  166. package/src/commands/column.ts +273 -0
  167. package/src/commands/cp.ts +93 -4
  168. package/src/commands/cron.ts +115 -23
  169. package/src/commands/curl.ts +231 -0
  170. package/src/commands/echo.ts +122 -2
  171. package/src/commands/env.ts +143 -0
  172. package/src/commands/expand.ts +207 -0
  173. package/src/commands/factor.ts +151 -0
  174. package/src/commands/fmt.ts +293 -0
  175. package/src/commands/fold.ts +257 -0
  176. package/src/commands/groups.ts +72 -0
  177. package/src/commands/head.ts +176 -77
  178. package/src/commands/hostname.ts +81 -0
  179. package/src/commands/less.ts +1 -0
  180. package/src/commands/man.ts +4 -1
  181. package/src/commands/mount.ts +1302 -0
  182. package/src/commands/od.ts +327 -0
  183. package/src/commands/pr.ts +291 -0
  184. package/src/commands/printf.ts +271 -0
  185. package/src/commands/readlink.ts +102 -0
  186. package/src/commands/realpath.ts +126 -0
  187. package/src/commands/rev.ts +143 -0
  188. package/src/commands/shuf.ts +218 -0
  189. package/src/commands/sleep.ts +109 -0
  190. package/src/commands/strings.ts +176 -0
  191. package/src/commands/tac.ts +138 -0
  192. package/src/commands/time.ts +144 -0
  193. package/src/commands/umount.ts +116 -0
  194. package/src/commands/uname.ts +130 -0
  195. package/src/commands/unexpand.ts +305 -0
  196. package/src/commands/uptime.ts +73 -0
  197. package/src/commands/view.ts +463 -73
  198. package/src/index.ts +82 -0
  199. package/tsconfig.json +4 -0
@@ -0,0 +1,207 @@
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
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
8
+ const usage = `Usage: expand [OPTION]... [FILE]...
9
+ Convert tabs to spaces in each FILE.
10
+
11
+ -t, --tabs=NUMBER have tabs NUMBER characters apart, not 8
12
+ -t, --tabs=LIST use comma separated list of tab positions
13
+ --help display this help and exit`
14
+ writelnStderr(process, terminal, usage)
15
+ }
16
+
17
+ function parseTabStops(tabStr: string): number[] {
18
+ if (tabStr.includes(',')) {
19
+ const stops = tabStr.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n) && n > 0)
20
+ return stops.length > 0 ? stops : [8]
21
+ }
22
+ const single = parseInt(tabStr, 10)
23
+ return !isNaN(single) && single > 0 ? [single] : [8]
24
+ }
25
+
26
+ function expandTabs(line: string, tabStops: number[]): string {
27
+ let result = ''
28
+ let column = 0
29
+
30
+ for (let i = 0; i < line.length; i++) {
31
+ const char = line[i]
32
+
33
+ if (char === '\t') {
34
+ let nextStop = tabStops[0] ?? 8
35
+ for (const stop of tabStops) {
36
+ if (stop > column) {
37
+ nextStop = stop
38
+ break
39
+ }
40
+ }
41
+
42
+ if (nextStop <= column) {
43
+ const lastStop = tabStops[tabStops.length - 1] ?? 8
44
+ nextStop = lastStop
45
+ while (nextStop <= column) {
46
+ nextStop += lastStop
47
+ }
48
+ }
49
+
50
+ const spaces = nextStop - column
51
+ result += ' '.repeat(spaces)
52
+ column = nextStop
53
+ } else {
54
+ result += char
55
+ if (char === '\n' || char === '\r') {
56
+ column = 0
57
+ } else {
58
+ column++
59
+ }
60
+ }
61
+ }
62
+
63
+ return result
64
+ }
65
+
66
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
67
+ return new TerminalCommand({
68
+ command: 'expand',
69
+ description: 'Convert tabs to spaces',
70
+ kernel,
71
+ shell,
72
+ terminal,
73
+ run: async (pid: number, argv: string[]) => {
74
+ const process = kernel.processes.get(pid) as Process | undefined
75
+
76
+ if (!process) return 1
77
+
78
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
79
+ printUsage(process, terminal)
80
+ return 0
81
+ }
82
+
83
+ let tabStops: number[] = [8]
84
+ const files: string[] = []
85
+
86
+ for (let i = 0; i < argv.length; i++) {
87
+ const arg = argv[i]
88
+ if (!arg) continue
89
+
90
+ if (arg === '--help' || arg === '-h') {
91
+ printUsage(process, terminal)
92
+ return 0
93
+ } else if (arg === '-t' || arg === '--tabs') {
94
+ if (i + 1 < argv.length) {
95
+ const tabStr = argv[++i]
96
+ if (tabStr !== undefined) {
97
+ tabStops = parseTabStops(tabStr)
98
+ }
99
+ }
100
+ } else if (arg.startsWith('--tabs=')) {
101
+ const tabStr = arg.slice(7)
102
+ tabStops = parseTabStops(tabStr)
103
+ } else if (arg.startsWith('-t')) {
104
+ const tabStr = arg.slice(2)
105
+ if (tabStr) {
106
+ tabStops = parseTabStops(tabStr)
107
+ }
108
+ } else if (!arg.startsWith('-')) {
109
+ files.push(arg)
110
+ } else {
111
+ await writelnStderr(process, terminal, `expand: invalid option -- '${arg.slice(1)}'`)
112
+ await writelnStderr(process, terminal, "Try 'expand --help' for more information.")
113
+ return 1
114
+ }
115
+ }
116
+
117
+ const writer = process.stdout.getWriter()
118
+
119
+ try {
120
+ let lines: string[] = []
121
+
122
+ if (files.length === 0) {
123
+ if (!process.stdin) {
124
+ return 0
125
+ }
126
+
127
+ const reader = process.stdin.getReader()
128
+ const decoder = new TextDecoder()
129
+ let buffer = ''
130
+
131
+ try {
132
+ while (true) {
133
+ const { done, value } = await reader.read()
134
+ if (done) break
135
+ if (value) {
136
+ buffer += decoder.decode(value, { stream: true })
137
+ const newLines = buffer.split('\n')
138
+ buffer = newLines.pop() || ''
139
+ lines.push(...newLines)
140
+ }
141
+ }
142
+ if (buffer) {
143
+ lines.push(buffer)
144
+ }
145
+ } finally {
146
+ reader.releaseLock()
147
+ }
148
+ } else {
149
+ for (const file of files) {
150
+ const fullPath = path.resolve(shell.cwd, file)
151
+
152
+ let interrupted = false
153
+ const interruptHandler = () => { interrupted = true }
154
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
155
+
156
+ try {
157
+ if (fullPath.startsWith('/dev')) {
158
+ await writelnStderr(process, terminal, `expand: ${file}: cannot process device files`)
159
+ continue
160
+ }
161
+
162
+ const handle = await shell.context.fs.promises.open(fullPath, 'r')
163
+ const stat = await shell.context.fs.promises.stat(fullPath)
164
+
165
+ const decoder = new TextDecoder()
166
+ let content = ''
167
+ let bytesRead = 0
168
+ const chunkSize = 1024
169
+
170
+ while (bytesRead < stat.size) {
171
+ if (interrupted) break
172
+ const data = new Uint8Array(chunkSize)
173
+ const readSize = Math.min(chunkSize, stat.size - bytesRead)
174
+ await handle.read(data, 0, readSize, bytesRead)
175
+ const chunk = data.subarray(0, readSize)
176
+ content += decoder.decode(chunk, { stream: true })
177
+ bytesRead += readSize
178
+ }
179
+
180
+ const fileLines = content.split('\n')
181
+ if (fileLines[fileLines.length - 1] === '') {
182
+ fileLines.pop()
183
+ }
184
+ lines.push(...fileLines)
185
+ } catch (error) {
186
+ await writelnStderr(process, terminal, `expand: ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`)
187
+ } finally {
188
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
189
+ }
190
+ }
191
+ }
192
+
193
+ for (const line of lines) {
194
+ const expanded = expandTabs(line, tabStops)
195
+ await writer.write(new TextEncoder().encode(expanded + '\n'))
196
+ }
197
+
198
+ return 0
199
+ } catch (error) {
200
+ await writelnStderr(process, terminal, `expand: ${error instanceof Error ? error.message : 'Unknown error'}`)
201
+ return 1
202
+ } finally {
203
+ writer.releaseLock()
204
+ }
205
+ }
206
+ })
207
+ }
@@ -0,0 +1,151 @@
1
+ import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
2
+ import { TerminalCommand } from '../shared/terminal-command.js'
3
+ import { writelnStderr, writelnStdout } from '../shared/helpers.js'
4
+
5
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
6
+ const usage = `Usage: factor [NUMBER]...
7
+ Print prime factors of each NUMBER.
8
+
9
+ --help display this help and exit`
10
+ writelnStderr(process, terminal, usage)
11
+ }
12
+
13
+ function factorize(n: number): number[] {
14
+ if (n < 2) {
15
+ return [n]
16
+ }
17
+
18
+ const factors: number[] = []
19
+ let num = n
20
+
21
+ while (num % 2 === 0) {
22
+ factors.push(2)
23
+ num /= 2
24
+ }
25
+
26
+ for (let i = 3; i * i <= num; i += 2) {
27
+ while (num % i === 0) {
28
+ factors.push(i)
29
+ num /= i
30
+ }
31
+ }
32
+
33
+ if (num > 2) {
34
+ factors.push(num)
35
+ }
36
+
37
+ return factors
38
+ }
39
+
40
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
41
+ return new TerminalCommand({
42
+ command: 'factor',
43
+ description: 'Print prime factors of numbers',
44
+ kernel,
45
+ shell,
46
+ terminal,
47
+ run: async (pid: number, argv: string[]) => {
48
+ const process = kernel.processes.get(pid) as Process | undefined
49
+
50
+ if (!process) return 1
51
+
52
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
53
+ printUsage(process, terminal)
54
+ return 0
55
+ }
56
+
57
+ const numbers: string[] = []
58
+
59
+ for (const arg of argv) {
60
+ if (!arg) continue
61
+
62
+ if (arg === '--help' || arg === '-h') {
63
+ printUsage(process, terminal)
64
+ return 0
65
+ } else if (!arg.startsWith('-')) {
66
+ numbers.push(arg)
67
+ } else {
68
+ await writelnStderr(process, terminal, `factor: invalid option -- '${arg.slice(1)}'`)
69
+ await writelnStderr(process, terminal, "Try 'factor --help' for more information.")
70
+ return 1
71
+ }
72
+ }
73
+
74
+ if (numbers.length === 0) {
75
+ if (!process.stdin) {
76
+ await writelnStderr(process, terminal, 'factor: missing operand')
77
+ await writelnStderr(process, terminal, "Try 'factor --help' for more information.")
78
+ return 1
79
+ }
80
+
81
+ if (process.stdinIsTTY) {
82
+ try {
83
+ while (true) {
84
+ const line = await terminal.readline('', false, true)
85
+ if (!line) break
86
+ const trimmed = line.trim()
87
+ if (trimmed) {
88
+ numbers.push(...trimmed.split(/\s+/))
89
+ } else {
90
+ break
91
+ }
92
+ }
93
+ } catch {
94
+ } finally {
95
+ terminal.listen()
96
+ }
97
+ } else {
98
+ const reader = process.stdin.getReader()
99
+ const decoder = new TextDecoder()
100
+ let buffer = ''
101
+
102
+ try {
103
+ while (true) {
104
+ const { done, value } = await reader.read()
105
+ if (done) break
106
+ if (value) {
107
+ buffer += decoder.decode(value, { stream: true })
108
+ const lines = buffer.split('\n')
109
+ buffer = lines.pop() || ''
110
+ for (const line of lines) {
111
+ const trimmed = line.trim()
112
+ if (trimmed) {
113
+ numbers.push(...trimmed.split(/\s+/))
114
+ }
115
+ }
116
+ }
117
+ }
118
+ if (buffer.trim()) {
119
+ numbers.push(...buffer.trim().split(/\s+/))
120
+ }
121
+ } finally {
122
+ reader.releaseLock()
123
+ }
124
+ }
125
+ }
126
+
127
+ if (numbers.length === 0) {
128
+ await writelnStderr(process, terminal, 'factor: missing operand')
129
+ await writelnStderr(process, terminal, "Try 'factor --help' for more information.")
130
+ return 1
131
+ }
132
+
133
+ let hasError = false
134
+
135
+ for (const numStr of numbers) {
136
+ const num = parseInt(numStr, 10)
137
+ if (isNaN(num) || num < 0) {
138
+ await writelnStderr(process, terminal, `factor: '${numStr}' is not a valid positive integer`)
139
+ hasError = true
140
+ continue
141
+ }
142
+
143
+ const factors = factorize(num)
144
+ const output = `${num}: ${factors.join(' ')}`
145
+ await writelnStdout(process, terminal, output)
146
+ }
147
+
148
+ return hasError ? 1 : 0
149
+ }
150
+ })
151
+ }
@@ -0,0 +1,293 @@
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
+ function printUsage(process: Process | undefined, terminal: Terminal): void {
8
+ const usage = `Usage: fmt [OPTION]... [FILE]...
9
+ Reformat paragraph text.
10
+
11
+ -w, --width=WIDTH maximum line width (default: 75)
12
+ -s, --split-only split long lines, but do not join short lines
13
+ -u, --uniform-spacing use uniform spacing (one space between words)
14
+ --help display this help and exit`
15
+ writelnStderr(process, terminal, usage)
16
+ }
17
+
18
+ function normalizeWhitespace(text: string): string {
19
+ return text.replace(/\s+/g, ' ').trim()
20
+ }
21
+
22
+ function formatText(lines: string[], width: number, splitOnly: boolean, uniformSpacing: boolean): string[] {
23
+ const result: string[] = []
24
+ let currentParagraph: string[] = []
25
+
26
+ for (const line of lines) {
27
+ const trimmed = line.trim()
28
+
29
+ if (trimmed === '') {
30
+ if (currentParagraph.length > 0) {
31
+ const formatted = formatParagraph(currentParagraph, width, splitOnly, uniformSpacing)
32
+ result.push(...formatted)
33
+ currentParagraph = []
34
+ }
35
+ result.push('')
36
+ } else {
37
+ currentParagraph.push(trimmed)
38
+ }
39
+ }
40
+
41
+ if (currentParagraph.length > 0) {
42
+ const formatted = formatParagraph(currentParagraph, width, splitOnly, uniformSpacing)
43
+ result.push(...formatted)
44
+ }
45
+
46
+ return result
47
+ }
48
+
49
+ function formatParagraph(paragraph: string[], width: number, splitOnly: boolean, uniformSpacing: boolean): string[] {
50
+ if (paragraph.length === 0) return []
51
+
52
+ let text = paragraph.join(' ')
53
+ if (uniformSpacing) {
54
+ text = normalizeWhitespace(text)
55
+ } else {
56
+ text = text.replace(/\s+/g, ' ')
57
+ }
58
+
59
+ if (splitOnly) {
60
+ return splitLongLines(text, width)
61
+ }
62
+
63
+ const words = text.split(/\s+/).filter(w => w.length > 0)
64
+ if (words.length === 0) return []
65
+
66
+ const result: string[] = []
67
+ let currentLine = ''
68
+
69
+ for (const word of words) {
70
+ const testLine = currentLine ? `${currentLine} ${word}` : word
71
+
72
+ if (testLine.length <= width) {
73
+ currentLine = testLine
74
+ } else {
75
+ if (currentLine) {
76
+ result.push(currentLine)
77
+ }
78
+ currentLine = word
79
+
80
+ if (currentLine.length > width) {
81
+ const split = splitLongLines(currentLine, width)
82
+ if (split.length > 0) {
83
+ result.push(...split.slice(0, -1))
84
+ currentLine = split[split.length - 1] || word
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ if (currentLine) {
91
+ result.push(currentLine)
92
+ }
93
+
94
+ return result
95
+ }
96
+
97
+ function splitLongLines(text: string, width: number): string[] {
98
+ if (text.length <= width) return [text]
99
+
100
+ const result: string[] = []
101
+ let remaining = text
102
+
103
+ while (remaining.length > width) {
104
+ let breakPoint = width
105
+
106
+ const spaceIndex = remaining.lastIndexOf(' ', width)
107
+ if (spaceIndex > 0) {
108
+ breakPoint = spaceIndex
109
+ }
110
+
111
+ result.push(remaining.slice(0, breakPoint).trim())
112
+ remaining = remaining.slice(breakPoint).trim()
113
+ }
114
+
115
+ if (remaining.length > 0) {
116
+ result.push(remaining)
117
+ }
118
+
119
+ return result
120
+ }
121
+
122
+ export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
123
+ return new TerminalCommand({
124
+ command: 'fmt',
125
+ description: 'Reformat paragraph text',
126
+ kernel,
127
+ shell,
128
+ terminal,
129
+ run: async (pid: number, argv: string[]) => {
130
+ const process = kernel.processes.get(pid) as Process | undefined
131
+
132
+ if (!process) return 1
133
+
134
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
135
+ printUsage(process, terminal)
136
+ return 0
137
+ }
138
+
139
+ let width = 75
140
+ let splitOnly = false
141
+ let uniformSpacing = false
142
+ const files: string[] = []
143
+
144
+ for (let i = 0; i < argv.length; i++) {
145
+ const arg = argv[i]
146
+ if (!arg) continue
147
+
148
+ if (arg === '--help' || arg === '-h') {
149
+ printUsage(process, terminal)
150
+ return 0
151
+ } else if (arg === '-w' || arg === '--width') {
152
+ if (i + 1 < argv.length) {
153
+ const widthStr = argv[++i]
154
+ if (widthStr !== undefined) {
155
+ const parsed = parseInt(widthStr, 10)
156
+ if (!isNaN(parsed) && parsed > 0) {
157
+ width = parsed
158
+ } else {
159
+ await writelnStderr(process, terminal, `fmt: invalid width: ${widthStr}`)
160
+ return 1
161
+ }
162
+ }
163
+ }
164
+ } else if (arg.startsWith('--width=')) {
165
+ const widthStr = arg.slice(8)
166
+ const parsed = parseInt(widthStr, 10)
167
+ if (!isNaN(parsed) && parsed > 0) {
168
+ width = parsed
169
+ } else {
170
+ await writelnStderr(process, terminal, `fmt: invalid width: ${widthStr}`)
171
+ return 1
172
+ }
173
+ } else if (arg.startsWith('-w')) {
174
+ const widthStr = arg.slice(2)
175
+ if (widthStr) {
176
+ const parsed = parseInt(widthStr, 10)
177
+ if (!isNaN(parsed) && parsed > 0) {
178
+ width = parsed
179
+ } else {
180
+ await writelnStderr(process, terminal, `fmt: invalid width: ${widthStr}`)
181
+ return 1
182
+ }
183
+ }
184
+ } else if (arg === '-s' || arg === '--split-only') {
185
+ splitOnly = true
186
+ } else if (arg === '-u' || arg === '--uniform-spacing') {
187
+ uniformSpacing = true
188
+ } else if (arg.startsWith('-')) {
189
+ const flags = arg.slice(1).split('')
190
+ if (flags.includes('s')) splitOnly = true
191
+ if (flags.includes('u')) uniformSpacing = true
192
+ const invalidFlags = flags.filter(f => !['s', 'u'].includes(f))
193
+ if (invalidFlags.length > 0) {
194
+ await writelnStderr(process, terminal, `fmt: invalid option -- '${invalidFlags[0]}'`)
195
+ await writelnStderr(process, terminal, "Try 'fmt --help' for more information.")
196
+ return 1
197
+ }
198
+ } else {
199
+ files.push(arg)
200
+ }
201
+ }
202
+
203
+ const writer = process.stdout.getWriter()
204
+
205
+ try {
206
+ let lines: string[] = []
207
+
208
+ if (files.length === 0) {
209
+ if (!process.stdin) {
210
+ return 0
211
+ }
212
+
213
+ const reader = process.stdin.getReader()
214
+ const decoder = new TextDecoder()
215
+ let buffer = ''
216
+
217
+ try {
218
+ while (true) {
219
+ const { done, value } = await reader.read()
220
+ if (done) break
221
+ if (value) {
222
+ buffer += decoder.decode(value, { stream: true })
223
+ const newLines = buffer.split('\n')
224
+ buffer = newLines.pop() || ''
225
+ lines.push(...newLines)
226
+ }
227
+ }
228
+ if (buffer) {
229
+ lines.push(buffer)
230
+ }
231
+ } finally {
232
+ reader.releaseLock()
233
+ }
234
+ } else {
235
+ for (const file of files) {
236
+ const fullPath = path.resolve(shell.cwd, file)
237
+
238
+ let interrupted = false
239
+ const interruptHandler = () => { interrupted = true }
240
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler)
241
+
242
+ try {
243
+ if (fullPath.startsWith('/dev')) {
244
+ await writelnStderr(process, terminal, `fmt: ${file}: cannot process device files`)
245
+ continue
246
+ }
247
+
248
+ const handle = await shell.context.fs.promises.open(fullPath, 'r')
249
+ const stat = await shell.context.fs.promises.stat(fullPath)
250
+
251
+ const decoder = new TextDecoder()
252
+ let content = ''
253
+ let bytesRead = 0
254
+ const chunkSize = 1024
255
+
256
+ while (bytesRead < stat.size) {
257
+ if (interrupted) break
258
+ const data = new Uint8Array(chunkSize)
259
+ const readSize = Math.min(chunkSize, stat.size - bytesRead)
260
+ await handle.read(data, 0, readSize, bytesRead)
261
+ const chunk = data.subarray(0, readSize)
262
+ content += decoder.decode(chunk, { stream: true })
263
+ bytesRead += readSize
264
+ }
265
+
266
+ const fileLines = content.split('\n')
267
+ if (fileLines[fileLines.length - 1] === '') {
268
+ fileLines.pop()
269
+ }
270
+ lines.push(...fileLines)
271
+ } catch (error) {
272
+ await writelnStderr(process, terminal, `fmt: ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`)
273
+ } finally {
274
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler)
275
+ }
276
+ }
277
+ }
278
+
279
+ const formatted = formatText(lines, width, splitOnly, uniformSpacing)
280
+ for (const line of formatted) {
281
+ await writer.write(new TextEncoder().encode(line + '\n'))
282
+ }
283
+
284
+ return 0
285
+ } catch (error) {
286
+ await writelnStderr(process, terminal, `fmt: ${error instanceof Error ? error.message : 'Unknown error'}`)
287
+ return 1
288
+ } finally {
289
+ writer.releaseLock()
290
+ }
291
+ }
292
+ })
293
+ }