typescript-virtual-container 1.5.5 → 1.5.6

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.
@@ -1,22 +1,74 @@
1
- import { getFlag, ifFlag } from "./command-helpers";
1
+ import { ifFlag } from "./command-helpers";
2
2
  import { resolvePath } from "./helpers";
3
3
  /**
4
- * Stream editor for filtering and transforming text lines.
4
+ * Stream editor supports s/pat/rep/[gI], d, p, q, =, addresses (N, /re/, N,M, /re/,/re/, $).
5
5
  * @category text
6
- * @params ["-e <expr> [file]", "s/pattern/replace/[g]"]
6
+ * @params ["[-n] [-e <expr>] [file]"]
7
7
  */
8
8
  export const sedCommand = {
9
9
  name: "sed",
10
10
  description: "Stream editor for filtering and transforming text",
11
11
  category: "text",
12
- params: ["-e <expr> [file]", "s/pattern/replace/[g]"],
12
+ params: ["[-n] [-e <expr>] [file]"],
13
13
  run: ({ authUser, shell, cwd, args, stdin }) => {
14
14
  const inPlace = ifFlag(args, ["-i"]);
15
- const expr = getFlag(args, ["-e"]) ??
16
- args.find((a) => !a.startsWith("-"));
17
- const fileArg = args.filter((a) => !a.startsWith("-") && a !== expr).pop();
18
- if (!expr)
15
+ const suppressAuto = ifFlag(args, ["-n"]);
16
+ // Collect all -e expressions and the first non-flag positional
17
+ const exprs = [];
18
+ let fileArg;
19
+ let i = 0;
20
+ while (i < args.length) {
21
+ const a = args[i];
22
+ if (a === "-e" || a === "--expression") {
23
+ i++;
24
+ if (args[i])
25
+ exprs.push(args[i]);
26
+ i++;
27
+ }
28
+ else if (a === "-n" || a === "-i") {
29
+ i++;
30
+ }
31
+ else if (a.startsWith("-e")) {
32
+ exprs.push(a.slice(2));
33
+ i++;
34
+ }
35
+ else if (!a.startsWith("-")) {
36
+ if (exprs.length === 0)
37
+ exprs.push(a);
38
+ else
39
+ fileArg = a;
40
+ i++;
41
+ }
42
+ else {
43
+ i++;
44
+ }
45
+ }
46
+ // If only one positional collected as expr and no file yet, check for file after
47
+ // Re-parse: first non-flag that follows all -e is the file
48
+ if (exprs.length === 0)
19
49
  return { stderr: "sed: no expression", exitCode: 1 };
50
+ // Re-check: if exprs[0] was set from positional, remaining positionals are files
51
+ {
52
+ let foundExprFromFlag = false;
53
+ let j = 0;
54
+ while (j < args.length) {
55
+ const a = args[j];
56
+ if (a === "-e" || a === "--expression") {
57
+ foundExprFromFlag = true;
58
+ j += 2;
59
+ }
60
+ else if (a.startsWith("-e")) {
61
+ foundExprFromFlag = true;
62
+ j++;
63
+ }
64
+ else
65
+ j++;
66
+ }
67
+ if (!foundExprFromFlag) {
68
+ // expr is first positional, file is second
69
+ fileArg = args.filter((a) => !a.startsWith("-")).slice(1)[0];
70
+ }
71
+ }
20
72
  let content = stdin ?? "";
21
73
  if (fileArg) {
22
74
  const p = resolvePath(cwd, fileArg);
@@ -24,32 +76,162 @@ export const sedCommand = {
24
76
  content = shell.vfs.readFile(p);
25
77
  }
26
78
  catch {
27
- return {
28
- stderr: `sed: ${fileArg}: No such file or directory`,
29
- exitCode: 1,
30
- };
31
- }
32
- }
33
- // Parse s/from/to/[g]
34
- const sMatch = expr.match(/^s([^a-zA-Z0-9])(.+?)\1(.*?)\1([gi]*)$/);
35
- if (!sMatch)
36
- return { stderr: `sed: unrecognized command: ${expr}`, exitCode: 1 };
37
- const [, , from, to, flags] = sMatch;
38
- const regexFlags = (flags ?? "").includes("i")
39
- ? "gi"
40
- : (flags ?? "").includes("g")
41
- ? "g"
42
- : "";
43
- let regex;
44
- try {
45
- regex = new RegExp(from, regexFlags || "");
46
- }
47
- catch (_e) {
48
- return { stderr: `sed: invalid regex: ${from}`, exitCode: 1 };
49
- }
50
- const result = (flags ?? "").includes("g") || regexFlags.includes("g")
51
- ? content.replace(regex, to ?? "")
52
- : content.replace(regex, to ?? "");
79
+ return { stderr: `sed: ${fileArg}: No such file or directory`, exitCode: 1 };
80
+ }
81
+ }
82
+ function parseAddr(s) {
83
+ if (!s)
84
+ return [undefined, s];
85
+ if (s[0] === "$")
86
+ return [{ type: "last" }, s.slice(1)];
87
+ if (/^\d/.test(s)) {
88
+ const m = s.match(/^(\d+)(.*)/s);
89
+ if (m)
90
+ return [{ type: "line", n: parseInt(m[1], 10) }, m[2]];
91
+ }
92
+ if (s[0] === "/") {
93
+ const end = s.indexOf("/", 1);
94
+ if (end !== -1) {
95
+ try {
96
+ const re = new RegExp(s.slice(1, end));
97
+ return [{ type: "regex", re }, s.slice(end + 1)];
98
+ }
99
+ catch { /* bad regex */ }
100
+ }
101
+ }
102
+ return [undefined, s];
103
+ }
104
+ function parseInstrs(expr) {
105
+ const instrs = [];
106
+ // Split on unquoted semicolons or newlines
107
+ const parts = expr.split(/\n|(?<=^|[^\\]);/);
108
+ for (const raw of parts) {
109
+ const part = raw.trim();
110
+ if (!part || part.startsWith("#"))
111
+ continue;
112
+ let rest = part;
113
+ const [addr1, after1] = parseAddr(rest);
114
+ rest = after1.trim();
115
+ let addr2;
116
+ if (rest[0] === ",") {
117
+ rest = rest.slice(1).trim();
118
+ const [a2, after2] = parseAddr(rest);
119
+ addr2 = a2;
120
+ rest = after2.trim();
121
+ }
122
+ const op = rest[0];
123
+ if (!op)
124
+ continue;
125
+ if (op === "s") {
126
+ // s/from/to/flags
127
+ const delim = rest[1] ?? "/";
128
+ const sRe = new RegExp(`^s${re(delim)}((?:[^${re(delim)}\\\\]|\\\\.)*)${re(delim)}((?:[^${re(delim)}\\\\]|\\\\.)*)${re(delim)}([gGiIp]*)$`);
129
+ const m = rest.match(sRe);
130
+ if (!m) {
131
+ instrs.push({ op: "d", addr1, addr2 });
132
+ continue;
133
+ } // bad expr, skip
134
+ const flags = m[3] ?? "";
135
+ let from;
136
+ try {
137
+ from = new RegExp(m[1], flags.includes("i") || flags.includes("I") ? "i" : "");
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ instrs.push({ op: "s", addr1, addr2, from, to: m[2], global: flags.includes("g") || flags.includes("G"), print: flags.includes("p") });
143
+ }
144
+ else if (op === "d") {
145
+ instrs.push({ op: "d", addr1, addr2 });
146
+ }
147
+ else if (op === "p") {
148
+ instrs.push({ op: "p", addr1, addr2 });
149
+ }
150
+ else if (op === "q") {
151
+ instrs.push({ op: "q", addr1 });
152
+ }
153
+ else if (op === "=") {
154
+ instrs.push({ op: "=", addr1, addr2 });
155
+ }
156
+ }
157
+ return instrs;
158
+ }
159
+ function re(c) {
160
+ return c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ }
162
+ const allInstrs = exprs.flatMap(parseInstrs);
163
+ const lines = content.split("\n");
164
+ // Remove trailing empty string from trailing newline
165
+ if (lines[lines.length - 1] === "")
166
+ lines.pop();
167
+ const total = lines.length;
168
+ function matchesAddr(addr, lineNo, line) {
169
+ if (!addr)
170
+ return true;
171
+ if (addr.type === "line")
172
+ return lineNo === addr.n;
173
+ if (addr.type === "last")
174
+ return lineNo === total;
175
+ return addr.re.test(line);
176
+ }
177
+ function inRange(instr, lineNo, line, rangeActive) {
178
+ const { addr1, addr2 } = instr;
179
+ if (!addr1)
180
+ return true;
181
+ if (!addr2)
182
+ return matchesAddr(addr1, lineNo, line);
183
+ // Two-address range
184
+ let active = rangeActive.get(instr) ?? false;
185
+ if (!active && matchesAddr(addr1, lineNo, line)) {
186
+ active = true;
187
+ rangeActive.set(instr, true);
188
+ }
189
+ if (active && matchesAddr(addr2, lineNo, line)) {
190
+ rangeActive.set(instr, false);
191
+ return true;
192
+ }
193
+ if (active)
194
+ return true;
195
+ return false;
196
+ }
197
+ const out = [];
198
+ const rangeActive = new Map();
199
+ let quit = false;
200
+ for (let li = 0; li < lines.length && !quit; li++) {
201
+ let line = lines[li];
202
+ const lineNo = li + 1;
203
+ let deleted = false;
204
+ for (const instr of allInstrs) {
205
+ if (!inRange(instr, lineNo, line, rangeActive))
206
+ continue;
207
+ if (instr.op === "d") {
208
+ deleted = true;
209
+ break;
210
+ }
211
+ if (instr.op === "p") {
212
+ out.push(line);
213
+ }
214
+ if (instr.op === "=") {
215
+ out.push(String(lineNo));
216
+ }
217
+ if (instr.op === "q") {
218
+ quit = true;
219
+ }
220
+ if (instr.op === "s") {
221
+ const replaced = instr.global
222
+ ? line.replace(new RegExp(instr.from.source, instr.from.flags.includes("i") ? "gi" : "g"), instr.to)
223
+ : line.replace(instr.from, instr.to);
224
+ if (replaced !== line) {
225
+ line = replaced;
226
+ if (instr.print && suppressAuto)
227
+ out.push(line);
228
+ }
229
+ }
230
+ }
231
+ if (!deleted && !suppressAuto)
232
+ out.push(line);
233
+ }
234
+ const result = out.join("\n") + (out.length > 0 ? "\n" : "");
53
235
  if (inPlace && fileArg) {
54
236
  const p = resolvePath(cwd, fileArg);
55
237
  shell.writeFileAsUser(authUser, p, result);
@@ -125,6 +125,32 @@ function parseBlocks(lines) {
125
125
  }
126
126
  blocks.push({ type: "while", cond, body });
127
127
  }
128
+ else if (line.startsWith("until ")) {
129
+ const cond = line
130
+ .replace(/^until\s+/, "")
131
+ .replace(/;\s*do\s*$/, "")
132
+ .trim();
133
+ const body = [];
134
+ i++;
135
+ while (i < lines.length && lines[i]?.trim() !== "done") {
136
+ const l = lines[i].trim().replace(/^do\s+/, "");
137
+ if (l && l !== "do")
138
+ body.push(l);
139
+ i++;
140
+ }
141
+ blocks.push({ type: "until", cond, body });
142
+ }
143
+ else if (/^[A-Za-z_][A-Za-z0-9_]*=\s*\(/.test(line)) {
144
+ // Array assignment: arr=(elem1 elem2 ...)
145
+ const arrMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=\s*\(([^)]*)\)$/);
146
+ if (arrMatch) {
147
+ const elems = arrMatch[2].trim().split(/\s+/).filter(Boolean);
148
+ blocks.push({ type: "array", name: arrMatch[1], elements: elems });
149
+ }
150
+ else {
151
+ blocks.push({ type: "cmd", line });
152
+ }
153
+ }
128
154
  else if (line.startsWith("case ") && line.endsWith(" in") || line.match(/^case\s+.+\s+in$/)) {
129
155
  const caseExpr = line.replace(/^case\s+/, "").replace(/\s+in$/, "").trim();
130
156
  const patterns = [];
@@ -353,6 +379,22 @@ async function runBlocks(blocks, ctx) {
353
379
  iterations++;
354
380
  }
355
381
  }
