typescript-virtual-container 1.5.7 → 1.5.9

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 (37) hide show
  1. package/README.md +39 -29
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/SSHMimic/executor.js +9 -0
  4. package/dist/SSHMimic/prompt.d.ts +2 -1
  5. package/dist/SSHMimic/prompt.js +28 -6
  6. package/dist/SSHMimic/sftp.d.ts +1 -1
  7. package/dist/SSHMimic/sftp.js +1 -1
  8. package/dist/VirtualShell/index.d.ts +2 -3
  9. package/dist/VirtualShell/index.js +2 -3
  10. package/dist/VirtualShell/shell.js +108 -134
  11. package/dist/VirtualShell/shellParser.js +35 -3
  12. package/dist/commands/coreutils.d.ts +55 -0
  13. package/dist/commands/coreutils.js +275 -0
  14. package/dist/commands/id.js +8 -1
  15. package/dist/commands/index.d.ts +1 -1
  16. package/dist/commands/index.js +1 -1
  17. package/dist/commands/manuals-bundle.js +237 -1
  18. package/dist/commands/pacman.d.ts +8 -0
  19. package/dist/commands/pacman.js +15 -0
  20. package/dist/commands/registry.js +13 -0
  21. package/dist/commands/rm.d.ts +1 -1
  22. package/dist/commands/rm.js +48 -11
  23. package/dist/commands/runtime.d.ts +5 -0
  24. package/dist/commands/runtime.js +60 -1
  25. package/dist/commands/sh.js +5 -3
  26. package/dist/modules/linuxRootfs.js +7 -3
  27. package/dist/modules/nanoEditor.d.ts +92 -0
  28. package/dist/modules/nanoEditor.js +974 -0
  29. package/dist/modules/pacmanGame.d.ts +59 -0
  30. package/dist/modules/pacmanGame.js +655 -0
  31. package/dist/modules/webTermRenderer.d.ts +50 -0
  32. package/dist/modules/webTermRenderer.js +425 -0
  33. package/dist/types/commands.d.ts +2 -0
  34. package/dist/types/pipeline.d.ts +2 -0
  35. package/dist/utils/shellSession.d.ts +10 -0
  36. package/dist/utils/shellSession.js +56 -0
  37. package/package.json +2 -2
