@ecmaos/coreutils 0.2.1 → 0.3.1

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