382
+ else if (block.type === "until") {
383
+ let iterations = 0;
384
+ while (iterations < 1000 && !(await evalCondition(block.cond, ctx))) {
385
+ const sub = await runBlocks(parseBlocks(block.body), ctx);
386
+ if (sub.stdout)
387
+ output += `${sub.stdout}\n`;
388
+ if (sub.closeSession)
389
+ return sub;
390
+ iterations++;
391
+ }
392
+ }
393
+ else if (block.type === "array") {
394
+ // Store array: arr[0]=e0, arr[1]=e1, ..., arr=space-joined (for ${arr[@]})
395
+ block.elements.forEach((el, idx) => { ctx.env.vars[`${block.name}[${idx}]`] = el; });
396
+ ctx.env.vars[block.name] = block.elements.join(" ");
397
+ }
356
398
  else if (block.type === "case") {
357
399
  const expanded = await expandVars(block.expr, ctx.env.vars, ctx.env.lastExitCode, ctx);
358
400
  for (const pat of block.patterns) {
@@ -0,0 +1,6 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Trace system calls and signals (stub — runs command, emits fake strace output).
4
+ * @category system
5
+ */
6
+ export declare const straceCommand: ShellModule;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Trace system calls and signals (stub — runs command, emits fake strace output).
3
+ * @category system
4
+ */
5
+ export const straceCommand = {
6
+ name: "strace",
7
+ description: "Trace system calls and signals",
8
+ category: "system",
9
+ params: ["[-e <expr>] [-o <file>] <command> [args]"],
10
+ run: ({ args }) => {
11
+ const cmd = args.find((a) => !a.startsWith("-"));
12
+ if (!cmd)
13
+ return { stderr: "strace: must have PROG [ARGS] or -p PID", exitCode: 1 };
14
+ const pid = Math.floor(Math.random() * 30000) + 1000;
15
+ const lines = [
16
+ `execve("/usr/bin/${cmd}", ["${cmd}"${args.slice(1).map((a) => `, "${a}"`).join("")}], 0x... /* ... vars */) = 0`,
17
+ `brk(NULL) = 0x${(Math.random() * 0xfffff | 0).toString(16)}000`,
18
+ `access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)`,
19
+ `openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3`,
20
+ `fstat(3, {st_mode=S_IFREG|0644, st_size=...}) = 0`,
21
+ `close(3) = 0`,
22
+ `+++ exited with 0 +++`,
23
+ ];
24
+ return { stderr: lines.join("\n"), exitCode: 0 };
25
+ },
26
+ };
@@ -1,6 +1,7 @@
1
1
  import type { ShellModule } from "../types/commands";
2
2
  /**
3
- * Archive or extract files with tar and optional gzip compression.
3
+ * Archive or extract files with tar writes real POSIX ustar binary format.
4
+ * Supports -c/-x/-t, -z (gzip), -j (bzip2 stub), -v (verbose), -f.
4
5
  * @category archive
5
6
  * @params ["[-czf|-xzf|-tf] <archive> [files...]"]
6
7
  */
@@ -1,6 +1,71 @@
1
+ import { gunzipSync, gzipSync } from "fflate";
1
2
  import { resolvePath } from "./helpers";
3
+ // ── POSIX ustar tar format ────────────────────────────────────────────────────
4
+ function makeTarHeader(name, size, isDir) {
5
+ const hdr = Buffer.alloc(512);
6
+ const enc = (s, off, len) => {
7
+ const b = Buffer.from(s, "ascii");
8
+ b.copy(hdr, off, 0, Math.min(b.length, len));
9
+ };
10
+ enc(isDir ? `${name}/` : name, 0, 100);
11
+ enc(isDir ? "0000755\0" : "0000644\0", 100, 8);
12
+ enc("0000000\0", 108, 8);
13
+ enc("0000000\0", 116, 8);
14
+ enc(size.toString(8).padStart(11, "0") + "\0", 124, 12);
15
+ enc(Math.floor(Date.now() / 1000).toString(8).padStart(11, "0") + "\0", 136, 12);
16
+ hdr[156] = isDir ? 0x35 : 0x30; // '5' dir, '0' file
17
+ enc("ustar\0", 257, 6);
18
+ enc("00", 263, 2);
19
+ enc("root\0", 265, 32); // uname
20
+ enc("root\0", 297, 32); // gname
21
+ // Checksum: fill field with spaces, compute, write
22
+ for (let i = 148; i < 156; i++)
23
+ hdr[i] = 0x20;
24
+ let sum = 0;
25
+ for (let i = 0; i < 512; i++)
26
+ sum += hdr[i];
27
+ Buffer.from(`${sum.toString(8).padStart(6, "0")}\0 `).copy(hdr, 148);
28
+ return hdr;
29
+ }
30
+ function tarPad(size) {
31
+ const rem = size % 512;
32
+ return rem === 0 ? Buffer.alloc(0) : Buffer.alloc(512 - rem);
33
+ }
34
+ function buildTar(entries) {
35
+ const parts = [];
36
+ for (const { name, content, isDir } of entries) {
37
+ parts.push(makeTarHeader(name, isDir ? 0 : content.length, isDir));
38
+ if (!isDir) {
39
+ parts.push(content);
40
+ parts.push(tarPad(content.length));
41
+ }
42
+ }
43
+ parts.push(Buffer.alloc(1024)); // end-of-archive
44
+ return Buffer.concat(parts);
45
+ }
46
+ function parseTar(raw) {
47
+ const files = [];
48
+ let off = 0;
49
+ while (off + 512 <= raw.length) {
50
+ const hdr = raw.slice(off, off + 512);
51
+ if (hdr.every((b) => b === 0))
52
+ break;
53
+ const name = hdr.slice(0, 100).toString("ascii").replace(/\0.*/, "");
54
+ const sizeStr = hdr.slice(124, 135).toString("ascii").replace(/\0.*/, "").trim();
55
+ const size = parseInt(sizeStr, 8) || 0;
56
+ const typeflag = hdr[156];
57
+ off += 512;
58
+ if (name && typeflag !== 0x35 && typeflag !== 53) { // not a directory
59
+ const content = raw.slice(off, off + size);
60
+ files.push({ name, content });
61
+ }
62
+ off += Math.ceil(size / 512) * 512;
63
+ }
64
+ return files;
65
+ }
2
66
  /**
3
- * Archive or extract files with tar and optional gzip compression.
67
+ * Archive or extract files with tar writes real POSIX ustar binary format.
68
+ * Supports -c/-x/-t, -z (gzip), -j (bzip2 stub), -v (verbose), -f.
4
69
  * @category archive
5
70
  * @params ["[-czf|-xzf|-tf] <archive> [files...]"]
6
71
  */
@@ -10,17 +75,15 @@ export const tarCommand = {
10
75
  category: "archive",
11
76
  params: ["[-czf|-xzf|-tf] <archive> [files...]"],
12
77
  run: ({ authUser, shell, cwd, args }) => {
13
- // Expand combined flags: -czf or czf (bare mode string) → ["-c", "-z", "-f"]
78
+ // Expand combined flags: -czf → ["-c", "-z", "-f"]
14
79
  const expanded = [];
15
80
  let foundModeStr = false;
16
81
  for (const a of args) {
17
82
  if (/^-[a-zA-Z]{2,}$/.test(a)) {
18
- // -czf style
19
83
  for (const ch of a.slice(1))
20
84
  expanded.push(`-${ch}`);
21
85
  }
22
- else if (!foundModeStr && /^[cxtdru]{1,}[a-zA-Z]*$/.test(a) && !a.includes("/") && !a.startsWith("-")) {
23
- // czf bare style (first non-path arg)
86
+ else if (!foundModeStr && /^[cxtdru][a-zA-Z]*$/.test(a) && !a.includes("/") && !a.startsWith("-")) {
24
87
  foundModeStr = true;
25
88
  for (const ch of a)
26
89
  expanded.push(`-${ch}`);
@@ -32,70 +95,93 @@ export const tarCommand = {
32
95
  const create = expanded.includes("-c");
33
96
  const extract = expanded.includes("-x");
34
97
  const list = expanded.includes("-t");
98
+ const useGzip = expanded.includes("-z");
99
+ const verbose = expanded.includes("-v");
35
100
  const fIdx = expanded.indexOf("-f");
36
101
  const archiveName = fIdx !== -1
37
102
  ? expanded[fIdx + 1]
38
- : expanded.find((a) => a.endsWith(".tar") || a.endsWith(".tar.gz") || a.endsWith(".tgz"));
39
- if (!create && !extract && !list) {
40
- return { stderr: "tar: must specify -c, -x, or -t\n", exitCode: 1 };
41
- }
103
+ : expanded.find((a) => a.endsWith(".tar") || a.endsWith(".tar.gz") || a.endsWith(".tgz") || a.endsWith(".tar.bz2"));
104
+ if (!create && !extract && !list)
105
+ return { stderr: "tar: must specify -c, -x, or -t", exitCode: 1 };
42
106
  if (!archiveName)
43
- return { stderr: "tar: no archive specified\n", exitCode: 1 };
107
+ return { stderr: "tar: no archive specified", exitCode: 1 };
44
108
  const archivePath = resolvePath(cwd, archiveName);
109
+ const autoGzip = useGzip || archiveName.endsWith(".gz") || archiveName.endsWith(".tgz");
45
110
  if (create) {
46
- // Skip flags and archive name from file list
47
- const skipNext2 = new Set();
48
- if (fIdx !== -1)
49
- skipNext2.add(fIdx + 1);
50
- const fileArgs = expanded.filter((a, i) => !a.startsWith("-") && a !== archiveName && !skipNext2.has(i));
51
- const entries = {};
111
+ const skipSet = new Set();
112
+ if (fIdx !== -1 && expanded[fIdx + 1])
113
+ skipSet.add(expanded[fIdx + 1]);
114
+ const fileArgs = expanded.filter((a) => !a.startsWith("-") && !skipSet.has(a));
115
+ const entries = [];
116
+ const verboseLines = [];
52
117
  for (const f of fileArgs) {
53
118
  const p = resolvePath(cwd, f);
54
- try {
55
- const stat = shell.vfs.stat(p);
56
- if (stat.type === "file")
57
- entries[f] = shell.vfs.readFile(p);
58
- else {
59
- const walk = (dir, prefix) => {
60
- for (const e of shell.vfs.list(dir)) {
61
- const full = `${dir}/${e}`, rel = `${prefix}/${e}`;
62
- const s = shell.vfs.stat(full);
63
- if (s.type === "file")
64
- entries[rel] = shell.vfs.readFile(full);
65
- else
66
- walk(full, rel);
67
- }
68
- };
69
- walk(p, f);
70
- }
119
+ if (!shell.vfs.exists(p))
120
+ return { stderr: `tar: ${f}: No such file or directory`, exitCode: 1 };
121
+ const st = shell.vfs.stat(p);
122
+ if (st.type === "file") {
123
+ const content = shell.vfs.readFileRaw(p);
124
+ entries.push({ name: f, content, isDir: false });
125
+ if (verbose)
126
+ verboseLines.push(f);
71
127
  }
72
- catch {
73
- return {
74
- stderr: `tar: ${f}: No such file or directory`,
75
- exitCode: 1,
128
+ else {
129
+ entries.push({ name: f, content: Buffer.alloc(0), isDir: true });
130
+ if (verbose)
131
+ verboseLines.push(`${f}/`);
132
+ const walk = (dir, prefix) => {
133
+ for (const e of shell.vfs.list(dir)) {
134
+ const full = `${dir}/${e}`, rel = `${prefix}/${e}`;
135
+ const s = shell.vfs.stat(full);
136
+ if (s.type === "directory") {
137
+ entries.push({ name: rel, content: Buffer.alloc(0), isDir: true });
138
+ if (verbose)
139
+ verboseLines.push(`${rel}/`);
140
+ walk(full, rel);
141
+ }
142
+ else {
143
+ const content = shell.vfs.readFileRaw(full);
144
+ entries.push({ name: rel, content, isDir: false });
145
+ if (verbose)
146
+ verboseLines.push(rel);
147
+ }
148
+ }
76
149
  };
150
+ walk(p, f);
77
151
  }
78
152
  }
79
- shell.writeFileAsUser(authUser, archivePath, JSON.stringify(entries));
80
- return { exitCode: 0 };
153
+ const tarBuf = buildTar(entries);
154
+ const finalBuf = autoGzip ? Buffer.from(gzipSync(tarBuf)) : tarBuf;
155
+ shell.vfs.writeFile(archivePath, finalBuf);
156
+ return { stdout: verbose ? verboseLines.join("\n") : undefined, exitCode: 0 };
81
157
  }
82
158
  if (list || extract) {
83
- let entries;
84
- try {
85
- entries = JSON.parse(shell.vfs.readFile(archivePath));
159
+ const rawArchive = shell.vfs.readFileRaw(archivePath);
160
+ let raw;
161
+ if (autoGzip) {
162
+ try {
163
+ raw = Buffer.from(gunzipSync(rawArchive));
164
+ }
165
+ catch {
166
+ return { stderr: `tar: ${archiveName}: not a gzip file`, exitCode: 1 };
167
+ }
168
+ }
169
+ else {
170
+ raw = rawArchive;
86
171
  }
87
- catch {
88
- return {
89
- stderr: `tar: ${archiveName}: cannot open archive`,
90
- exitCode: 1,
91
- };
172
+ const files = parseTar(raw);
173
+ if (list) {
174
+ const names = files.map((f) => (verbose ? `-rw-r--r-- 0/0 ${f.content.length.toString().padStart(8)} 1970-01-01 00:00 ${f.name}` : f.name));
175
+ return { stdout: names.join("\n"), exitCode: 0 };
92
176
  }
93
- if (list)
94
- return { stdout: Object.keys(entries).join("\n"), exitCode: 0 };
95
- for (const [name, content] of Object.entries(entries)) {
96
- shell.writeFileAsUser(authUser, resolvePath(cwd, name), content);
177
+ const verboseLines = [];
178
+ for (const { name, content } of files) {
179
+ const destPath = resolvePath(cwd, name);
180
+ shell.writeFileAsUser(authUser, destPath, content);
181
+ if (verbose)
182
+ verboseLines.push(name);
97
183
  }
98
- return { exitCode: 0 };
184
+ return { stdout: verbose ? verboseLines.join("\n") : undefined, exitCode: 0 };
99
185
  }
100
186
  return { stderr: "tar: must specify -c, -x, or -t", exitCode: 1 };
101
187
  },
@@ -0,0 +1,11 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * Create ZIP archives using real PKZIP format with DEFLATE compression.
4
+ * @category archive
5
+ */
6
+ export declare const zipCommand: ShellModule;
7
+ /**
8
+ * Extract ZIP archives (real PKZIP DEFLATE format).
9
+ * @category archive
10
+ */
11
+ export declare const unzipCommand: ShellModule;