@@ -78,9 +78,8 @@ export interface VirtualShellVfsOptions {
78
78
  * const result = await client.exec("uname -a");
79
79
  * ```
80
80
  *
81
- * @fires VirtualShell#initialized Emitted once the VFS and users are ready.
82
- * @fires VirtualShell#command Emitted after every command execution.
83
- * @fires VirtualShell#session:start Emitted when an interactive session opens.
81
+ * **Events:** `initialized` (VFS and users ready), `command` (after each execution),
82
+ * `session:start` (interactive session opened).
84
83
  */
85
84
  declare class VirtualShell extends EventEmitter {
86
85
  /** Backing virtual filesystem — use for direct path operations. */
@@ -60,9 +60,8 @@ function resolveAutoSudoForNewUsers() {
60
60
  * const result = await client.exec("uname -a");
61
61
  * ```
62
62
  *
63
- * @fires VirtualShell#initialized Emitted once the VFS and users are ready.
64
- * @fires VirtualShell#command Emitted after every command execution.
65
- * @fires VirtualShell#session:start Emitted when an interactive session opens.
63
+ * **Events:** `initialized` (VFS and users ready), `command` (after each execution),
64
+ * `session:start` (interactive session opened).
66
65
  */
67
66
  class VirtualShell extends EventEmitter {
68
67
  /** Backing virtual filesystem — use for direct path operations. */
@@ -1,10 +1,12 @@
1
- import { readFile, unlink, writeFile } from "node:fs/promises";
2
1
  import * as path from "node:path";
3
- import { getCommandNames, makeDefaultEnv, runCommand, userHome } from "../commands";
4
- import { spawnHtopProcess, spawnNanoEditorProcess, } from "../modules/shellInteractive";
5
- import { getVisibleHtopPidList, resolvePath, toTtyLines, } from "../modules/shellRuntime";
2
+ import { applyUserSwitch, getCommandNames, makeDefaultEnv, runCommand, userHome } from "../commands";
3
+ import { NanoEditor } from "../modules/nanoEditor";
4
+ import { PacmanGame } from "../modules/pacmanGame";
5
+ import { spawnHtopProcess, } from "../modules/shellInteractive";
6
+ import { getVisibleHtopPidList, toTtyLines, } from "../modules/shellRuntime";
6
7
  import { buildLoginBanner } from "../SSHMimic/loginBanner";
7
8
  import { buildPrompt } from "../SSHMimic/prompt";
9
+ import { listPathCompletions, loadHistory, readLastLogin, saveHistory, writeLastLogin } from "../utils/shellSession";
8
10
  export function startShell(properties, stream, authUser, hostname, sessionId, remoteAddress = "unknown", terminalSize = { cols: 80, rows: 24 }, shell) {
9
11
  let lineBuffer = "";
10
12
  let cursorPos = 0;
@@ -18,43 +20,48 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
18
20
  let nanoSession = null;
19
21
  let pendingSudo = null;
20
22
  const buildCurrentPrompt = () => {
23
+ if (shellEnv.vars.PS1)
24
+ return buildPrompt(authUser, hostname, "", shellEnv.vars.PS1, cwd);
21
25
  const homePath = userHome(authUser);
22
26
  const cwdLabel = cwd === homePath ? "~" : path.posix.basename(cwd) || "/";
23
27
  return buildPrompt(authUser, hostname, cwdLabel);
24
28
  };
25
29
  const commandNames = Array.from(new Set(getCommandNames())).sort();
26
30
  console.log(`[${sessionId}] Shell started for user '${authUser}' at ${remoteAddress}`);
27
- // Source login/rc files at startup
28
- void (async () => {
29
- const sourceFile = async (filePath, isEnvFile = false) => {
30
- if (!shell.vfs.exists(filePath))
31
- return;
32
- try {
33
- const content = shell.vfs.readFile(filePath);
34
- for (const line of content.split("\n")) {
35
- const l = line.trim();
36
- if (!l || l.startsWith("#"))
37
- continue;
38
- if (isEnvFile) {
39
- // /etc/environment: KEY=VALUE pairs only, no shell syntax
40
- const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=["']?(.+?)["']?\s*$/);
41
- if (m)
42
- shellEnv.vars[m[1]] = m[2];
43
- }
44
- else {
45
- await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
46
- }
31
+ // Source login/rc files before first prompt.
32
+ let loginReady = false;
33
+ const sourceFile = async (filePath, isEnvFile = false) => {
34
+ if (!shell.vfs.exists(filePath))
35
+ return;
36
+ try {
37
+ const content = shell.vfs.readFile(filePath);
38
+ for (const line of content.split("\n")) {
39
+ const l = line.trim();
40
+ if (!l || l.startsWith("#"))
41
+ continue;
42
+ if (isEnvFile) {
43
+ const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=["']?(.+?)["']?\s*$/);
44
+ if (m)
45
+ shellEnv.vars[m[1]] = m[2];
46
+ }
47
+ else {
48
+ const r = await runCommand(l, authUser, hostname, "shell", cwd, shell, undefined, shellEnv);
49
+ if (r.stdout)
50
+ stream.write(r.stdout.replace(/\n/g, "\r\n"));
47
51
  }
48
52
  }
49
- catch { /* ignore */ }
50
- };
53
+ }
54
+ catch { /* ignore */ }
55
+ };
56
+ const loginPromise = (async () => {
51
57
  await sourceFile("/etc/environment", true);
52
58
  await sourceFile(`${userHome(authUser)}/.profile`);
53
59
  await sourceFile(`${userHome(authUser)}/.bashrc`);
60
+ loginReady = true;
54
61
  })();
55
62
  function renderLine() {
56
63
  const prompt = buildCurrentPrompt();
57
- stream.write(`\r${prompt}${lineBuffer}\u001b[K`);
64
+ stream.write(`\r\x1b[0m${prompt}${lineBuffer}\u001b[K`);
58
65
  const moveLeft = lineBuffer.length - cursorPos;
59
66
  if (moveLeft > 0) {
60
67
  stream.write(`\u001b[${moveLeft}D`);
@@ -88,6 +95,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
88
95
  cwd = userHome(authUser);
89
96
  }
90
97
  shell.users.updateSession(sessionId, authUser, remoteAddress);
98
+ await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
91
99
  stream.write("\r\n");
92
100
  renderLine();
93
101
  return;
@@ -103,6 +111,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
103
111
  await startHtop();
104
112
  return;
105
113
  }
114
+ if (result.openPacman) {
115
+ startPacman();
116
+ return;
117
+ }
106
118
  if (result.clearScreen) {
107
119
  stream.write("\u001b[2J\u001b[H");
108
120
  }
@@ -117,6 +129,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
117
129
  authUser = result.switchUser;
118
130
  cwd = result.nextCwd ?? userHome(authUser);
119
131
  shell.users.updateSession(sessionId, authUser, remoteAddress);
132
+ await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
120
133
  }
121
134
  else if (result.nextCwd) {
122
135
  cwd = result.nextCwd;
@@ -124,46 +137,34 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
124
137
  // WAL: checkpoint handled by auto-flush timer
125
138
  renderLine();
126
139
  }
127
- async function finishNanoEditor() {
128
- if (!nanoSession) {
129
- return;
130
- }
131
- const activeSession = nanoSession;
132
- if (activeSession.kind === "nano") {
133
- try {
134
- const updatedContent = await readFile(activeSession.tempPath, "utf8");
135
- shell.writeFileAsUser(authUser, activeSession.targetPath, updatedContent);
136
- // WAL: checkpoint handled by auto-flush timer
137
- }
138
- catch {
139
- // If temp file does not exist, nano exited without writing.
140
- }
141
- await unlink(activeSession.tempPath).catch(() => undefined);
140
+ function finishInteractiveSession(savedContent, targetPath) {
141
+ if (savedContent !== undefined && targetPath) {
142
+ shell.writeFileAsUser(authUser, targetPath, savedContent);
142
143
  }
143
144
  nanoSession = null;
144
145
  lineBuffer = "";
145
146
  cursorPos = 0;
146
- stream.write("\r\n");
147
+ // Clear screen + reset SGR so nano residue is gone before next prompt
148
+ stream.write("\x1b[2J\x1b[H\x1b[0m");
147
149
  renderLine();
148
150
  }
149
- async function startNanoEditor(targetPath, initialContent, tempPath) {
150
- if (shell.vfs.exists(targetPath)) {
151
- await writeFile(tempPath, initialContent, "utf8");
152
- }
153
- const editor = spawnNanoEditorProcess(tempPath, terminalSize, stream);
154
- editor.on("error", (error) => {
155
- stream.write(`nano: ${error.message}\r\n`);
156
- void finishNanoEditor();
157
- });
158
- editor.on("close", () => {
159
- void finishNanoEditor();
151
+ function startNanoEditor(targetPath, initialContent, _tempPath) {
152
+ const editor = new NanoEditor({
153
+ stream,
154
+ terminalSize,
155
+ content: initialContent,
156
+ filename: path.posix.basename(targetPath),
157
+ onExit: (reason, content) => {
158
+ if (reason === "saved") {
159
+ finishInteractiveSession(content, targetPath);
160
+ }
161
+ else {
162
+ finishInteractiveSession();
163
+ }
164
+ },
160
165
  });
161
- nanoSession = {
162
- kind: "nano",
163
- targetPath,
164
- tempPath,
165
- process: editor,
166
- };
166
+ nanoSession = { kind: "nano", targetPath, editor };
167
+ editor.start();
167
168
  }
168
169
  async function startHtop() {
169
170
  const pidList = await getVisibleHtopPidList();
@@ -174,17 +175,27 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
174
175
  const monitor = spawnHtopProcess(pidList, terminalSize, stream);
175
176
  monitor.on("error", (error) => {
176
177
  stream.write(`htop: ${error.message}\r\n`);
177
- void finishNanoEditor();
178
+ finishInteractiveSession();
178
179
  });
179
180
  monitor.on("close", () => {
180
- void finishNanoEditor();
181
+ finishInteractiveSession();
181
182
  });
182
- nanoSession = {
183
- kind: "htop",
184
- targetPath: "",
185
- tempPath: "",
186
- process: monitor,
187
- };
183
+ nanoSession = { kind: "htop", process: monitor };
184
+ }
185
+ function startPacman() {
186
+ const game = new PacmanGame({
187
+ stream,
188
+ terminalSize,
189
+ onExit: () => {
190
+ nanoSession = null;
191
+ lineBuffer = "";
192
+ cursorPos = 0;
193
+ stream.write("\x1b[2J\x1b[H\x1b[0m");
194
+ renderLine();
195
+ },
196
+ });
197
+ nanoSession = { kind: "pacman", game };
198
+ game.start();
188
199
  }
189
200
  function applyHistoryLine(nextLine) {
190
201
  lineBuffer = nextLine;
@@ -207,28 +218,6 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
207
218
  }
208
219
  return { start, end };
209
220
  }
210
- function listPathCompletions(prefix) {
211
- const slashIndex = prefix.lastIndexOf("/");
212
- const dirPart = slashIndex >= 0 ? prefix.slice(0, slashIndex + 1) : "";
213
- const namePart = slashIndex >= 0 ? prefix.slice(slashIndex + 1) : prefix;
214
- const basePath = resolvePath(cwd, dirPart || ".");
215
- try {
216
- return shell.vfs
217
- .list(basePath)
218
- .filter((entry) => !entry.startsWith("."))
219
- .filter((entry) => entry.startsWith(namePart))
220
- .map((entry) => {
221
- const fullPath = path.posix.join(basePath, entry);
222
- const st = shell.vfs.stat(fullPath);
223
- const suffix = st.type === "directory" ? "/" : "";
224
- return `${dirPart}${entry}${suffix}`;
225
- })
226
- .sort();
227
- }
228
- catch {
229
- return [];
230
- }
231
- }
232
221
  function handleTabCompletion() {
233
222
  const { start, end } = getTokenRange(lineBuffer, cursorPos);
234
223
  const token = lineBuffer.slice(start, cursorPos);
@@ -239,7 +228,7 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
239
228
  const commandCandidates = firstToken
240
229
  ? commandNames.filter((name) => name.startsWith(token))
241
230
  : [];
242
- const pathCandidates = listPathCompletions(token);
231
+ const pathCandidates = listPathCompletions(shell.vfs, cwd, token);
243
232
  const candidates = Array.from(new Set([...commandCandidates, ...pathCandidates])).sort();
244
233
  if (candidates.length === 0) {
245
234
  return;
@@ -257,43 +246,33 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
257
246
  renderLine();
258
247
  }
259
248
  function pushHistory(cmd) {
260
- if (cmd.length === 0) {
249
+ if (cmd.length === 0)
261
250
  return;
262
- }
263
251
  history.push(cmd);
264
- if (history.length > 500) {
252
+ if (history.length > 500)
265
253
  history = history.slice(history.length - 500);
266
- }
267
- const data = history.length > 0 ? `${history.join("\n")}\n` : "";
268
- shell.vfs.writeFile(`${userHome(authUser)}/.bash_history`, data);
269
- }
270
- function readLastLogin() {
271
- const lastlogPath = `${userHome(authUser)}/.lastlog.json`;
272
- if (!shell.vfs.exists(lastlogPath)) {
273
- return null;
274
- }
275
- try {
276
- return JSON.parse(shell.vfs.readFile(lastlogPath));
277
- }
278
- catch {
279
- return null;
280
- }
281
- }
282
- function writeLastLogin(nowIso) {
283
- const lastlogPath = `${userHome(authUser)}/.lastlog`;
284
- shell.vfs.writeFile(lastlogPath, JSON.stringify({ at: nowIso, from: remoteAddress }));
254
+ saveHistory(shell.vfs, authUser, history);
285
255
  }
286
256
  function renderLoginBanner() {
287
- const last = readLastLogin();
288
- const nowIso = new Date().toISOString();
257
+ const last = readLastLogin(shell.vfs, authUser);
289
258
  stream.write(buildLoginBanner(hostname, properties, last));
290
- writeLastLogin(nowIso);
259
+ writeLastLogin(shell.vfs, authUser, remoteAddress);
291
260
  }
292
261
  renderLoginBanner();
293
- renderLine();
262
+ void loginPromise.then(() => renderLine());
294
263
  stream.on("data", async (chunk) => {
264
+ if (!loginReady)
265
+ return;
295
266
  if (nanoSession) {
296
- nanoSession.process.stdin.write(chunk);
267
+ if (nanoSession.kind === "nano") {
268
+ nanoSession.editor.handleInput(chunk);
269
+ }
270
+ else if (nanoSession.kind === "pacman") {
271
+ nanoSession.game.handleInput(chunk);
272
+ }
273
+ else {
274
+ nanoSession.process.stdin.write(chunk);
275
+ }
297
276
  return;
298
277
  }
299
278
  if (pendingHeredoc) {
@@ -596,6 +575,10 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
596
575
  await startHtop();
597
576
  return;
598
577
  }
578
+ if (result.openPacman) {
579
+ startPacman();
580
+ return;
581
+ }
599
582
  if (result.sudoChallenge) {
600
583
  startSudoPrompt(result.sudoChallenge);
601
584
  return;
@@ -634,11 +617,9 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
634
617
  sessionStack.push({ authUser, cwd });
635
618
  authUser = result.switchUser;
636
619
  cwd = result.nextCwd ?? userHome(authUser);
637
- shellEnv.vars.USER = authUser;
638
- shellEnv.vars.LOGNAME = authUser;
639
- shellEnv.vars.HOME = userHome(authUser);
640
620
  shellEnv.vars.PWD = cwd;
641
621
  shell.users.updateSession(sessionId, authUser, remoteAddress);
622
+ await applyUserSwitch(authUser, hostname, cwd, shellEnv, shell);
642
623
  lineBuffer = "";
643
624
  cursorPos = 0;
644
625
  }
@@ -660,20 +641,13 @@ export function startShell(properties, stream, authUser, hostname, sessionId, re
660
641
  });
661
642
  stream.on("close", () => {
662
643
  if (nanoSession) {
663
- nanoSession.process.kill("SIGTERM");
644
+ if (nanoSession.kind === "htop") {
645
+ nanoSession.process.kill("SIGTERM");
646
+ }
647
+ else if (nanoSession.kind === "pacman") {
648
+ nanoSession.game.stop();
649
+ }
664
650
  nanoSession = null;
665
651
  }
666
652
  });
667
653
  }
668
- function loadHistory(vfs, authUser) {
669
- const historyPath = `${userHome(authUser)}/.bash_history`;
670
- if (!vfs.exists(historyPath)) {
671
- vfs.writeFile(historyPath, "");
672
- return [];
673
- }
674
- const raw = vfs.readFile(historyPath);
675
- return raw
676
- .split("\n")
677
- .map((line) => line.trim())
678
- .filter((line) => line.length > 0);
679
- }
@@ -45,7 +45,7 @@ export function expandGlob(pattern, entries) {
45
45
  }
46
46
  // ── Internal parser ───────────────────────────────────────────────────────────
47
47
  function parseStatements(input) {
48
- // Split by ;, &&, || — respecting quotes and parens
48
+ // Split by ;, &&, ||, & — respecting quotes and parens
49
49
  const segments = splitByLogicalOps(input);
50
50
  const statements = [];
51
51
  for (const seg of segments) {
@@ -53,6 +53,8 @@ function parseStatements(input) {
53
53
  const stmt = { pipeline: { commands, isValid: true } };
54
54
  if (seg.op)
55
55
  stmt.op = seg.op;
56
+ if (seg.background)
57
+ stmt.background = true;
56
58
  statements.push(stmt);
57
59
  }
58
60
  return statements;
@@ -64,9 +66,9 @@ function splitByLogicalOps(input) {
64
66
  let inQ = false;
65
67
  let qChar = "";
66
68
  let i = 0;
67
- const flush = (op) => {
69
+ const flush = (op, background) => {
68
70
  if (current.trim())
69
- segments.push({ text: current, op });
71
+ segments.push({ text: current, op, background });
70
72
  current = "";
71
73
  };
72
74
  while (i < input.length) {
@@ -117,6 +119,25 @@ function splitByLogicalOps(input) {
117
119
  i += 2;
118
120
  continue;
119
121
  }
122
+ if (ch === "&" && input[i + 1] !== "&") {
123
+ // &> redirect (stdout+stderr) — keep in current segment, not a background op
124
+ if (input[i + 1] === ">") {
125
+ current += ch;
126
+ i++;
127
+ continue;
128
+ }
129
+ // 2>&1 — the & is part of a redirection target, not a background op
130
+ const trimmed = current.trimEnd();
131
+ if (trimmed.endsWith(">") || trimmed.endsWith("2>") || trimmed.endsWith(">>")) {
132
+ current += ch;
133
+ i++;
134
+ continue;
135
+ }
136
+ // trailing & → background job; treat like ; for sequencing
137
+ flush(";", true);
138
+ i++;
139
+ continue;
140
+ }
120
141
  if (ch === ";") {
121
142
  flush(";");
122
143
  i++;
@@ -209,6 +230,17 @@ function parseCommandWithRedirections(token) {
209
230
  appendOutput = false;
210
231
  i++;
211
232
  }
233
+ else if (part === "&>" || part === "&>>") {
234
+ // &> file — redirect both stdout and stderr to file
235
+ const append = part === "&>>";
236
+ i++;
237
+ if (i >= parts.length)
238
+ throw new Error(`Syntax error: expected filename after ${part}`);
239
+ outputFile = parts[i];
240
+ appendOutput = append;
241
+ stderrToStdout = true;
242
+ i++;
243
+ }
212
244
  else if (part === "2>&1") {
213
245
  stderrToStdout = true;
214
246
  i++;
@@ -0,0 +1,55 @@
1
+ import type { ShellModule } from "../types/commands";
2
+ /**
3
+ * timeout — run command with time limit (simulated: just runs the command)
4
+ * @category shell
5
+ * @params ["<duration> <command> [args...]"]
6
+ */
7
+ export declare const timeoutCommand: ShellModule;
8
+ /**
9
+ * mktemp — create a temporary file or directory
10
+ * @category shell
11
+ * @params ["[TEMPLATE]"]
12
+ */
13
+ export declare const mktempCommand: ShellModule;
14
+ /**
15
+ * nproc — print number of processing units
16
+ * @category system
17
+ * @params ["[--all]"]
18
+ */
19
+ export declare const nprocCommand: ShellModule;
20
+ /**
21
+ * wait — wait for background jobs (no-op: background jobs are fire-and-forget)
22
+ * @category shell
23
+ * @params ["[job_id...]"]
24
+ */
25
+ export declare const waitCommand: ShellModule;
26
+ /**
27
+ * shuf — shuffle lines of input
28
+ * @category text
29
+ * @params ["[-n count] [-i lo-hi] [file]"]
30
+ */
31
+ export declare const shufCommand: ShellModule;
32
+ /**
33
+ * paste — merge lines of files side by side
34
+ * @category text
35
+ * @params ["[-d delimiter] file..."]
36
+ */
37
+ export declare const pasteCommand: ShellModule;
38
+ /**
39
+ * tac — concatenate files in reverse (line order)
40
+ * @category text
41
+ * @params ["[file...]"]
42
+ */
43
+ export declare const tacCommand: ShellModule;
44
+ /**
45
+ * nl — number lines of files
46
+ * @category text
47
+ * @params ["[file]"]
48
+ */
49
+ export declare const nlCommand: ShellModule;
50
+ /**
51
+ * column — columnate lists
52
+ * @category text
53
+ * @params ["[-t] [-s sep] [file]"]
54
+ */
55
+ export declare const columnCommand: ShellModule;