@ecmaos/coreutils 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +25 -0
- package/LICENSE +1 -1
- package/dist/commands/cal.js +2 -2
- package/dist/commands/cal.js.map +1 -1
- package/dist/commands/cat.js +2 -2
- package/dist/commands/cd.js +2 -2
- package/dist/commands/chmod.d.ts.map +1 -1
- package/dist/commands/chmod.js +16 -11
- package/dist/commands/chmod.js.map +1 -1
- package/dist/commands/cp.js +2 -2
- package/dist/commands/cp.js.map +1 -1
- package/dist/commands/date.js +2 -2
- package/dist/commands/date.js.map +1 -1
- package/dist/commands/echo.js +2 -2
- package/dist/commands/echo.js.map +1 -1
- package/dist/commands/false.js +2 -2
- package/dist/commands/fetch.d.ts +4 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +210 -0
- package/dist/commands/fetch.js.map +1 -0
- package/dist/commands/format.d.ts +4 -0
- package/dist/commands/format.d.ts.map +1 -0
- package/dist/commands/format.js +178 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/hash.d.ts +4 -0
- package/dist/commands/hash.d.ts.map +1 -0
- package/dist/commands/hash.js +200 -0
- package/dist/commands/hash.js.map +1 -0
- package/dist/commands/head.js +2 -2
- package/dist/commands/id.js +2 -2
- package/dist/commands/id.js.map +1 -1
- package/dist/commands/less.d.ts.map +1 -1
- package/dist/commands/less.js +53 -2
- package/dist/commands/less.js.map +1 -1
- package/dist/commands/ls.d.ts.map +1 -1
- package/dist/commands/ls.js +120 -97
- package/dist/commands/ls.js.map +1 -1
- package/dist/commands/man.d.ts +4 -0
- package/dist/commands/man.d.ts.map +1 -0
- package/dist/commands/man.js +554 -0
- package/dist/commands/man.js.map +1 -0
- package/dist/commands/mkdir.js +2 -2
- package/dist/commands/mkdir.js.map +1 -1
- package/dist/commands/mktemp.d.ts +4 -0
- package/dist/commands/mktemp.d.ts.map +1 -0
- package/dist/commands/mktemp.js +229 -0
- package/dist/commands/mktemp.js.map +1 -0
- package/dist/commands/nc.js +2 -2
- package/dist/commands/nc.js.map +1 -1
- package/dist/commands/passkey.js +3 -3
- package/dist/commands/pwd.js +2 -2
- package/dist/commands/pwd.js.map +1 -1
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +57 -12
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/rmdir.js +2 -2
- package/dist/commands/rmdir.js.map +1 -1
- package/dist/commands/sockets.js +1 -1
- package/dist/commands/stat.d.ts.map +1 -1
- package/dist/commands/stat.js +37 -15
- package/dist/commands/stat.js.map +1 -1
- package/dist/commands/tail.js +2 -2
- package/dist/commands/tar.d.ts +4 -0
- package/dist/commands/tar.d.ts.map +1 -0
- package/dist/commands/tar.js +693 -0
- package/dist/commands/tar.js.map +1 -0
- package/dist/commands/touch.js +2 -2
- package/dist/commands/touch.js.map +1 -1
- package/dist/commands/true.js +2 -2
- package/dist/commands/unzip.d.ts +4 -0
- package/dist/commands/unzip.d.ts.map +1 -0
- package/dist/commands/unzip.js +443 -0
- package/dist/commands/unzip.js.map +1 -0
- package/dist/commands/user.d.ts +4 -0
- package/dist/commands/user.d.ts.map +1 -0
- package/dist/commands/user.js +427 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/commands/whoami.js +2 -2
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/zip.d.ts +4 -0
- package/dist/commands/zip.d.ts.map +1 -0
- package/dist/commands/zip.js +264 -0
- package/dist/commands/zip.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/src/commands/cal.ts +2 -2
- package/src/commands/cat.ts +2 -2
- package/src/commands/cd.ts +2 -2
- package/src/commands/chmod.ts +19 -11
- package/src/commands/cp.ts +2 -2
- package/src/commands/date.ts +2 -2
- package/src/commands/echo.ts +2 -2
- package/src/commands/false.ts +2 -2
- package/src/commands/fetch.ts +205 -0
- package/src/commands/format.ts +204 -0
- package/src/commands/hash.ts +215 -0
- package/src/commands/head.ts +2 -2
- package/src/commands/id.ts +2 -2
- package/src/commands/less.ts +50 -2
- package/src/commands/ls.ts +131 -91
- package/src/commands/man.ts +643 -0
- package/src/commands/mkdir.ts +2 -2
- package/src/commands/mktemp.ts +235 -0
- package/src/commands/nc.ts +2 -2
- package/src/commands/passkey.ts +3 -3
- package/src/commands/pwd.ts +2 -2
- package/src/commands/rm.ts +54 -12
- package/src/commands/rmdir.ts +2 -2
- package/src/commands/sockets.ts +1 -1
- package/src/commands/stat.ts +44 -16
- package/src/commands/tail.ts +2 -2
- package/src/commands/tar.ts +737 -0
- package/src/commands/touch.ts +2 -2
- package/src/commands/true.ts +2 -2
- package/src/commands/unzip.ts +517 -0
- package/src/commands/user.ts +436 -0
- package/src/commands/whoami.ts +2 -2
- package/src/commands/zip.ts +319 -0
- package/src/index.ts +28 -1
package/src/commands/touch.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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 {
|
|
4
|
+
import { writelnStderr } from '../shared/helpers.js'
|
|
5
5
|
|
|
6
6
|
function printUsage(process: Process | undefined, terminal: Terminal): void {
|
|
7
7
|
const usage = `Usage: touch [OPTION]... FILE...
|
|
8
8
|
Update the access and modification times of each FILE to the current time.
|
|
9
9
|
|
|
10
10
|
--help display this help and exit`
|
|
11
|
-
|
|
11
|
+
writelnStderr(process, terminal, usage)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
|
package/src/commands/true.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
|
|
2
2
|
import { TerminalCommand } from '../shared/terminal-command.js'
|
|
3
|
-
import {
|
|
3
|
+
import { writelnStderr } from '../shared/helpers.js'
|
|
4
4
|
|
|
5
5
|
function printUsage(process: Process | undefined, terminal: Terminal): void {
|
|
6
6
|
const usage = `Usage: true
|
|
7
7
|
Return a successful exit status.
|
|
8
8
|
|
|
9
9
|
--help display this help and exit`
|
|
10
|
-
|
|
10
|
+
writelnStderr(process, terminal, usage)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import * as zipjs from '@zip.js/zip.js'
|
|
3
|
+
import type { Kernel, Process, Shell, Terminal } from '@ecmaos/types'
|
|
4
|
+
import { TerminalCommand } from '../shared/terminal-command.js'
|
|
5
|
+
import { writelnStdout, writelnStderr } from '../shared/helpers.js'
|
|
6
|
+
import chalk from 'chalk'
|
|
7
|
+
|
|
8
|
+
function printUsage(process: Process | undefined, terminal: Terminal): void {
|
|
9
|
+
const usage = `Usage: unzip [OPTION]... ZIPFILE [FILE]...
|
|
10
|
+
Extract files from a zip archive.
|
|
11
|
+
|
|
12
|
+
-l, --list list contents of zip file
|
|
13
|
+
-d, --directory extract files to directory
|
|
14
|
+
-o, --overwrite overwrite files without prompting
|
|
15
|
+
-q, --quiet quiet mode (suppress output)
|
|
16
|
+
-v, --verbose verbose mode
|
|
17
|
+
-x, --exclude exclude files from extraction
|
|
18
|
+
-h, --help display this help and exit
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
unzip archive.zip
|
|
22
|
+
unzip -d /tmp archive.zip
|
|
23
|
+
unzip -l archive.zip
|
|
24
|
+
unzip -x "*.txt" archive.zip`
|
|
25
|
+
writelnStderr(process, terminal, usage)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UnzipOptions {
|
|
29
|
+
list: boolean
|
|
30
|
+
directory: string | null
|
|
31
|
+
overwrite: boolean
|
|
32
|
+
quiet: boolean
|
|
33
|
+
verbose: boolean
|
|
34
|
+
exclude: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv: string[]): { options: UnzipOptions; zipfile: string | null; files: string[] } {
|
|
38
|
+
const options: UnzipOptions = {
|
|
39
|
+
list: false,
|
|
40
|
+
directory: null,
|
|
41
|
+
overwrite: false,
|
|
42
|
+
quiet: false,
|
|
43
|
+
verbose: false,
|
|
44
|
+
exclude: []
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const files: string[] = []
|
|
48
|
+
let zipfile: string | null = null
|
|
49
|
+
let i = 0
|
|
50
|
+
|
|
51
|
+
while (i < argv.length) {
|
|
52
|
+
const arg = argv[i]
|
|
53
|
+
if (!arg) {
|
|
54
|
+
i++
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (arg === '--help' || arg === '-h') {
|
|
59
|
+
i++
|
|
60
|
+
continue
|
|
61
|
+
} else if (arg === '-l' || arg === '--list') {
|
|
62
|
+
options.list = true
|
|
63
|
+
i++
|
|
64
|
+
} else if (arg === '-d' || arg === '--directory') {
|
|
65
|
+
if (i + 1 < argv.length) {
|
|
66
|
+
i++
|
|
67
|
+
options.directory = argv[i] || null
|
|
68
|
+
}
|
|
69
|
+
i++
|
|
70
|
+
} else if (arg === '-o' || arg === '--overwrite') {
|
|
71
|
+
options.overwrite = true
|
|
72
|
+
i++
|
|
73
|
+
} else if (arg === '-q' || arg === '--quiet') {
|
|
74
|
+
options.quiet = true
|
|
75
|
+
i++
|
|
76
|
+
} else if (arg === '-v' || arg === '--verbose') {
|
|
77
|
+
options.verbose = true
|
|
78
|
+
i++
|
|
79
|
+
} else if (arg === '-x' || arg === '--exclude') {
|
|
80
|
+
if (i + 1 < argv.length) {
|
|
81
|
+
i++
|
|
82
|
+
const pattern = argv[i]
|
|
83
|
+
if (pattern) {
|
|
84
|
+
options.exclude.push(pattern)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
i++
|
|
88
|
+
} else if (arg.startsWith('-')) {
|
|
89
|
+
// Handle combined flags like -oq
|
|
90
|
+
const flags = arg.slice(1).split('')
|
|
91
|
+
for (const flag of flags) {
|
|
92
|
+
if (flag === 'l') options.list = true
|
|
93
|
+
else if (flag === 'o') options.overwrite = true
|
|
94
|
+
else if (flag === 'q') options.quiet = true
|
|
95
|
+
else if (flag === 'v') options.verbose = true
|
|
96
|
+
}
|
|
97
|
+
i++
|
|
98
|
+
} else {
|
|
99
|
+
// First non-option argument is the zipfile
|
|
100
|
+
if (!zipfile) {
|
|
101
|
+
zipfile = arg
|
|
102
|
+
} else {
|
|
103
|
+
files.push(arg)
|
|
104
|
+
}
|
|
105
|
+
i++
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { options, zipfile, files }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function matchesPattern(filename: string, pattern: string): boolean {
|
|
113
|
+
// Simple glob pattern matching
|
|
114
|
+
// Convert glob pattern to regex
|
|
115
|
+
const regexPattern = pattern
|
|
116
|
+
.replace(/\./g, '\\.')
|
|
117
|
+
.replace(/\*/g, '.*')
|
|
118
|
+
.replace(/\?/g, '.')
|
|
119
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
120
|
+
return regex.test(filename)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function expandGlob(pattern: string, shell: Shell): Promise<string[]> {
|
|
124
|
+
if (!pattern.includes('*') && !pattern.includes('?')) {
|
|
125
|
+
return [pattern]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const lastSlashIndex = pattern.lastIndexOf('/')
|
|
129
|
+
const searchDir = lastSlashIndex !== -1
|
|
130
|
+
? path.resolve(shell.cwd, pattern.substring(0, lastSlashIndex + 1))
|
|
131
|
+
: shell.cwd
|
|
132
|
+
const globPattern = lastSlashIndex !== -1
|
|
133
|
+
? pattern.substring(lastSlashIndex + 1)
|
|
134
|
+
: pattern
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const entries = await shell.context.fs.promises.readdir(searchDir)
|
|
138
|
+
const regexPattern = globPattern
|
|
139
|
+
.replace(/\./g, '\\.')
|
|
140
|
+
.replace(/\*/g, '.*')
|
|
141
|
+
.replace(/\?/g, '.')
|
|
142
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
143
|
+
|
|
144
|
+
const matches = entries.filter(entry => regex.test(entry))
|
|
145
|
+
|
|
146
|
+
if (lastSlashIndex !== -1) {
|
|
147
|
+
const dirPart = pattern.substring(0, lastSlashIndex + 1)
|
|
148
|
+
return matches.map(match => dirPart + match)
|
|
149
|
+
}
|
|
150
|
+
return matches
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return []
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function extractFromZip(
|
|
157
|
+
zipfilePath: string,
|
|
158
|
+
shell: Shell,
|
|
159
|
+
terminal: Terminal,
|
|
160
|
+
process: Process | undefined,
|
|
161
|
+
options: UnzipOptions,
|
|
162
|
+
extractPath: string,
|
|
163
|
+
files: string[]
|
|
164
|
+
): Promise<{ extractedCount: number; skippedCount: number; hasError: boolean }> {
|
|
165
|
+
const zipData = await shell.context.fs.promises.readFile(zipfilePath)
|
|
166
|
+
const blob = new Blob([new Uint8Array(zipData)])
|
|
167
|
+
const zipReader = new zipjs.ZipReader(new zipjs.BlobReader(blob))
|
|
168
|
+
const entries = await zipReader.getEntries()
|
|
169
|
+
|
|
170
|
+
let extractedCount = 0
|
|
171
|
+
let skippedCount = 0
|
|
172
|
+
let hasError = false
|
|
173
|
+
|
|
174
|
+
// Filter entries if specific files are requested
|
|
175
|
+
const entriesToExtract = files.length > 0
|
|
176
|
+
? entries.filter(entry => files.some(file => entry.filename === file || entry.filename.startsWith(file + '/')))
|
|
177
|
+
: entries
|
|
178
|
+
|
|
179
|
+
for (const entry of entriesToExtract) {
|
|
180
|
+
// Normalize the entry name: strip leading slashes and resolve relative to extraction directory
|
|
181
|
+
let entryName = entry.filename
|
|
182
|
+
// Remove leading slashes to make it relative
|
|
183
|
+
while (entryName.startsWith('/')) {
|
|
184
|
+
entryName = entryName.slice(1)
|
|
185
|
+
}
|
|
186
|
+
// Skip empty entries (like just "/")
|
|
187
|
+
if (!entryName) {
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if entry should be excluded (use original filename for pattern matching)
|
|
192
|
+
if (options.exclude.some(pattern => matchesPattern(entry.filename, pattern))) {
|
|
193
|
+
if (options.verbose && !options.quiet) {
|
|
194
|
+
await writelnStdout(process, terminal, ` skipping: ${entryName}`)
|
|
195
|
+
}
|
|
196
|
+
skippedCount++
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const entryPath = path.resolve(extractPath, entryName)
|
|
201
|
+
|
|
202
|
+
// Security check: ensure target path is within extract base (prevent directory traversal)
|
|
203
|
+
const resolvedBase = path.resolve(extractPath)
|
|
204
|
+
const resolvedTarget = path.resolve(entryPath)
|
|
205
|
+
if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
|
|
206
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: ${entry.filename}: path outside extraction directory`))
|
|
207
|
+
hasError = true
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const entryDir = path.dirname(entryPath)
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
// Check if file already exists
|
|
215
|
+
const exists = await shell.context.fs.promises.exists(entryPath)
|
|
216
|
+
if (exists && !options.overwrite) {
|
|
217
|
+
if (!options.quiet) {
|
|
218
|
+
await writelnStderr(process, terminal,
|
|
219
|
+
chalk.yellow(`unzip: ${entryName} already exists - skipping (use -o to overwrite)`)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
skippedCount++
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Ensure directory exists
|
|
227
|
+
if (entryDir !== extractPath) {
|
|
228
|
+
await shell.context.fs.promises.mkdir(entryDir, { recursive: true })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (entry.directory || entryName.endsWith('/')) {
|
|
232
|
+
await shell.context.fs.promises.mkdir(entryPath, { recursive: true })
|
|
233
|
+
if (options.verbose && !options.quiet) {
|
|
234
|
+
await writelnStdout(process, terminal, ` creating: ${entryName}/`)
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
const writer = new zipjs.Uint8ArrayWriter()
|
|
238
|
+
const data = await entry.getData?.(writer)
|
|
239
|
+
if (!data) {
|
|
240
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: Failed to read ${entryName}`))
|
|
241
|
+
hasError = true
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
await shell.context.fs.promises.writeFile(entryPath, data)
|
|
245
|
+
if (!options.quiet) {
|
|
246
|
+
await writelnStdout(process, terminal, ` inflating: ${entryName}`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
extractedCount++
|
|
250
|
+
} catch (error) {
|
|
251
|
+
await writelnStderr(process, terminal,
|
|
252
|
+
chalk.red(`unzip error: ${entryName}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
253
|
+
)
|
|
254
|
+
hasError = true
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await zipReader.close()
|
|
259
|
+
return { extractedCount, skippedCount, hasError }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function listZipContents(
|
|
263
|
+
zipfilePath: string,
|
|
264
|
+
shell: Shell,
|
|
265
|
+
terminal: Terminal,
|
|
266
|
+
process: Process | undefined
|
|
267
|
+
): Promise<number> {
|
|
268
|
+
try {
|
|
269
|
+
const zipData = await shell.context.fs.promises.readFile(zipfilePath)
|
|
270
|
+
const blob = new Blob([new Uint8Array(zipData)])
|
|
271
|
+
const zipReader = new zipjs.ZipReader(new zipjs.BlobReader(blob))
|
|
272
|
+
const entries = await zipReader.getEntries()
|
|
273
|
+
|
|
274
|
+
if (entries.length === 0) {
|
|
275
|
+
await writelnStdout(process, terminal, 'Archive: ' + path.basename(zipfilePath))
|
|
276
|
+
await writelnStdout(process, terminal, ' Empty archive')
|
|
277
|
+
await zipReader.close()
|
|
278
|
+
return 0
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Calculate column widths
|
|
282
|
+
let maxLength = 0
|
|
283
|
+
let maxSize = 0
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
if (entry.filename.length > maxLength) maxLength = entry.filename.length
|
|
286
|
+
const size = entry.uncompressedSize || 0
|
|
287
|
+
if (size > maxSize) maxSize = size
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sizeWidth = Math.max(12, String(maxSize).length)
|
|
291
|
+
const nameWidth = Math.max(20, maxLength)
|
|
292
|
+
|
|
293
|
+
await writelnStdout(process, terminal, `Archive: ${path.basename(zipfilePath)}`)
|
|
294
|
+
await writelnStdout(process, terminal, '')
|
|
295
|
+
await writelnStdout(process, terminal,
|
|
296
|
+
` Length Date Time Name`.padEnd(nameWidth + sizeWidth + 20)
|
|
297
|
+
)
|
|
298
|
+
await writelnStdout(process, terminal,
|
|
299
|
+
` ${'-'.repeat(sizeWidth)} ${'-'.repeat(10)} ${'-'.repeat(5)} ${'-'.repeat(nameWidth)}`
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
let totalLength = 0
|
|
303
|
+
for (const entry of entries) {
|
|
304
|
+
const length = entry.uncompressedSize || 0
|
|
305
|
+
totalLength += length
|
|
306
|
+
|
|
307
|
+
let date = '--'
|
|
308
|
+
let time = '--'
|
|
309
|
+
|
|
310
|
+
if (entry.lastModDate) {
|
|
311
|
+
const d = new Date(entry.lastModDate)
|
|
312
|
+
const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
313
|
+
const day = String(d.getDate()).padStart(2, '0')
|
|
314
|
+
const year = String(d.getFullYear()).slice(-2)
|
|
315
|
+
date = `${month}-${day}-${year}`
|
|
316
|
+
|
|
317
|
+
const hours = String(d.getHours()).padStart(2, '0')
|
|
318
|
+
const minutes = String(d.getMinutes()).padStart(2, '0')
|
|
319
|
+
time = `${hours}:${minutes}`
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const name = entry.directory ? entry.filename + '/' : entry.filename
|
|
323
|
+
const lengthStr = entry.directory ? '' : String(length).padStart(sizeWidth)
|
|
324
|
+
|
|
325
|
+
await writelnStdout(process, terminal,
|
|
326
|
+
` ${lengthStr.padEnd(sizeWidth)} ${date.padEnd(10)} ${time.padEnd(5)} ${name}`
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await writelnStdout(process, terminal,
|
|
331
|
+
` ${'-'.repeat(sizeWidth)} ${'-'.repeat(10)} ${'-'.repeat(5)} ${'-'.repeat(nameWidth)}`
|
|
332
|
+
)
|
|
333
|
+
await writelnStdout(process, terminal,
|
|
334
|
+
` ${String(totalLength).padStart(sizeWidth)} ${entries.length} file${entries.length !== 1 ? 's' : ''}`
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
await zipReader.close()
|
|
338
|
+
return 0
|
|
339
|
+
} catch (error) {
|
|
340
|
+
await writelnStderr(process, terminal,
|
|
341
|
+
chalk.red(`unzip error: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
342
|
+
)
|
|
343
|
+
return 1
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
|
|
348
|
+
return new TerminalCommand({
|
|
349
|
+
command: 'unzip',
|
|
350
|
+
description: 'Extract zip archives',
|
|
351
|
+
kernel,
|
|
352
|
+
shell,
|
|
353
|
+
terminal,
|
|
354
|
+
run: async (pid: number, argv: string[]) => {
|
|
355
|
+
const process = kernel.processes.get(pid) as Process | undefined
|
|
356
|
+
|
|
357
|
+
if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
|
|
358
|
+
printUsage(process, terminal)
|
|
359
|
+
return 0
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { options, zipfile, files } = parseArgs(argv)
|
|
363
|
+
|
|
364
|
+
if (!zipfile) {
|
|
365
|
+
await writelnStderr(process, terminal, chalk.red('unzip error: zipfile name required'))
|
|
366
|
+
await writelnStderr(process, terminal, "Try 'unzip --help' for more information.")
|
|
367
|
+
return 1
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// The shell should have already expanded globs when they match files.
|
|
371
|
+
// However, if a glob pattern doesn't match, the shell passes it as-is.
|
|
372
|
+
// So we need to handle glob expansion as a fallback.
|
|
373
|
+
// Also, when the shell expands a glob like "sample*.zip" to multiple files,
|
|
374
|
+
// parseArgs treats the first as zipfile and the rest as "files".
|
|
375
|
+
// We need to collect all zip files and separate them from files to extract.
|
|
376
|
+
|
|
377
|
+
const zipfiles: string[] = []
|
|
378
|
+
const filesToExtract: string[] = []
|
|
379
|
+
|
|
380
|
+
// Process the zipfile argument
|
|
381
|
+
const zipfilePath = path.resolve(shell.cwd, zipfile)
|
|
382
|
+
const zipfileExists = await shell.context.fs.promises.exists(zipfilePath)
|
|
383
|
+
|
|
384
|
+
if (zipfileExists) {
|
|
385
|
+
// File exists, use it as-is
|
|
386
|
+
zipfiles.push(zipfile)
|
|
387
|
+
} else if (zipfile.includes('*') || zipfile.includes('?')) {
|
|
388
|
+
// Contains glob chars and doesn't exist - expand it
|
|
389
|
+
const expanded = await expandGlob(zipfile, shell)
|
|
390
|
+
if (expanded.length === 0) {
|
|
391
|
+
// No matches - this is an error
|
|
392
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: ${zipfile}: No such file or directory`))
|
|
393
|
+
return 1
|
|
394
|
+
}
|
|
395
|
+
zipfiles.push(...expanded)
|
|
396
|
+
} else {
|
|
397
|
+
// Doesn't exist and no glob chars - error
|
|
398
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: ${zipfile}: No such file or directory`))
|
|
399
|
+
return 1
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Process the files arguments
|
|
403
|
+
// Standard unzip behavior: arguments ending with .zip are zip files to extract from
|
|
404
|
+
// Other arguments are files to extract from within the zip
|
|
405
|
+
for (const file of files) {
|
|
406
|
+
if (file.toLowerCase().endsWith('.zip')) {
|
|
407
|
+
// This looks like a zip file
|
|
408
|
+
const filePath = path.resolve(shell.cwd, file)
|
|
409
|
+
const fileExists = await shell.context.fs.promises.exists(filePath)
|
|
410
|
+
|
|
411
|
+
if (fileExists) {
|
|
412
|
+
zipfiles.push(file)
|
|
413
|
+
} else if (file.includes('*') || file.includes('?')) {
|
|
414
|
+
// Contains glob chars - expand it
|
|
415
|
+
const expanded = await expandGlob(file, shell)
|
|
416
|
+
if (expanded.length === 0) {
|
|
417
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: ${file}: No such file or directory`))
|
|
418
|
+
// Continue processing other files
|
|
419
|
+
} else {
|
|
420
|
+
zipfiles.push(...expanded)
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: ${file}: No such file or directory`))
|
|
424
|
+
// Continue processing other files
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
// This is a file to extract from within the zip
|
|
428
|
+
filesToExtract.push(file)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (zipfiles.length === 0) {
|
|
433
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: No zip files to process`))
|
|
434
|
+
return 1
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const actualFiles = filesToExtract
|
|
438
|
+
|
|
439
|
+
// List mode - only process first zip file
|
|
440
|
+
if (options.list) {
|
|
441
|
+
if (!zipfiles[0]) {
|
|
442
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: No zip files to list`))
|
|
443
|
+
return 1
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const zipfilePath = path.resolve(shell.cwd, zipfiles[0])
|
|
447
|
+
const exists = await shell.context.fs.promises.exists(zipfilePath)
|
|
448
|
+
|
|
449
|
+
if (!exists) {
|
|
450
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: ${zipfiles[0]}: No such file or directory`))
|
|
451
|
+
return 1
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return await listZipContents(zipfilePath, shell, terminal, process)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Extract mode - process all zip files
|
|
458
|
+
const extractPath = options.directory
|
|
459
|
+
? path.resolve(shell.cwd, options.directory)
|
|
460
|
+
: shell.cwd
|
|
461
|
+
|
|
462
|
+
// Ensure extract directory exists
|
|
463
|
+
try {
|
|
464
|
+
const extractPathStat = await shell.context.fs.promises.stat(extractPath).catch(() => null)
|
|
465
|
+
if (!extractPathStat) {
|
|
466
|
+
await shell.context.fs.promises.mkdir(extractPath, { recursive: true })
|
|
467
|
+
} else if (!extractPathStat.isDirectory()) {
|
|
468
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: ${options.directory}: Not a directory`))
|
|
469
|
+
return 1
|
|
470
|
+
}
|
|
471
|
+
} catch (error) {
|
|
472
|
+
await writelnStderr(process, terminal,
|
|
473
|
+
chalk.red(`unzip error: Cannot create directory ${extractPath}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
474
|
+
)
|
|
475
|
+
return 1
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
let totalExtracted = 0
|
|
479
|
+
let totalSkipped = 0
|
|
480
|
+
let hasError = false
|
|
481
|
+
|
|
482
|
+
for (const zipfileItem of zipfiles) {
|
|
483
|
+
const zipfilePath = path.resolve(shell.cwd, zipfileItem)
|
|
484
|
+
const exists = await shell.context.fs.promises.exists(zipfilePath)
|
|
485
|
+
if (!exists) {
|
|
486
|
+
await writelnStderr(process, terminal, chalk.red(`unzip error: ${zipfileItem}: No such file or directory`))
|
|
487
|
+
hasError = true
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const result = await extractFromZip(zipfilePath, shell, terminal, process, options, extractPath, actualFiles)
|
|
493
|
+
totalExtracted += result.extractedCount
|
|
494
|
+
totalSkipped += result.skippedCount
|
|
495
|
+
if (result.hasError) {
|
|
496
|
+
hasError = true
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (!options.quiet) {
|
|
500
|
+
await writelnStdout(process, terminal, `\nArchive: ${path.basename(zipfilePath)}`)
|
|
501
|
+
await writelnStdout(process, terminal, ` ${result.extractedCount} file${result.extractedCount !== 1 ? 's' : ''} extracted`)
|
|
502
|
+
if (result.skippedCount > 0) {
|
|
503
|
+
await writelnStdout(process, terminal, ` ${result.skippedCount} file${result.skippedCount !== 1 ? 's' : ''} skipped`)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
await writelnStderr(process, terminal,
|
|
508
|
+
chalk.red(`unzip error: ${zipfileItem}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
|
509
|
+
)
|
|
510
|
+
hasError = true
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return hasError ? 1 : 0
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
}
|