typescript-virtual-container 1.0.4 → 1.0.5
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.
- package/CHANGELOG.md +11 -0
- package/README.md +50 -0
- package/package.json +1 -1
- package/src/SSHMimic/client.ts +1 -1
- package/src/SSHMimic/exec.ts +1 -1
- package/src/SSHMimic/executor.ts +2 -2
- package/src/SSHMimic/index.ts +14 -18
- package/src/{VirtualFileSystem.ts → VirtualFileSystem/index.ts} +6 -15
- package/src/{SSHMimic → VirtualShell}/commands/cat.ts +2 -1
- package/src/VirtualShell/commands/command-helpers.ts +135 -0
- package/src/{SSHMimic → VirtualShell}/commands/curl.ts +16 -37
- package/src/{SSHMimic → VirtualShell}/commands/echo.ts +10 -2
- package/src/{SSHMimic → VirtualShell}/commands/export.ts +7 -1
- package/src/{SSHMimic → VirtualShell}/commands/grep.ts +15 -8
- package/src/{SSHMimic → VirtualShell}/commands/helpers.ts +0 -37
- package/src/{SSHMimic → VirtualShell}/commands/index.ts +63 -8
- package/src/{SSHMimic → VirtualShell}/commands/ls.ts +3 -2
- package/src/{SSHMimic → VirtualShell}/commands/mkdir.ts +6 -1
- package/src/{SSHMimic → VirtualShell}/commands/rm.ts +10 -3
- package/src/{SSHMimic → VirtualShell}/commands/set.ts +7 -1
- package/src/VirtualShell/commands/sh.ts +58 -0
- package/src/{SSHMimic → VirtualShell}/commands/su.ts +3 -3
- package/src/{SSHMimic → VirtualShell}/commands/sudo.ts +16 -26
- package/src/{SSHMimic → VirtualShell}/commands/tree.ts +2 -1
- package/src/{SSHMimic → VirtualShell}/commands/wget.ts +23 -6
- package/src/{SSHMimic → VirtualShell}/commands/who.ts +1 -1
- package/src/VirtualShell/index.ts +69 -0
- package/src/{SSHMimic → VirtualShell}/shell.ts +3 -3
- package/src/index.ts +8 -0
- package/src/standalone.ts +10 -1
- package/tests/command-helpers.test.ts +40 -0
- package/tests/helpers.test.ts +1 -1
- package/src/SSHMimic/commands/sh.ts +0 -121
- /package/src/{vfs → VirtualFileSystem}/archive.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/internalTypes.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/path.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/snapshot.ts +0 -0
- /package/src/{vfs → VirtualFileSystem}/tree.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/adduser.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/cd.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/clear.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/deluser.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/env.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/exit.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/help.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/hostname.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/htop.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/nano.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/pwd.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/touch.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/unset.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/commands/whoami.ts +0 -0
- /package/src/{SSHMimic → VirtualShell}/shellParser.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ The format is based on Keep a Changelog.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.0.5] - 2026-04-15
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Refactored commands to use shared argument/flag parsing helpers.
|
|
14
|
+
- Improved maintainability and consistency of argument parsing across commands.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Verified all refactored commands pass existing test cases without regressions.
|
|
19
|
+
|
|
9
20
|
## [1.0.4] - 2026-04-15
|
|
10
21
|
|
|
11
22
|
### Added
|
package/README.md
CHANGED
|
@@ -417,6 +417,56 @@ console.log(client.getUsername()); // Username from constructor
|
|
|
417
417
|
|
|
418
418
|
---
|
|
419
419
|
|
|
420
|
+
### VirtualShell
|
|
421
|
+
|
|
422
|
+
Encapsulates shell execution primitives used by the SSH runtime for command dispatch and interactive sessions.
|
|
423
|
+
|
|
424
|
+
#### Constructor
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
new VirtualShell(
|
|
428
|
+
vfs: VirtualFileSystem,
|
|
429
|
+
users: VirtualUserManager,
|
|
430
|
+
hostname: string,
|
|
431
|
+
)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
- **vfs**: Virtual filesystem instance used by shell commands.
|
|
435
|
+
- **users**: User manager for authentication/session-aware command behavior.
|
|
436
|
+
- **hostname**: Hostname injected into command context and prompt behavior.
|
|
437
|
+
|
|
438
|
+
**Example:**
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
const shell = new VirtualShell(vfs, users, "typescript-vm");
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### Methods
|
|
445
|
+
|
|
446
|
+
##### `executeCommand(rawInput: string, authUser: string, cwd: string): void`
|
|
447
|
+
|
|
448
|
+
Runs one command input in shell mode for a given user and working directory.
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
shell.executeCommand("ls -la", "root", "/home/root");
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
##### `startInteractiveSession(stream: ShellStream, authUser: string, sessionId: string | null, remoteAddress: string, terminalSize: { cols: number; rows: number }): void`
|
|
455
|
+
|
|
456
|
+
Starts an interactive shell session over a shell stream.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
shell.startInteractiveSession(
|
|
460
|
+
stream,
|
|
461
|
+
"root",
|
|
462
|
+
sessionId,
|
|
463
|
+
"127.0.0.1",
|
|
464
|
+
{ cols: 120, rows: 30 },
|
|
465
|
+
);
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
420
470
|
### VirtualFileSystem
|
|
421
471
|
|
|
422
472
|
In-memory filesystem with optional gzip compression and tar.gz persistence.
|
package/package.json
CHANGED
package/src/SSHMimic/client.ts
CHANGED
package/src/SSHMimic/exec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExecStream } from "../types/streams";
|
|
2
2
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
3
|
-
import { runCommand } from "
|
|
3
|
+
import { runCommand } from "../VirtualShell/commands";
|
|
4
4
|
import type { VirtualUserManager } from "./users";
|
|
5
5
|
|
|
6
6
|
function toTtyLines(text: string): string {
|
package/src/SSHMimic/executor.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CommandMode, CommandResult } from "../types/commands";
|
|
2
2
|
import type { Pipeline, PipelineCommand } from "../types/pipeline";
|
|
3
3
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
4
|
-
import { runCommand as runSingleCommand } from "
|
|
5
|
-
import { resolvePath } from "
|
|
4
|
+
import { runCommand as runSingleCommand } from "../VirtualShell/commands";
|
|
5
|
+
import { resolvePath } from "../VirtualShell/commands/helpers";
|
|
6
6
|
import type { VirtualUserManager } from "./users";
|
|
7
7
|
|
|
8
8
|
/**
|
package/src/SSHMimic/index.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { Server as SshServer } from "ssh2";
|
|
3
3
|
import VirtualFileSystem from "../VirtualFileSystem";
|
|
4
|
-
import {
|
|
4
|
+
import { VirtualShell } from "../VirtualShell";
|
|
5
5
|
import { loadOrCreateHostKey } from "./hostKey";
|
|
6
|
-
import { startShell } from "./shell";
|
|
7
6
|
import { VirtualUserManager } from "./users";
|
|
8
7
|
|
|
9
8
|
function resolveRootPassword(): string {
|
|
@@ -35,12 +34,13 @@ function resolveAutoSudoForNewUsers(): boolean {
|
|
|
35
34
|
* {@link SshMimic.stop} when your process exits.
|
|
36
35
|
*/
|
|
37
36
|
class SshMimic {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
port: number;
|
|
38
|
+
hostname: string;
|
|
39
|
+
server: SshServer | null;
|
|
40
|
+
vfs: VirtualFileSystem | null = null;
|
|
41
|
+
users: VirtualUserManager | null = null;
|
|
42
|
+
shell: VirtualShell | null = null;
|
|
43
|
+
basePath: string = ".";
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Creates a new SSH mimic server instance.
|
|
@@ -80,6 +80,8 @@ class SshMimic {
|
|
|
80
80
|
);
|
|
81
81
|
await this.users.initialize();
|
|
82
82
|
|
|
83
|
+
this.shell = new VirtualShell(this.vfs, this.users, this.hostname);
|
|
84
|
+
|
|
83
85
|
this.server = new SshServer(
|
|
84
86
|
{
|
|
85
87
|
hostKeys: [privateKey],
|
|
@@ -148,12 +150,9 @@ class SshMimic {
|
|
|
148
150
|
|
|
149
151
|
session.on("shell", (acceptShell) => {
|
|
150
152
|
const stream = acceptShell();
|
|
151
|
-
|
|
153
|
+
this.shell?.startInteractiveSession(
|
|
152
154
|
stream,
|
|
153
155
|
authUser,
|
|
154
|
-
this.vfs!,
|
|
155
|
-
this.hostname,
|
|
156
|
-
this.users!,
|
|
157
156
|
sessionId,
|
|
158
157
|
remoteAddress,
|
|
159
158
|
terminalSize,
|
|
@@ -161,14 +160,11 @@ class SshMimic {
|
|
|
161
160
|
});
|
|
162
161
|
|
|
163
162
|
session.on("exec", (acceptExec, _rejectExec, info) => {
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
stream,
|
|
163
|
+
const _stream = acceptExec();
|
|
164
|
+
this.shell?.executeCommand(
|
|
167
165
|
info.command.trim(),
|
|
168
166
|
authUser,
|
|
169
|
-
|
|
170
|
-
this.users!,
|
|
171
|
-
this.vfs!,
|
|
167
|
+
`/home/${authUser}`,
|
|
172
168
|
);
|
|
173
169
|
});
|
|
174
170
|
});
|
|
@@ -5,21 +5,12 @@ import type {
|
|
|
5
5
|
RemoveOptions,
|
|
6
6
|
VfsNodeStats,
|
|
7
7
|
WriteFileOptions,
|
|
8
|
-
} from "
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./
|
|
14
|
-
import type { InternalDirectoryNode, InternalNode } from "./vfs/internalTypes";
|
|
15
|
-
import {
|
|
16
|
-
getNode,
|
|
17
|
-
getParentDirectory,
|
|
18
|
-
normalizePath,
|
|
19
|
-
splitPath,
|
|
20
|
-
} from "./vfs/path";
|
|
21
|
-
import { applySnapshot, createSnapshot } from "./vfs/snapshot";
|
|
22
|
-
import { renderTree } from "./vfs/tree";
|
|
8
|
+
} from "../types/vfs";
|
|
9
|
+
import { archiveExists, createTarBuffer, readSnapshotFromTar } from "./archive";
|
|
10
|
+
import type { InternalDirectoryNode, InternalNode } from "./internalTypes";
|
|
11
|
+
import { getNode, getParentDirectory, normalizePath, splitPath } from "./path";
|
|
12
|
+
import { applySnapshot, createSnapshot } from "./snapshot";
|
|
13
|
+
import { renderTree } from "./tree";
|
|
23
14
|
|
|
24
15
|
/**
|
|
25
16
|
* In-memory virtual filesystem with tar.gz mirror persistence.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg } from "./command-helpers";
|
|
2
3
|
import { assertPathAccess, resolveReadablePath } from "./helpers";
|
|
3
4
|
|
|
4
5
|
export const catCommand: ShellModule = {
|
|
5
6
|
name: "cat",
|
|
6
7
|
params: ["<file>"],
|
|
7
8
|
run: ({ authUser, vfs, cwd, args }) => {
|
|
8
|
-
const fileArg = args
|
|
9
|
+
const fileArg = getArg(args, 0);
|
|
9
10
|
if (!fileArg) {
|
|
10
11
|
return { stderr: "cat: missing file operand", exitCode: 1 };
|
|
11
12
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
type ArgParseOptions = {
|
|
2
|
+
flags?: string[];
|
|
3
|
+
flagsWithValue?: string[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
function toFlagList(flags: string | string[]): string[] {
|
|
7
|
+
return Array.isArray(flags) ? flags : [flags];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function matchFlagToken(
|
|
11
|
+
token: string,
|
|
12
|
+
flag: string,
|
|
13
|
+
): { matched: boolean; inlineValue: string | null } {
|
|
14
|
+
if (token === flag) {
|
|
15
|
+
return { matched: true, inlineValue: null };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const prefix = `${flag}=`;
|
|
19
|
+
if (token.startsWith(prefix)) {
|
|
20
|
+
return { matched: true, inlineValue: token.slice(prefix.length) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { matched: false, inlineValue: null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function collectPositionals(
|
|
27
|
+
args: string[],
|
|
28
|
+
options: ArgParseOptions = {},
|
|
29
|
+
): string[] {
|
|
30
|
+
const boolFlags = new Set(options.flags ?? []);
|
|
31
|
+
const valueFlags = new Set(options.flagsWithValue ?? []);
|
|
32
|
+
const positionals: string[] = [];
|
|
33
|
+
let passthrough = false;
|
|
34
|
+
|
|
35
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
36
|
+
const arg = args[index]!;
|
|
37
|
+
|
|
38
|
+
if (passthrough) {
|
|
39
|
+
positionals.push(arg);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (arg === "--") {
|
|
44
|
+
passthrough = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let consumed = false;
|
|
49
|
+
|
|
50
|
+
for (const flag of boolFlags) {
|
|
51
|
+
const { matched } = matchFlagToken(arg, flag);
|
|
52
|
+
if (matched) {
|
|
53
|
+
consumed = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (consumed) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const flag of valueFlags) {
|
|
63
|
+
const match = matchFlagToken(arg, flag);
|
|
64
|
+
if (!match.matched) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
consumed = true;
|
|
69
|
+
if (match.inlineValue === null && index + 1 < args.length) {
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!consumed) {
|
|
76
|
+
positionals.push(arg);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return positionals;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function ifFlag(args: string[], flags: string | string[]): boolean {
|
|
84
|
+
const allFlags = toFlagList(flags);
|
|
85
|
+
|
|
86
|
+
for (const arg of args) {
|
|
87
|
+
for (const flag of allFlags) {
|
|
88
|
+
if (matchFlagToken(arg, flag).matched) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getFlag(
|
|
98
|
+
args: string[],
|
|
99
|
+
flags: string | string[],
|
|
100
|
+
): string | true | undefined {
|
|
101
|
+
const allFlags = toFlagList(flags);
|
|
102
|
+
|
|
103
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
104
|
+
const arg = args[index]!;
|
|
105
|
+
|
|
106
|
+
for (const flag of allFlags) {
|
|
107
|
+
const match = matchFlagToken(arg, flag);
|
|
108
|
+
if (!match.matched) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (match.inlineValue !== null) {
|
|
113
|
+
return match.inlineValue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const next = args[index + 1];
|
|
117
|
+
if (next !== undefined && next !== "--") {
|
|
118
|
+
return next;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getArg(
|
|
129
|
+
args: string[],
|
|
130
|
+
index: number,
|
|
131
|
+
options: ArgParseOptions = {},
|
|
132
|
+
): string | undefined {
|
|
133
|
+
const positionals = collectPositionals(args, options);
|
|
134
|
+
return positionals[index];
|
|
135
|
+
}
|
|
@@ -1,43 +1,12 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import type { ShellModule } from "../../types/commands";
|
|
3
|
+
import { getArg, getFlag, ifFlag } from "./command-helpers";
|
|
3
4
|
import {
|
|
4
5
|
assertPathAccess,
|
|
5
6
|
normalizeTerminalOutput,
|
|
6
7
|
resolvePath,
|
|
7
8
|
} from "./helpers";
|
|
8
9
|
|
|
9
|
-
function parseCurlOutputPath(args: string[]): {
|
|
10
|
-
outputPath: string | null;
|
|
11
|
-
inputArgs: string[];
|
|
12
|
-
} {
|
|
13
|
-
const filtered: string[] = [];
|
|
14
|
-
let outputPath: string | null = null;
|
|
15
|
-
|
|
16
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
17
|
-
const arg = args[index]!;
|
|
18
|
-
|
|
19
|
-
if (arg === "-o" || arg === "--output") {
|
|
20
|
-
outputPath = args[index + 1] ?? null;
|
|
21
|
-
index += 1;
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (arg.startsWith("-o=")) {
|
|
26
|
-
outputPath = arg.slice(3);
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (arg.startsWith("--output=")) {
|
|
31
|
-
outputPath = arg.slice("--output=".length);
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
filtered.push(arg);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { outputPath, inputArgs: filtered };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
10
|
function runHostCurl(args: string[]): Promise<{
|
|
42
11
|
stdout: string;
|
|
43
12
|
stderr: string;
|
|
@@ -110,12 +79,22 @@ export const curlCommand: ShellModule = {
|
|
|
110
79
|
name: "curl",
|
|
111
80
|
params: ["[-o file] <url>"],
|
|
112
81
|
run: async ({ authUser, vfs, cwd, args }) => {
|
|
113
|
-
const
|
|
82
|
+
const outputPathValue = getFlag(args, ["-o", "--output"]);
|
|
83
|
+
const outputPath =
|
|
84
|
+
typeof outputPathValue === "string" && outputPathValue.length > 0
|
|
85
|
+
? outputPathValue
|
|
86
|
+
: null;
|
|
87
|
+
const parserOptions = { flagsWithValue: ["-o", "--output"] };
|
|
88
|
+
const inputArgs: string[] = [];
|
|
89
|
+
for (let index = 0; ; index += 1) {
|
|
90
|
+
const arg = getArg(args, index, parserOptions);
|
|
91
|
+
if (!arg) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
inputArgs.push(arg);
|
|
95
|
+
}
|
|
114
96
|
const url = inputArgs[0];
|
|
115
|
-
const isHelpLike =
|
|
116
|
-
(arg) =>
|
|
117
|
-
arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version",
|
|
118
|
-
);
|
|
97
|
+
const isHelpLike = ifFlag(args, ["-h", "--help", "-V", "--version"]);
|
|
119
98
|
|
|
120
99
|
if (!url) {
|
|
121
100
|
return { stderr: "curl: missing URL", exitCode: 1 };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg, ifFlag } from "./command-helpers";
|
|
2
3
|
import { getAllEnvVars } from "./set";
|
|
3
4
|
|
|
4
5
|
function expandEnvVars(input: string, env: Record<string, string>): string {
|
|
@@ -11,8 +12,15 @@ export const echoCommand: ShellModule = {
|
|
|
11
12
|
name: "echo",
|
|
12
13
|
params: ["[options] [text...]"],
|
|
13
14
|
run: ({ args, authUser, stdin }) => {
|
|
14
|
-
const newline = !args
|
|
15
|
-
const filteredArgs =
|
|
15
|
+
const newline = !ifFlag(args, "-n");
|
|
16
|
+
const filteredArgs: string[] = [];
|
|
17
|
+
for (let index = 0; ; index += 1) {
|
|
18
|
+
const value = getArg(args, index, { flags: ["-n"] });
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
filteredArgs.push(value);
|
|
23
|
+
}
|
|
16
24
|
const env = getAllEnvVars(authUser);
|
|
17
25
|
const rawText =
|
|
18
26
|
filteredArgs.length > 0 ? filteredArgs.join(" ") : (stdin ?? "");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg } from "./command-helpers";
|
|
2
3
|
import { getEnvVar, setEnvVar } from "./set";
|
|
3
4
|
|
|
4
5
|
export const exportCommand: ShellModule = {
|
|
@@ -15,7 +16,12 @@ export const exportCommand: ShellModule = {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
// Parse VAR=value format
|
|
18
|
-
for (
|
|
19
|
+
for (let index = 0; ; index += 1) {
|
|
20
|
+
const arg = getArg(args, index);
|
|
21
|
+
if (!arg) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
if (arg.includes("=")) {
|
|
20
26
|
const [varName, varValue] = arg.split("=", 2);
|
|
21
27
|
if (varName && varValue !== undefined) {
|
|
@@ -1,25 +1,32 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg, ifFlag } from "./command-helpers";
|
|
2
3
|
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
4
|
|
|
4
5
|
export const grepCommand: ShellModule = {
|
|
5
6
|
name: "grep",
|
|
6
7
|
params: ["[-i] [-v] <pattern> [file...]"],
|
|
7
8
|
run: ({ authUser, vfs, cwd, args, stdin }) => {
|
|
8
|
-
const caseInsensitive = args
|
|
9
|
-
const invertMatch = args
|
|
10
|
-
const
|
|
9
|
+
const caseInsensitive = ifFlag(args, "-i");
|
|
10
|
+
const invertMatch = ifFlag(args, "-v");
|
|
11
|
+
const parserOptions = { flags: ["-i", "-v"] };
|
|
12
|
+
const pattern = getArg(args, 0, parserOptions);
|
|
13
|
+
const files: string[] = [];
|
|
14
|
+
for (let index = 1; ; index += 1) {
|
|
15
|
+
const file = getArg(args, index, parserOptions);
|
|
16
|
+
if (!file) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
files.push(file);
|
|
20
|
+
}
|
|
11
21
|
|
|
12
|
-
if (
|
|
22
|
+
if (!pattern) {
|
|
13
23
|
return { stderr: "grep: no pattern specified", exitCode: 1 };
|
|
14
24
|
}
|
|
15
25
|
|
|
16
|
-
const pattern = filteredArgs[0];
|
|
17
|
-
const files = filteredArgs.slice(1);
|
|
18
|
-
|
|
19
26
|
let regex: RegExp;
|
|
20
27
|
try {
|
|
21
28
|
const flags = caseInsensitive ? "gmi" : "gm";
|
|
22
|
-
regex = new RegExp(pattern
|
|
29
|
+
regex = new RegExp(pattern, flags);
|
|
23
30
|
} catch {
|
|
24
31
|
return { stderr: `grep: invalid regex: ${pattern}`, exitCode: 1 };
|
|
25
32
|
}
|
|
@@ -58,43 +58,6 @@ export function assertPathAccess(
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function parseOutputPath(args: string[]): {
|
|
62
|
-
outputPath: string | null;
|
|
63
|
-
inputArgs: string[];
|
|
64
|
-
} {
|
|
65
|
-
const filtered: string[] = [];
|
|
66
|
-
let outputPath: string | null = null;
|
|
67
|
-
|
|
68
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
69
|
-
const arg = args[index]!;
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
arg === "-o" ||
|
|
73
|
-
arg === "-O" ||
|
|
74
|
-
arg === "--output" ||
|
|
75
|
-
arg === "--output-document"
|
|
76
|
-
) {
|
|
77
|
-
outputPath = args[index + 1] ?? null;
|
|
78
|
-
index += 1;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (arg.startsWith("-o=")) {
|
|
83
|
-
outputPath = arg.slice(3);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (arg.startsWith("-O=")) {
|
|
88
|
-
outputPath = arg.slice(3);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
filtered.push(arg);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { outputPath, inputArgs: filtered };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
61
|
export function stripUrlFilename(url: string): string {
|
|
99
62
|
const cleaned = url.split("?")[0]?.split("#")[0] ?? url;
|
|
100
63
|
const lastPart = cleaned.split("/").filter(Boolean).pop();
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import type { VirtualUserManager } from "../../SSHMimic/users";
|
|
1
2
|
import type {
|
|
3
|
+
CommandContext,
|
|
2
4
|
CommandMode,
|
|
3
5
|
CommandOutcome,
|
|
4
6
|
CommandResult,
|
|
5
7
|
ShellModule,
|
|
6
8
|
} from "../../types/commands";
|
|
7
9
|
import type VirtualFileSystem from "../../VirtualFileSystem";
|
|
8
|
-
import type { VirtualUserManager } from "../users";
|
|
9
10
|
import { adduserCommand } from "./adduser";
|
|
10
11
|
import { catCommand } from "./cat";
|
|
11
12
|
import { cdCommand } from "./cd";
|
|
@@ -67,18 +68,72 @@ const BASE_COMMANDS: ShellModule[] = [
|
|
|
67
68
|
exitCommand,
|
|
68
69
|
];
|
|
69
70
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const customCommands: ShellModule[] = [];
|
|
72
|
+
|
|
73
|
+
const helpCommand = createHelpCommand(() =>
|
|
74
|
+
getCommandModules().map((cmd) => cmd.name),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
function getCommandModules(): ShellModule[] {
|
|
78
|
+
return [...BASE_COMMANDS, ...customCommands, helpCommand];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getTakenCommandNames(modules: ShellModule[]): Set<string> {
|
|
82
|
+
const taken = new Set<string>();
|
|
83
|
+
for (const mod of modules) {
|
|
84
|
+
taken.add(mod.name);
|
|
85
|
+
for (const alias of mod.aliases ?? []) {
|
|
86
|
+
taken.add(alias);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return taken;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function registerCommand(module: ShellModule): void {
|
|
93
|
+
const normalized: ShellModule = {
|
|
94
|
+
...module,
|
|
95
|
+
name: module.name.trim().toLowerCase(),
|
|
96
|
+
aliases: module.aliases?.map((alias) => alias.trim().toLowerCase()),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const names = [normalized.name, ...(normalized.aliases ?? [])];
|
|
100
|
+
if (names.some((name) => name.length === 0 || /\s/.test(name))) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"Command names and aliases must be non-empty and contain no spaces",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const takenNames = getTakenCommandNames(getCommandModules());
|
|
107
|
+
const conflict = names.find((name) => takenNames.has(name));
|
|
108
|
+
if (conflict) {
|
|
109
|
+
throw new Error(`Command '${conflict}' already exists`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
customCommands.push(normalized);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function createCustomCommand(
|
|
116
|
+
name: string,
|
|
117
|
+
params: string[],
|
|
118
|
+
run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
|
|
119
|
+
): ShellModule {
|
|
120
|
+
return {
|
|
121
|
+
name,
|
|
122
|
+
params,
|
|
123
|
+
run,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
74
126
|
|
|
75
127
|
export function getCommandNames(): string[] {
|
|
76
|
-
return
|
|
128
|
+
return getCommandModules().flatMap((cmd) => [
|
|
129
|
+
cmd.name,
|
|
130
|
+
...(cmd.aliases ?? []),
|
|
131
|
+
]);
|
|
77
132
|
}
|
|
78
133
|
|
|
79
134
|
function resolveModule(name: string): ShellModule | undefined {
|
|
80
135
|
const lowered = name.toLowerCase();
|
|
81
|
-
return
|
|
136
|
+
return getCommandModules().find(
|
|
82
137
|
(cmd) => cmd.name === lowered || cmd.aliases?.includes(lowered),
|
|
83
138
|
);
|
|
84
139
|
}
|
|
@@ -152,7 +207,7 @@ async function runCommandInternal(
|
|
|
152
207
|
) {
|
|
153
208
|
// Use pipeline executor
|
|
154
209
|
const { parseShellPipeline } = await import("../shellParser");
|
|
155
|
-
const { executePipeline } = await import("
|
|
210
|
+
const { executePipeline } = await import("../../SSHMimic/executor");
|
|
156
211
|
|
|
157
212
|
const pipeline = parseShellPipeline(rawInput);
|
|
158
213
|
if (!pipeline.isValid) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
+
import { getArg, ifFlag } from "./command-helpers";
|
|
2
3
|
import { assertPathAccess, joinListWithType, resolvePath } from "./helpers";
|
|
3
4
|
|
|
4
5
|
function formatPermissions(mode: number, isDirectory: boolean): string {
|
|
@@ -29,8 +30,8 @@ export const lsCommand: ShellModule = {
|
|
|
29
30
|
name: "ls",
|
|
30
31
|
params: ["[path]"],
|
|
31
32
|
run: ({ authUser, vfs, cwd, args }) => {
|
|
32
|
-
const longFormat = args
|
|
33
|
-
const targetArg = args
|
|
33
|
+
const longFormat = ifFlag(args, ["-l", "--long"]);
|
|
34
|
+
const targetArg = getArg(args, 0, { flags: ["-l", "--long"] });
|
|
34
35
|
const target = resolvePath(cwd, targetArg ?? cwd);
|
|
35
36
|
assertPathAccess(authUser, target, "ls");
|
|
36
37
|
const items = vfs.list(target).filter((name) => !name.startsWith("."));
|