@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
@@ -0,0 +1,693 @@
1
+ import path from 'path';
2
+ import { TerminalEvents } from '@ecmaos/types';
3
+ import { TerminalCommand } from '../shared/terminal-command.js';
4
+ import { writelnStdout, writelnStderr } from '../shared/helpers.js';
5
+ import { createTarPacker, createTarDecoder } from 'modern-tar';
6
+ function printUsage(process, terminal) {
7
+ const usage = `Usage: tar [OPTION]... [FILE]...
8
+ Create, extract, or list tar archives.
9
+
10
+ -c, --create create a new archive
11
+ -x, --extract extract files from an archive
12
+ -t, --list list the contents of an archive
13
+ -f, --file use archive file (required for create, optional for extract/list - uses stdin if omitted)
14
+ -z filter the archive through gzip
15
+ -v, --verbose verbosely list files processed
16
+ -C, --directory change to directory before extracting
17
+ -h, --help display this help and exit`;
18
+ writelnStderr(process, terminal, usage);
19
+ }
20
+ function parseArgs(argv) {
21
+ const options = {
22
+ create: false,
23
+ extract: false,
24
+ list: false,
25
+ file: null,
26
+ gzip: false,
27
+ verbose: false,
28
+ directory: null
29
+ };
30
+ const files = [];
31
+ let i = 0;
32
+ while (i < argv.length) {
33
+ const arg = argv[i];
34
+ if (!arg || typeof arg !== 'string') {
35
+ i++;
36
+ continue;
37
+ }
38
+ if (arg === '--help' || arg === '-h') {
39
+ i++;
40
+ continue;
41
+ }
42
+ else if (arg === '-c' || arg === '--create') {
43
+ options.create = true;
44
+ i++;
45
+ }
46
+ else if (arg === '-x' || arg === '--extract') {
47
+ options.extract = true;
48
+ i++;
49
+ }
50
+ else if (arg === '-t' || arg === '--list') {
51
+ options.list = true;
52
+ i++;
53
+ }
54
+ else if (arg === '-f' || arg === '--file') {
55
+ if (i + 1 < argv.length) {
56
+ i++;
57
+ const nextArg = argv[i];
58
+ options.file = (typeof nextArg === 'string' ? nextArg : null) || null;
59
+ }
60
+ else {
61
+ options.file = null;
62
+ }
63
+ i++;
64
+ }
65
+ else if (arg === '-z') {
66
+ options.gzip = true;
67
+ i++;
68
+ }
69
+ else if (arg === '-v' || arg === '--verbose') {
70
+ options.verbose = true;
71
+ i++;
72
+ }
73
+ else if (arg === '-C' || arg === '--directory') {
74
+ if (i + 1 < argv.length) {
75
+ i++;
76
+ const nextArg = argv[i];
77
+ options.directory = (typeof nextArg === 'string' ? nextArg : null) || null;
78
+ }
79
+ else {
80
+ options.directory = null;
81
+ }
82
+ i++;
83
+ }
84
+ else if (arg.startsWith('-')) {
85
+ // Handle combined flags like -czf
86
+ const flags = arg.slice(1).split('');
87
+ for (const flag of flags) {
88
+ if (flag === 'c')
89
+ options.create = true;
90
+ else if (flag === 'x')
91
+ options.extract = true;
92
+ else if (flag === 't')
93
+ options.list = true;
94
+ else if (flag === 'f') {
95
+ // -f needs to be followed by filename, check if next arg exists
96
+ const nextArg = argv[i + 1];
97
+ if (i + 1 < argv.length && typeof nextArg === 'string' && !nextArg.startsWith('-')) {
98
+ i++;
99
+ options.file = nextArg;
100
+ }
101
+ }
102
+ else if (flag === 'z')
103
+ options.gzip = true;
104
+ else if (flag === 'v')
105
+ options.verbose = true;
106
+ else if (flag === 'C') {
107
+ // -C needs to be followed by directory, check if next arg exists
108
+ const nextArg = argv[i + 1];
109
+ if (i + 1 < argv.length && typeof nextArg === 'string' && !nextArg.startsWith('-')) {
110
+ i++;
111
+ options.directory = nextArg;
112
+ }
113
+ }
114
+ }
115
+ i++;
116
+ }
117
+ else {
118
+ files.push(arg);
119
+ i++;
120
+ }
121
+ }
122
+ return { options, files };
123
+ }
124
+ async function collectFiles(shell, filePaths, basePath = '') {
125
+ const result = [];
126
+ for (const filePath of filePaths) {
127
+ const fullPath = path.resolve(shell.cwd, filePath);
128
+ try {
129
+ const stat = await shell.context.fs.promises.stat(fullPath);
130
+ if (stat.isDirectory()) {
131
+ // Add directory entry
132
+ const relativePath = path.join(basePath, filePath);
133
+ result.push({
134
+ path: relativePath.endsWith('/') ? relativePath : relativePath + '/',
135
+ fullPath,
136
+ isDirectory: true
137
+ });
138
+ // Recursively collect directory contents
139
+ const entries = await shell.context.fs.promises.readdir(fullPath);
140
+ const subFiles = [];
141
+ for (const entry of entries) {
142
+ subFiles.push(path.join(fullPath, entry));
143
+ }
144
+ const subResults = await collectFiles(shell, subFiles.map(f => path.relative(shell.cwd, f)), path.join(basePath, filePath));
145
+ result.push(...subResults);
146
+ }
147
+ else {
148
+ // Add file entry
149
+ result.push({
150
+ path: path.join(basePath, filePath),
151
+ fullPath,
152
+ isDirectory: false
153
+ });
154
+ }
155
+ }
156
+ catch (error) {
157
+ // Skip files that can't be accessed
158
+ continue;
159
+ }
160
+ }
161
+ return result;
162
+ }
163
+ async function createArchive(shell, terminal, process, archivePath, filePaths, options) {
164
+ if (filePaths.length === 0) {
165
+ await writelnStderr(process, terminal, 'tar: no files specified');
166
+ return 1;
167
+ }
168
+ try {
169
+ const fullArchivePath = path.resolve(shell.cwd, archivePath);
170
+ // Collect all files to archive
171
+ const filesToArchive = await collectFiles(shell, filePaths);
172
+ if (filesToArchive.length === 0) {
173
+ await writelnStderr(process, terminal, 'tar: no files to archive');
174
+ return 1;
175
+ }
176
+ // Create tar packer
177
+ const { readable: tarStream, controller } = createTarPacker();
178
+ // Apply gzip compression if requested
179
+ let finalStream = tarStream;
180
+ if (options.gzip) {
181
+ finalStream = tarStream.pipeThrough(new CompressionStream('gzip'));
182
+ }
183
+ // Start writing the archive in the background
184
+ const writePromise = (async () => {
185
+ const archiveHandle = await shell.context.fs.promises.open(fullArchivePath, 'w');
186
+ const writer = archiveHandle.writableWebStream?.()?.getWriter();
187
+ if (!writer) {
188
+ // Fallback: read stream and write in chunks
189
+ const reader = finalStream.getReader();
190
+ try {
191
+ while (true) {
192
+ const { done, value } = await reader.read();
193
+ if (done)
194
+ break;
195
+ await archiveHandle.writeFile(value);
196
+ }
197
+ }
198
+ finally {
199
+ reader.releaseLock();
200
+ await archiveHandle.close();
201
+ }
202
+ }
203
+ else {
204
+ try {
205
+ const reader = finalStream.getReader();
206
+ try {
207
+ while (true) {
208
+ const { done, value } = await reader.read();
209
+ if (done)
210
+ break;
211
+ await writer.write(value);
212
+ }
213
+ }
214
+ finally {
215
+ reader.releaseLock();
216
+ }
217
+ await writer.close();
218
+ }
219
+ finally {
220
+ writer.releaseLock();
221
+ await archiveHandle.close();
222
+ }
223
+ }
224
+ })();
225
+ // Add entries to the tar archive
226
+ for (const file of filesToArchive) {
227
+ if (file.isDirectory) {
228
+ // Add directory entry
229
+ const dirName = file.path.endsWith('/') ? file.path : file.path + '/';
230
+ controller.add({
231
+ name: dirName,
232
+ type: 'directory',
233
+ size: 0
234
+ });
235
+ if (options.verbose) {
236
+ await writelnStdout(process, terminal, dirName);
237
+ }
238
+ }
239
+ else {
240
+ try {
241
+ const handle = await shell.context.fs.promises.open(file.fullPath, 'r');
242
+ const stat = await shell.context.fs.promises.stat(file.fullPath);
243
+ // Create a readable stream from the file
244
+ const fileStream = new ReadableStream({
245
+ async start(controller) {
246
+ try {
247
+ const chunkSize = 64 * 1024; // 64KB chunks
248
+ let offset = 0;
249
+ while (offset < stat.size) {
250
+ const buffer = new Uint8Array(chunkSize);
251
+ const readSize = Math.min(chunkSize, stat.size - offset);
252
+ await handle.read(buffer, 0, readSize, offset);
253
+ const chunk = buffer.subarray(0, readSize);
254
+ controller.enqueue(chunk);
255
+ offset += readSize;
256
+ }
257
+ controller.close();
258
+ }
259
+ catch (error) {
260
+ controller.error(error);
261
+ }
262
+ finally {
263
+ await handle.close();
264
+ }
265
+ }
266
+ });
267
+ // Add file entry to tar
268
+ const entryStream = controller.add({
269
+ name: file.path,
270
+ type: 'file',
271
+ size: stat.size
272
+ });
273
+ // Copy file stream to entry stream
274
+ const fileReader = fileStream.getReader();
275
+ const entryWriter = entryStream.getWriter();
276
+ try {
277
+ while (true) {
278
+ const { done, value } = await fileReader.read();
279
+ if (done)
280
+ break;
281
+ await entryWriter.write(value);
282
+ }
283
+ await entryWriter.close();
284
+ }
285
+ finally {
286
+ fileReader.releaseLock();
287
+ entryWriter.releaseLock();
288
+ }
289
+ if (options.verbose) {
290
+ await writelnStdout(process, terminal, file.path);
291
+ }
292
+ }
293
+ catch (error) {
294
+ await writelnStderr(process, terminal, `tar: ${file.path}: ${error instanceof Error ? error.message : 'Unknown error'}`);
295
+ }
296
+ }
297
+ }
298
+ // Finalize the tar archive
299
+ controller.finalize();
300
+ // Wait for writing to complete
301
+ await writePromise;
302
+ return 0;
303
+ }
304
+ catch (error) {
305
+ await writelnStderr(process, terminal, `tar: ${error instanceof Error ? error.message : 'Unknown error'}`);
306
+ return 1;
307
+ }
308
+ }
309
+ async function extractArchive(kernel, shell, terminal, process, archivePath, options) {
310
+ try {
311
+ // Create readable stream from archive file or stdin
312
+ let archiveStream;
313
+ if (archivePath) {
314
+ // Read from file
315
+ const fullArchivePath = path.resolve(shell.cwd, archivePath);
316
+ // Check if archive exists
317
+ try {
318
+ await shell.context.fs.promises.stat(fullArchivePath);
319
+ }
320
+ catch {
321
+ await writelnStderr(process, terminal, `tar: ${archivePath}: Cannot open: No such file or directory`);
322
+ return 1;
323
+ }
324
+ // Read archive file
325
+ const archiveHandle = await shell.context.fs.promises.open(fullArchivePath, 'r');
326
+ const stat = await shell.context.fs.promises.stat(fullArchivePath);
327
+ if (archiveHandle.readableWebStream) {
328
+ archiveStream = archiveHandle.readableWebStream();
329
+ }
330
+ else {
331
+ // Fallback: create stream manually
332
+ archiveStream = new ReadableStream({
333
+ async start(controller) {
334
+ try {
335
+ const chunkSize = 64 * 1024; // 64KB chunks
336
+ let offset = 0;
337
+ while (offset < stat.size) {
338
+ const buffer = new Uint8Array(chunkSize);
339
+ const readSize = Math.min(chunkSize, stat.size - offset);
340
+ await archiveHandle.read(buffer, 0, readSize, offset);
341
+ const chunk = buffer.subarray(0, readSize);
342
+ controller.enqueue(chunk);
343
+ offset += readSize;
344
+ }
345
+ controller.close();
346
+ }
347
+ catch (error) {
348
+ controller.error(error);
349
+ }
350
+ finally {
351
+ await archiveHandle.close();
352
+ }
353
+ }
354
+ });
355
+ }
356
+ }
357
+ else {
358
+ // Read from stdin
359
+ if (!process || !process.stdin) {
360
+ await writelnStderr(process, terminal, 'tar: no input provided');
361
+ return 1;
362
+ }
363
+ archiveStream = process.stdin;
364
+ }
365
+ // Apply gzip decompression if requested
366
+ let tarStream = archiveStream;
367
+ if (options.gzip) {
368
+ tarStream = archiveStream.pipeThrough(new DecompressionStream('gzip'));
369
+ }
370
+ // Extract using modern-tar decoder
371
+ let hasError = false;
372
+ let interrupted = false;
373
+ const interruptHandler = () => { interrupted = true; };
374
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler);
375
+ try {
376
+ const decoder = createTarDecoder();
377
+ const entriesStream = tarStream.pipeThrough(decoder);
378
+ const entriesReader = entriesStream.getReader();
379
+ try {
380
+ while (true) {
381
+ if (interrupted)
382
+ break;
383
+ const { done, value: entry } = await entriesReader.read();
384
+ if (done)
385
+ break;
386
+ if (!entry)
387
+ continue;
388
+ if (options.verbose) {
389
+ await writelnStdout(process, terminal, entry.header.name);
390
+ }
391
+ try {
392
+ // Normalize the entry name: strip leading slashes and resolve relative to extraction directory
393
+ let entryName = entry.header.name;
394
+ // Remove leading slashes to make it relative
395
+ while (entryName.startsWith('/')) {
396
+ entryName = entryName.slice(1);
397
+ }
398
+ // Skip empty entries (like just "/")
399
+ if (!entryName) {
400
+ await entry.body.cancel();
401
+ continue;
402
+ }
403
+ // Determine extraction base directory
404
+ const extractBase = options.directory
405
+ ? path.resolve(shell.cwd, options.directory)
406
+ : shell.cwd;
407
+ const targetPath = path.resolve(extractBase, entryName);
408
+ // Security check: ensure target path is within extract base (prevent directory traversal)
409
+ const resolvedBase = path.resolve(extractBase);
410
+ const resolvedTarget = path.resolve(targetPath);
411
+ if (!resolvedTarget.startsWith(resolvedBase + path.sep) && resolvedTarget !== resolvedBase) {
412
+ await writelnStderr(process, terminal, `tar: ${entry.header.name}: path outside extraction directory`);
413
+ await entry.body.cancel();
414
+ hasError = true;
415
+ continue;
416
+ }
417
+ if (entry.header.type === 'directory' || entry.header.name.endsWith('/')) {
418
+ // Create directory
419
+ try {
420
+ await shell.context.fs.promises.mkdir(targetPath, { recursive: true });
421
+ }
422
+ catch (error) {
423
+ // Directory might already exist, ignore
424
+ }
425
+ // Drain the body stream for directories
426
+ await entry.body.cancel();
427
+ }
428
+ else if (entry.header.type === 'file') {
429
+ // Extract file
430
+ const dirPath = path.dirname(targetPath);
431
+ try {
432
+ await shell.context.fs.promises.mkdir(dirPath, { recursive: true });
433
+ }
434
+ catch {
435
+ // Directory might already exist
436
+ }
437
+ // Read file content from entry stream
438
+ const chunks = [];
439
+ const reader = entry.body.getReader();
440
+ try {
441
+ while (true) {
442
+ const { done, value } = await reader.read();
443
+ if (done)
444
+ break;
445
+ if (value)
446
+ chunks.push(value);
447
+ }
448
+ }
449
+ finally {
450
+ reader.releaseLock();
451
+ }
452
+ // Combine chunks and write file
453
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
454
+ const fileData = new Uint8Array(totalLength);
455
+ let offset = 0;
456
+ for (const chunk of chunks) {
457
+ fileData.set(chunk, offset);
458
+ offset += chunk.length;
459
+ }
460
+ await shell.context.fs.promises.writeFile(targetPath, fileData);
461
+ }
462
+ else {
463
+ // For other entry types (symlinks, etc.), drain the body
464
+ await entry.body.cancel();
465
+ }
466
+ }
467
+ catch (error) {
468
+ await writelnStderr(process, terminal, `tar: ${entry.header.name}: ${error instanceof Error ? error.message : 'Unknown error'}`);
469
+ hasError = true;
470
+ // Drain the body stream on error
471
+ try {
472
+ await entry.body.cancel();
473
+ }
474
+ catch {
475
+ // Ignore cancel errors
476
+ }
477
+ }
478
+ }
479
+ }
480
+ finally {
481
+ entriesReader.releaseLock();
482
+ }
483
+ }
484
+ finally {
485
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler);
486
+ }
487
+ return hasError ? 1 : 0;
488
+ }
489
+ catch (error) {
490
+ await writelnStderr(process, terminal, `tar: ${error instanceof Error ? error.message : 'Unknown error'}`);
491
+ return 1;
492
+ }
493
+ }
494
+ async function listArchive(kernel, shell, terminal, process, archivePath, options) {
495
+ try {
496
+ // Create readable stream from archive file or stdin
497
+ let archiveStream;
498
+ if (archivePath) {
499
+ // Read from file
500
+ const fullArchivePath = path.resolve(shell.cwd, archivePath);
501
+ // Check if archive exists
502
+ try {
503
+ await shell.context.fs.promises.stat(fullArchivePath);
504
+ }
505
+ catch {
506
+ await writelnStderr(process, terminal, `tar: ${archivePath}: Cannot open: No such file or directory`);
507
+ return 1;
508
+ }
509
+ // Read archive file
510
+ const archiveHandle = await shell.context.fs.promises.open(fullArchivePath, 'r');
511
+ const stat = await shell.context.fs.promises.stat(fullArchivePath);
512
+ if (archiveHandle.readableWebStream) {
513
+ archiveStream = archiveHandle.readableWebStream();
514
+ }
515
+ else {
516
+ // Fallback: create stream manually
517
+ archiveStream = new ReadableStream({
518
+ async start(controller) {
519
+ try {
520
+ const chunkSize = 64 * 1024; // 64KB chunks
521
+ let offset = 0;
522
+ while (offset < stat.size) {
523
+ const buffer = new Uint8Array(chunkSize);
524
+ const readSize = Math.min(chunkSize, stat.size - offset);
525
+ await archiveHandle.read(buffer, 0, readSize, offset);
526
+ const chunk = buffer.subarray(0, readSize);
527
+ controller.enqueue(chunk);
528
+ offset += readSize;
529
+ }
530
+ controller.close();
531
+ }
532
+ catch (error) {
533
+ controller.error(error);
534
+ }
535
+ finally {
536
+ await archiveHandle.close();
537
+ }
538
+ }
539
+ });
540
+ }
541
+ }
542
+ else {
543
+ // Read from stdin
544
+ if (!process || !process.stdin) {
545
+ await writelnStderr(process, terminal, 'tar: no input provided');
546
+ return 1;
547
+ }
548
+ archiveStream = process.stdin;
549
+ }
550
+ // Apply gzip decompression if requested
551
+ let tarStream = archiveStream;
552
+ if (options.gzip) {
553
+ tarStream = archiveStream.pipeThrough(new DecompressionStream('gzip'));
554
+ }
555
+ // List contents using modern-tar decoder
556
+ let hasError = false;
557
+ let interrupted = false;
558
+ const interruptHandler = () => { interrupted = true; };
559
+ kernel.terminal.events.on(TerminalEvents.INTERRUPT, interruptHandler);
560
+ try {
561
+ const decoder = createTarDecoder();
562
+ const entriesStream = tarStream.pipeThrough(decoder);
563
+ const entriesReader = entriesStream.getReader();
564
+ try {
565
+ while (true) {
566
+ if (interrupted)
567
+ break;
568
+ const { done, value: entry } = await entriesReader.read();
569
+ if (done)
570
+ break;
571
+ if (!entry)
572
+ continue;
573
+ await writelnStdout(process, terminal, entry.header.name);
574
+ // Drain the body stream since we're just listing
575
+ await entry.body.cancel();
576
+ }
577
+ }
578
+ finally {
579
+ entriesReader.releaseLock();
580
+ }
581
+ }
582
+ catch (error) {
583
+ await writelnStderr(process, terminal, `tar: ${error instanceof Error ? error.message : 'Unknown error'}`);
584
+ hasError = true;
585
+ }
586
+ finally {
587
+ kernel.terminal.events.off(TerminalEvents.INTERRUPT, interruptHandler);
588
+ }
589
+ return hasError ? 1 : 0;
590
+ }
591
+ catch (error) {
592
+ await writelnStderr(process, terminal, `tar: ${error instanceof Error ? error.message : 'Unknown error'}`);
593
+ return 1;
594
+ }
595
+ }
596
+ export function createCommand(kernel, shell, terminal) {
597
+ return new TerminalCommand({
598
+ command: 'tar',
599
+ description: 'Create, extract, or list tar archives',
600
+ kernel,
601
+ shell,
602
+ terminal,
603
+ run: async (pid, argv) => {
604
+ const process = kernel.processes.get(pid);
605
+ if (argv.length > 0 && (argv[0] === '--help' || argv[0] === '-h')) {
606
+ printUsage(process, terminal);
607
+ return 0;
608
+ }
609
+ const { options, files } = parseArgs(argv);
610
+ // Validate operation mode
611
+ const operationCount = [options.create, options.extract, options.list].filter(Boolean).length;
612
+ if (operationCount === 0) {
613
+ await writelnStderr(process, terminal, 'tar: You must specify one of the -c, -x, or -t options');
614
+ await writelnStderr(process, terminal, "Try 'tar --help' for more information.");
615
+ return 1;
616
+ }
617
+ if (operationCount > 1) {
618
+ await writelnStderr(process, terminal, 'tar: You may not specify more than one of -c, -x, or -t');
619
+ return 1;
620
+ }
621
+ // Validate file option for create (create always needs a file)
622
+ if (options.create && !options.file) {
623
+ await writelnStderr(process, terminal, 'tar: option requires an argument -- f');
624
+ await writelnStderr(process, terminal, "Try 'tar --help' for more information.");
625
+ return 1;
626
+ }
627
+ // For extract and list, if no file is specified, use stdin
628
+ // Expand glob patterns in file list
629
+ const expandGlob = async (pattern) => {
630
+ if (!pattern.includes('*') && !pattern.includes('?')) {
631
+ return [pattern];
632
+ }
633
+ const lastSlashIndex = pattern.lastIndexOf('/');
634
+ const searchDir = lastSlashIndex !== -1
635
+ ? path.resolve(shell.cwd, pattern.substring(0, lastSlashIndex + 1))
636
+ : shell.cwd;
637
+ const globPattern = lastSlashIndex !== -1
638
+ ? pattern.substring(lastSlashIndex + 1)
639
+ : pattern;
640
+ try {
641
+ const entries = await shell.context.fs.promises.readdir(searchDir);
642
+ const regexPattern = globPattern
643
+ .replace(/\./g, '\\.')
644
+ .replace(/\*/g, '.*')
645
+ .replace(/\?/g, '.');
646
+ const regex = new RegExp(`^${regexPattern}$`);
647
+ const matches = entries.filter(entry => regex.test(entry));
648
+ if (lastSlashIndex !== -1) {
649
+ const dirPart = pattern.substring(0, lastSlashIndex + 1);
650
+ return matches.map(match => dirPart + match);
651
+ }
652
+ return matches;
653
+ }
654
+ catch (error) {
655
+ return [];
656
+ }
657
+ };
658
+ // Expand glob patterns in file list (shell should have already expanded them, but keep as fallback)
659
+ const expandedFiles = [];
660
+ for (const filePattern of files) {
661
+ if (typeof filePattern !== 'string') {
662
+ // Skip non-string entries (shouldn't happen, but handle gracefully)
663
+ continue;
664
+ }
665
+ // Check if this looks like a glob pattern (shell should have expanded it, but handle as fallback)
666
+ const expanded = await expandGlob(filePattern);
667
+ if (expanded.length === 0) {
668
+ // If glob doesn't match anything, include the pattern as-is (might be a literal path)
669
+ expandedFiles.push(filePattern);
670
+ }
671
+ else {
672
+ expandedFiles.push(...expanded);
673
+ }
674
+ }
675
+ // Execute operation
676
+ if (options.create) {
677
+ if (!options.file) {
678
+ await writelnStderr(process, terminal, 'tar: option requires an argument -- f');
679
+ return 1;
680
+ }
681
+ return await createArchive(shell, terminal, process, options.file, expandedFiles, options);
682
+ }
683
+ else if (options.extract) {
684
+ return await extractArchive(kernel, shell, terminal, process, options.file, options);
685
+ }
686
+ else if (options.list) {
687
+ return await listArchive(kernel, shell, terminal, process, options.file, options);
688
+ }
689
+ return 1;
690
+ }
691
+ });
692
+ }
693
+ //# sourceMappingURL=tar.js.map