@ecmaos/coreutils 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/LICENSE +1 -1
  4. package/dist/commands/cal.js +2 -2
  5. package/dist/commands/cal.js.map +1 -1
  6. package/dist/commands/cat.js +2 -2
  7. package/dist/commands/cd.js +2 -2
  8. package/dist/commands/chmod.d.ts.map +1 -1
  9. package/dist/commands/chmod.js +16 -11
  10. package/dist/commands/chmod.js.map +1 -1
  11. package/dist/commands/cp.js +2 -2
  12. package/dist/commands/cp.js.map +1 -1
  13. package/dist/commands/date.js +2 -2
  14. package/dist/commands/date.js.map +1 -1
  15. package/dist/commands/echo.js +2 -2
  16. package/dist/commands/echo.js.map +1 -1
  17. package/dist/commands/false.js +2 -2
  18. package/dist/commands/fetch.d.ts +4 -0
  19. package/dist/commands/fetch.d.ts.map +1 -0
  20. package/dist/commands/fetch.js +210 -0
  21. package/dist/commands/fetch.js.map +1 -0
  22. package/dist/commands/format.d.ts +4 -0
  23. package/dist/commands/format.d.ts.map +1 -0
  24. package/dist/commands/format.js +178 -0
  25. package/dist/commands/format.js.map +1 -0
  26. package/dist/commands/hash.d.ts +4 -0
  27. package/dist/commands/hash.d.ts.map +1 -0
  28. package/dist/commands/hash.js +200 -0
  29. package/dist/commands/hash.js.map +1 -0
  30. package/dist/commands/head.js +2 -2
  31. package/dist/commands/id.js +2 -2
  32. package/dist/commands/id.js.map +1 -1
  33. package/dist/commands/less.d.ts.map +1 -1
  34. package/dist/commands/less.js +53 -2
  35. package/dist/commands/less.js.map +1 -1
  36. package/dist/commands/ls.d.ts.map +1 -1
  37. package/dist/commands/ls.js +120 -97
  38. package/dist/commands/ls.js.map +1 -1
  39. package/dist/commands/man.d.ts +4 -0
  40. package/dist/commands/man.d.ts.map +1 -0
  41. package/dist/commands/man.js +554 -0
  42. package/dist/commands/man.js.map +1 -0
  43. package/dist/commands/mkdir.js +2 -2
  44. package/dist/commands/mkdir.js.map +1 -1
  45. package/dist/commands/mktemp.d.ts +4 -0
  46. package/dist/commands/mktemp.d.ts.map +1 -0
  47. package/dist/commands/mktemp.js +229 -0
  48. package/dist/commands/mktemp.js.map +1 -0
  49. package/dist/commands/nc.js +2 -2
  50. package/dist/commands/nc.js.map +1 -1
  51. package/dist/commands/passkey.js +3 -3
  52. package/dist/commands/pwd.js +2 -2
  53. package/dist/commands/pwd.js.map +1 -1
  54. package/dist/commands/rm.d.ts.map +1 -1
  55. package/dist/commands/rm.js +57 -12
  56. package/dist/commands/rm.js.map +1 -1
  57. package/dist/commands/rmdir.js +2 -2
  58. package/dist/commands/rmdir.js.map +1 -1
  59. package/dist/commands/sockets.js +1 -1
  60. package/dist/commands/stat.d.ts.map +1 -1
  61. package/dist/commands/stat.js +37 -15
  62. package/dist/commands/stat.js.map +1 -1
  63. package/dist/commands/tail.js +2 -2
  64. package/dist/commands/tar.d.ts +4 -0
  65. package/dist/commands/tar.d.ts.map +1 -0
  66. package/dist/commands/tar.js +693 -0
  67. package/dist/commands/tar.js.map +1 -0
  68. package/dist/commands/touch.js +2 -2
  69. package/dist/commands/touch.js.map +1 -1
  70. package/dist/commands/true.js +2 -2
  71. package/dist/commands/unzip.d.ts +4 -0
  72. package/dist/commands/unzip.d.ts.map +1 -0
  73. package/dist/commands/unzip.js +443 -0
  74. package/dist/commands/unzip.js.map +1 -0
  75. package/dist/commands/user.d.ts +4 -0
  76. package/dist/commands/user.d.ts.map +1 -0
  77. package/dist/commands/user.js +427 -0
  78. package/dist/commands/user.js.map +1 -0
  79. package/dist/commands/whoami.js +2 -2
  80. package/dist/commands/whoami.js.map +1 -1
  81. package/dist/commands/zip.d.ts +4 -0
  82. package/dist/commands/zip.d.ts.map +1 -0
  83. package/dist/commands/zip.js +264 -0
  84. package/dist/commands/zip.js.map +1 -0
  85. package/dist/index.d.ts +9 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +28 -1
  88. package/dist/index.js.map +1 -1
  89. package/package.json +6 -4
  90. package/src/commands/cal.ts +2 -2
  91. package/src/commands/cat.ts +2 -2
  92. package/src/commands/cd.ts +2 -2
  93. package/src/commands/chmod.ts +19 -11
  94. package/src/commands/cp.ts +2 -2
  95. package/src/commands/date.ts +2 -2
  96. package/src/commands/echo.ts +2 -2
  97. package/src/commands/false.ts +2 -2
  98. package/src/commands/fetch.ts +205 -0
  99. package/src/commands/format.ts +204 -0
  100. package/src/commands/hash.ts +215 -0
  101. package/src/commands/head.ts +2 -2
  102. package/src/commands/id.ts +2 -2
  103. package/src/commands/less.ts +50 -2
  104. package/src/commands/ls.ts +131 -91
  105. package/src/commands/man.ts +643 -0
  106. package/src/commands/mkdir.ts +2 -2
  107. package/src/commands/mktemp.ts +235 -0
  108. package/src/commands/nc.ts +2 -2
  109. package/src/commands/passkey.ts +3 -3
  110. package/src/commands/pwd.ts +2 -2
  111. package/src/commands/rm.ts +54 -12
  112. package/src/commands/rmdir.ts +2 -2
  113. package/src/commands/sockets.ts +1 -1
  114. package/src/commands/stat.ts +44 -16
  115. package/src/commands/tail.ts +2 -2
  116. package/src/commands/tar.ts +737 -0
  117. package/src/commands/touch.ts +2 -2
  118. package/src/commands/true.ts +2 -2
  119. package/src/commands/unzip.ts +517 -0
  120. package/src/commands/user.ts +436 -0
  121. package/src/commands/whoami.ts +2 -2
  122. package/src/commands/zip.ts +319 -0
  123. package/src/index.ts +28 -1
@@ -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 { writelnStdout, writelnStderr } from '../shared/helpers.js'
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
- writelnStdout(process, terminal, usage)
11
+ writelnStderr(process, terminal, usage)
12
12
  }
13
13
 
14
14
  export function createCommand(kernel: Kernel, shell: Shell, terminal: Terminal): TerminalCommand {
@@ -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 { writelnStdout } from '../shared/helpers.js'
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
- writelnStdout(process, terminal, usage)
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
+ }