typescript-virtual-container 0.1.0 → 1.0.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.
- package/.github/workflows/create-pull-request.yml +11 -9
- package/.github/workflows/test-battery.yml +0 -2
- package/CHANGELOG.md +26 -0
- package/biome.json +7 -6
- package/package.json +1 -1
- package/src/SSHMimic/commands/curl.ts +133 -8
- package/src/SSHMimic/commands/helpers.ts +23 -1
- package/src/SSHMimic/commands/ls.ts +35 -1
- package/src/SSHMimic/commands/wget.ts +112 -10
- package/src/SSHMimic/exec.ts +9 -2
- package/src/SSHMimic/shell.ts +11 -4
- package/src/index.ts +6 -4
|
@@ -3,11 +3,10 @@ name: Auto Create Pull Request
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
branches:
|
|
6
|
-
- 'dev'
|
|
7
6
|
|
|
8
7
|
jobs:
|
|
9
8
|
create-pull-request:
|
|
10
|
-
if: github.ref
|
|
9
|
+
if: github.ref != 'refs/heads/main'
|
|
11
10
|
runs-on: ubuntu-latest
|
|
12
11
|
steps:
|
|
13
12
|
- name: Validate pull request token
|
|
@@ -26,18 +25,21 @@ jobs:
|
|
|
26
25
|
script: |
|
|
27
26
|
const owner = context.repo.owner;
|
|
28
27
|
const repo = context.repo.repo;
|
|
29
|
-
const
|
|
28
|
+
const sourceBranch = context.ref.startsWith('refs/heads/')
|
|
29
|
+
? context.ref.slice('refs/heads/'.length)
|
|
30
|
+
: context.ref;
|
|
31
|
+
const head = `${owner}:${sourceBranch}`;
|
|
30
32
|
const base = 'main';
|
|
31
33
|
|
|
32
|
-
// Check if there are commits between main and
|
|
34
|
+
// Check if there are commits between main and the current branch
|
|
33
35
|
const { data: comparison } = await github.rest.repos.compareCommits({
|
|
34
36
|
owner,
|
|
35
37
|
repo,
|
|
36
38
|
base: 'main',
|
|
37
|
-
head:
|
|
39
|
+
head: sourceBranch,
|
|
38
40
|
}).catch(err => {
|
|
39
41
|
if (err.status === 404 && err.message.includes('No commits')) {
|
|
40
|
-
core.info('No commits between main and
|
|
42
|
+
core.info('No commits between main and ' + sourceBranch + ' - skipping PR creation');
|
|
41
43
|
return { data: { ahead_by: 0 } };
|
|
42
44
|
}
|
|
43
45
|
throw err;
|
|
@@ -64,13 +66,13 @@ jobs:
|
|
|
64
66
|
const { data: pullRequest } = await github.rest.pulls.create({
|
|
65
67
|
owner,
|
|
66
68
|
repo,
|
|
67
|
-
head:
|
|
69
|
+
head: sourceBranch,
|
|
68
70
|
base,
|
|
69
|
-
title:
|
|
71
|
+
title: `chore: auto PR from ${sourceBranch} to main`,
|
|
70
72
|
body: [
|
|
71
73
|
'This pull request was created automatically by GitHub Actions.',
|
|
72
74
|
'',
|
|
73
|
-
`- source branch:
|
|
75
|
+
`- source branch: \`${sourceBranch}\``,
|
|
74
76
|
`- target branch: \`main\``,
|
|
75
77
|
`- triggered by commit \`${context.sha}\``,
|
|
76
78
|
].join('\n'),
|
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,32 @@ The format is based on Keep a Changelog.
|
|
|
15
15
|
- CODE_OF_CONDUCT.md
|
|
16
16
|
- GitHub issue and PR templates
|
|
17
17
|
|
|
18
|
+
## [1.0.1] - 2026-04-14
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `ls -l` / `ls --long` support with long listing format (permissions, size, updated time).
|
|
23
|
+
- Host-command mirroring for network tools:
|
|
24
|
+
- `curl` now runs through host `curl` via `child_process`.
|
|
25
|
+
- `wget` now runs through host `wget` via `child_process`.
|
|
26
|
+
- Temporary host download flow for `wget` using `/tmp` before import into VFS.
|
|
27
|
+
- Terminal line normalization utility for command help and diagnostics rendering.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `curl` behavior is now aligned with the host binary output and exit codes.
|
|
32
|
+
- `curl -o` writes host command output to the virtual filesystem target path.
|
|
33
|
+
- `wget` writes downloaded payloads to VFS after host-side transfer, preserving command semantics.
|
|
34
|
+
- URL fetch helper now accepts host-only inputs by normalizing missing protocol to `http://`.
|
|
35
|
+
- Auto pull-request GitHub workflow now targets any non-`main` branch instead of only `dev`.
|
|
36
|
+
- Auto PR metadata now uses the dynamic source branch name in PR head/title/body.
|
|
37
|
+
- Test workflow trigger scope was generalized by removing hardcoded branch filters.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Resolved large horizontal spacing artifacts in SSH terminal output by normalizing TTY line endings (`\r\n`) in both interactive shell and exec paths.
|
|
42
|
+
- Reduced excessive whitespace in help output rendering (`curl --help`, `wget --help`) by normalizing tabs and over-padded spacing.
|
|
43
|
+
|
|
18
44
|
## [1.0.0] - 2026-04-14
|
|
19
45
|
|
|
20
46
|
### Added
|
package/biome.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
+
"assist": {
|
|
3
|
+
"actions": {
|
|
4
|
+
"source": {
|
|
5
|
+
"organizeImports": "off"
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
},
|
|
2
9
|
"linter": {
|
|
3
10
|
"rules": {
|
|
4
11
|
"suspicious": {
|
|
@@ -10,11 +17,5 @@
|
|
|
10
17
|
"noNonNullAssertion": "off"
|
|
11
18
|
}
|
|
12
19
|
}
|
|
13
|
-
},
|
|
14
|
-
"assist": {
|
|
15
|
-
"source": {
|
|
16
|
-
"autoImport": "on",
|
|
17
|
-
"importModuleSpecifierPreference": "relative"
|
|
18
|
-
}
|
|
19
20
|
}
|
|
20
21
|
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,152 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import type { ShellModule } from "../../types/commands";
|
|
2
|
-
import {
|
|
3
|
+
import { normalizeTerminalOutput, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
function parseCurlOutputPath(args: string[]): {
|
|
6
|
+
outputPath: string | null;
|
|
7
|
+
inputArgs: string[];
|
|
8
|
+
} {
|
|
9
|
+
const filtered: string[] = [];
|
|
10
|
+
let outputPath: string | null = null;
|
|
11
|
+
|
|
12
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
13
|
+
const arg = args[index]!;
|
|
14
|
+
|
|
15
|
+
if (arg === "-o" || arg === "--output") {
|
|
16
|
+
outputPath = args[index + 1] ?? null;
|
|
17
|
+
index += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (arg.startsWith("-o=")) {
|
|
22
|
+
outputPath = arg.slice(3);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (arg.startsWith("--output=")) {
|
|
27
|
+
outputPath = arg.slice("--output=".length);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
filtered.push(arg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { outputPath, inputArgs: filtered };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runHostCurl(args: string[]): Promise<{
|
|
38
|
+
stdout: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
exitCode: number;
|
|
41
|
+
}> {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
let childProcess: ReturnType<typeof spawn>;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
childProcess = spawn("curl", args, {
|
|
47
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
resolve({
|
|
51
|
+
stdout: "",
|
|
52
|
+
stderr: `curl: ${error instanceof Error ? error.message : String(error)}`,
|
|
53
|
+
exitCode: 1,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let stdout = "";
|
|
59
|
+
let stderr = "";
|
|
60
|
+
const stdoutStream = childProcess.stdout;
|
|
61
|
+
const stderrStream = childProcess.stderr;
|
|
62
|
+
|
|
63
|
+
if (!stdoutStream || !stderrStream) {
|
|
64
|
+
resolve({
|
|
65
|
+
stdout: "",
|
|
66
|
+
stderr: "curl: failed to capture process output",
|
|
67
|
+
exitCode: 1,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
stdoutStream.setEncoding("utf8");
|
|
73
|
+
stderrStream.setEncoding("utf8");
|
|
74
|
+
|
|
75
|
+
stdoutStream.on("data", (chunk: string) => {
|
|
76
|
+
stdout += chunk;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
stderrStream.on("data", (chunk: string) => {
|
|
80
|
+
stderr += chunk;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
childProcess.on("error", (error) => {
|
|
84
|
+
const errorCode =
|
|
85
|
+
error instanceof Error && "code" in error
|
|
86
|
+
? String((error as NodeJS.ErrnoException).code ?? "")
|
|
87
|
+
: "";
|
|
88
|
+
resolve({
|
|
89
|
+
stdout: "",
|
|
90
|
+
stderr: `curl: ${error.message}`,
|
|
91
|
+
exitCode: errorCode === "ENOENT" ? 127 : 1,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
childProcess.on("close", (code) => {
|
|
96
|
+
resolve({
|
|
97
|
+
stdout,
|
|
98
|
+
stderr,
|
|
99
|
+
exitCode: code ?? 1,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
3
104
|
|
|
4
105
|
export const curlCommand: ShellModule = {
|
|
5
106
|
name: "curl",
|
|
6
107
|
params: ["[-o file] <url>"],
|
|
7
108
|
run: async ({ vfs, cwd, args }) => {
|
|
8
|
-
const { outputPath, inputArgs } =
|
|
109
|
+
const { outputPath, inputArgs } = parseCurlOutputPath(args);
|
|
9
110
|
const url = inputArgs[0];
|
|
111
|
+
const isHelpLike = inputArgs.some(
|
|
112
|
+
(arg) =>
|
|
113
|
+
arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version",
|
|
114
|
+
);
|
|
10
115
|
|
|
11
116
|
if (!url) {
|
|
12
117
|
return { stderr: "curl: missing URL", exitCode: 1 };
|
|
13
118
|
}
|
|
14
119
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
120
|
+
const passthroughArgs = outputPath ? [...inputArgs, "-o", "-"] : inputArgs;
|
|
121
|
+
const result = await runHostCurl(passthroughArgs);
|
|
122
|
+
|
|
123
|
+
if (result.exitCode !== 0) {
|
|
124
|
+
return {
|
|
125
|
+
stderr: normalizeTerminalOutput(
|
|
126
|
+
result.stderr || `curl: exited with code ${result.exitCode}`,
|
|
127
|
+
),
|
|
128
|
+
exitCode: result.exitCode,
|
|
129
|
+
};
|
|
18
130
|
}
|
|
19
131
|
|
|
20
132
|
if (outputPath) {
|
|
21
|
-
vfs.writeFile(resolvePath(cwd, outputPath), result.
|
|
22
|
-
return {
|
|
133
|
+
vfs.writeFile(resolvePath(cwd, outputPath), result.stdout);
|
|
134
|
+
return {
|
|
135
|
+
stderr: result.stderr
|
|
136
|
+
? normalizeTerminalOutput(result.stderr)
|
|
137
|
+
: undefined,
|
|
138
|
+
exitCode: 0,
|
|
139
|
+
};
|
|
23
140
|
}
|
|
24
141
|
|
|
25
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
stdout: isHelpLike
|
|
144
|
+
? normalizeTerminalOutput(result.stdout)
|
|
145
|
+
: result.stdout,
|
|
146
|
+
stderr: result.stderr
|
|
147
|
+
? normalizeTerminalOutput(result.stderr)
|
|
148
|
+
: undefined,
|
|
149
|
+
exitCode: 0,
|
|
150
|
+
};
|
|
26
151
|
},
|
|
27
152
|
};
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import type VirtualFileSystem from "../../VirtualFileSystem";
|
|
3
3
|
|
|
4
|
+
function normalizeFetchUrl(input: string): string {
|
|
5
|
+
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(input)) {
|
|
6
|
+
return input;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return `http://${input}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeTerminalOutput(text: string): string {
|
|
13
|
+
return text
|
|
14
|
+
.replace(/\r\n/g, "\n")
|
|
15
|
+
.replace(/\r/g, "\n")
|
|
16
|
+
.replace(/\t/g, " ")
|
|
17
|
+
.split("\n")
|
|
18
|
+
.map((line) =>
|
|
19
|
+
line.replace(/^[ \u00A0]{8,}/, " ").replace(/[ \u00A0]{3,}/g, " "),
|
|
20
|
+
)
|
|
21
|
+
.join("\n")
|
|
22
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
23
|
+
.trimEnd();
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
export function resolvePath(cwd: string, inputPath: string): string {
|
|
5
27
|
if (!inputPath || inputPath.trim() === "") {
|
|
6
28
|
return cwd;
|
|
@@ -56,7 +78,7 @@ export function stripUrlFilename(url: string): string {
|
|
|
56
78
|
export async function fetchResource(
|
|
57
79
|
url: string,
|
|
58
80
|
): Promise<{ text: string; status: number; contentType: string | null }> {
|
|
59
|
-
const response = await fetch(url);
|
|
81
|
+
const response = await fetch(normalizeFetchUrl(url));
|
|
60
82
|
const contentType = response.headers.get("content-type");
|
|
61
83
|
return {
|
|
62
84
|
text: await response.text(),
|
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
import type { ShellModule } from "../../types/commands";
|
|
2
2
|
import { joinListWithType, resolvePath } from "./helpers";
|
|
3
3
|
|
|
4
|
+
function formatPermissions(mode: number, isDirectory: boolean): string {
|
|
5
|
+
const fileType = isDirectory ? "d" : "-";
|
|
6
|
+
const permissionBits = [
|
|
7
|
+
[0o400, "r"],
|
|
8
|
+
[0o200, "w"],
|
|
9
|
+
[0o100, "x"],
|
|
10
|
+
[0o040, "r"],
|
|
11
|
+
[0o020, "w"],
|
|
12
|
+
[0o010, "x"],
|
|
13
|
+
[0o004, "r"],
|
|
14
|
+
[0o002, "w"],
|
|
15
|
+
[0o001, "x"],
|
|
16
|
+
] as const;
|
|
17
|
+
const permissions = permissionBits
|
|
18
|
+
.map(([bit, symbol]) => (mode & bit ? symbol : "-"))
|
|
19
|
+
.join("");
|
|
20
|
+
|
|
21
|
+
return `${fileType}${permissions}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatDate(date: Date): string {
|
|
25
|
+
return date.toISOString().replace("T", " ").slice(0, 16);
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
export const lsCommand: ShellModule = {
|
|
5
29
|
name: "ls",
|
|
6
30
|
params: ["[path]"],
|
|
7
31
|
run: ({ vfs, cwd, args }) => {
|
|
32
|
+
const longFormat = args.includes("-l") || args.includes("--long");
|
|
8
33
|
const targetArg = args.find((arg) => !arg.startsWith("-"));
|
|
9
34
|
const target = resolvePath(cwd, targetArg ?? cwd);
|
|
10
35
|
const items = vfs.list(target).filter((name) => !name.startsWith("."));
|
|
11
|
-
const rendered =
|
|
36
|
+
const rendered = longFormat
|
|
37
|
+
? items
|
|
38
|
+
.map((name) => {
|
|
39
|
+
const childPath = resolvePath(target, name);
|
|
40
|
+
const stat = vfs.stat(childPath);
|
|
41
|
+
const size = stat.type === "file" ? stat.size : stat.childrenCount;
|
|
42
|
+
return `${formatPermissions(stat.mode, stat.type === "directory")} 1 ${size} ${formatDate(stat.updatedAt)} ${name}${stat.type === "directory" ? "/" : ""}`;
|
|
43
|
+
})
|
|
44
|
+
.join("\n")
|
|
45
|
+
: joinListWithType(target, items, (p) => vfs.stat(p));
|
|
12
46
|
return { stdout: rendered, exitCode: 0 };
|
|
13
47
|
},
|
|
14
48
|
};
|
|
@@ -1,33 +1,135 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
1
5
|
import type { ShellModule } from "../../types/commands";
|
|
2
6
|
import {
|
|
3
|
-
|
|
7
|
+
normalizeTerminalOutput,
|
|
4
8
|
parseOutputPath,
|
|
5
9
|
resolvePath,
|
|
6
10
|
stripUrlFilename,
|
|
7
11
|
} from "./helpers";
|
|
8
12
|
|
|
13
|
+
function runHostWget(args: string[]): Promise<{
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
exitCode: number;
|
|
17
|
+
}> {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
let childProcess: ReturnType<typeof spawn>;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
childProcess = spawn("wget", args, {
|
|
23
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
resolve({
|
|
27
|
+
stdout: "",
|
|
28
|
+
stderr: `wget: ${error instanceof Error ? error.message : String(error)}`,
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let stdout = "";
|
|
35
|
+
let stderr = "";
|
|
36
|
+
const stdoutStream = childProcess.stdout;
|
|
37
|
+
const stderrStream = childProcess.stderr;
|
|
38
|
+
|
|
39
|
+
if (!stdoutStream || !stderrStream) {
|
|
40
|
+
resolve({
|
|
41
|
+
stdout: "",
|
|
42
|
+
stderr: "wget: failed to capture process output",
|
|
43
|
+
exitCode: 1,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
stdoutStream.setEncoding("utf8");
|
|
49
|
+
stderrStream.setEncoding("utf8");
|
|
50
|
+
|
|
51
|
+
stdoutStream.on("data", (chunk: string) => {
|
|
52
|
+
stdout += chunk;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
stderrStream.on("data", (chunk: string) => {
|
|
56
|
+
stderr += chunk;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
childProcess.on("error", (error) => {
|
|
60
|
+
const errorCode =
|
|
61
|
+
error instanceof Error && "code" in error
|
|
62
|
+
? String((error as NodeJS.ErrnoException).code ?? "")
|
|
63
|
+
: "";
|
|
64
|
+
resolve({
|
|
65
|
+
stdout: "",
|
|
66
|
+
stderr: `wget: ${error.message}`,
|
|
67
|
+
exitCode: errorCode === "ENOENT" ? 127 : 1,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
childProcess.on("close", (code) => {
|
|
72
|
+
resolve({
|
|
73
|
+
stdout,
|
|
74
|
+
stderr,
|
|
75
|
+
exitCode: code ?? 1,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
9
81
|
export const wgetCommand: ShellModule = {
|
|
10
82
|
name: "wget",
|
|
11
83
|
params: ["[url]"],
|
|
12
84
|
run: async ({ vfs, cwd, args }) => {
|
|
13
85
|
const { outputPath, inputArgs } = parseOutputPath(args);
|
|
14
86
|
const url = inputArgs[0];
|
|
87
|
+
const isHelpLike = inputArgs.some(
|
|
88
|
+
(arg) =>
|
|
89
|
+
arg === "-h" || arg === "--help" || arg === "-V" || arg === "--version",
|
|
90
|
+
);
|
|
15
91
|
|
|
16
92
|
if (!url) {
|
|
17
93
|
return { stderr: "wget: missing URL", exitCode: 1 };
|
|
18
94
|
}
|
|
19
95
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return {
|
|
96
|
+
if (isHelpLike) {
|
|
97
|
+
const result = await runHostWget(inputArgs);
|
|
98
|
+
return {
|
|
99
|
+
stdout: normalizeTerminalOutput(result.stdout),
|
|
100
|
+
stderr: result.stderr
|
|
101
|
+
? normalizeTerminalOutput(result.stderr)
|
|
102
|
+
: undefined,
|
|
103
|
+
exitCode: result.exitCode,
|
|
104
|
+
};
|
|
23
105
|
}
|
|
24
106
|
|
|
25
|
-
const
|
|
26
|
-
|
|
107
|
+
const tempDir = await mkdtemp(join(tmpdir(), "virtual-env-js-wget-"));
|
|
108
|
+
const tempFile = join(tempDir, "download");
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const hostArgs = [...inputArgs, "-O", tempFile];
|
|
112
|
+
const result = await runHostWget(hostArgs);
|
|
27
113
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
114
|
+
if (result.exitCode !== 0) {
|
|
115
|
+
return {
|
|
116
|
+
stderr: normalizeTerminalOutput(
|
|
117
|
+
result.stderr || `wget: exited with code ${result.exitCode}`,
|
|
118
|
+
),
|
|
119
|
+
exitCode: result.exitCode,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const content = await readFile(tempFile, "utf8");
|
|
124
|
+
const target = resolvePath(cwd, outputPath ?? stripUrlFilename(url));
|
|
125
|
+
vfs.writeFile(target, content);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
stdout: `saved ${target}`,
|
|
129
|
+
exitCode: 0,
|
|
130
|
+
};
|
|
131
|
+
} finally {
|
|
132
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
32
134
|
},
|
|
33
135
|
};
|
package/src/SSHMimic/exec.ts
CHANGED
|
@@ -3,6 +3,13 @@ import type VirtualFileSystem from "../VirtualFileSystem";
|
|
|
3
3
|
import { runCommand } from "./commands";
|
|
4
4
|
import type { VirtualUserManager } from "./users";
|
|
5
5
|
|
|
6
|
+
function toTtyLines(text: string): string {
|
|
7
|
+
return text
|
|
8
|
+
.replace(/\r\n/g, "\n")
|
|
9
|
+
.replace(/\r/g, "\n")
|
|
10
|
+
.replace(/\n/g, "\r\n");
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
export function runExec(
|
|
7
14
|
stream: ExecStream,
|
|
8
15
|
cmd: string,
|
|
@@ -23,11 +30,11 @@ export function runExec(
|
|
|
23
30
|
),
|
|
24
31
|
).then((result) => {
|
|
25
32
|
if (result.stdout) {
|
|
26
|
-
stream.write(`${result.stdout}\n`);
|
|
33
|
+
stream.write(`${toTtyLines(result.stdout)}\r\n`);
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
if (result.stderr) {
|
|
30
|
-
stream.stderr.write(`${result.stderr}\n`);
|
|
37
|
+
stream.stderr.write(`${toTtyLines(result.stderr)}\r\n`);
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
stream.exit(result.exitCode ?? 0);
|
package/src/SSHMimic/shell.ts
CHANGED
|
@@ -33,6 +33,13 @@ interface TerminalSize {
|
|
|
33
33
|
rows: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function toTtyLines(text: string): string {
|
|
37
|
+
return text
|
|
38
|
+
.replace(/\r\n/g, "\n")
|
|
39
|
+
.replace(/\r/g, "\n")
|
|
40
|
+
.replace(/\n/g, "\r\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
export function startShell(
|
|
37
44
|
stream: ShellStream,
|
|
38
45
|
authUser: string,
|
|
@@ -198,11 +205,11 @@ export function startShell(
|
|
|
198
205
|
}
|
|
199
206
|
|
|
200
207
|
if (result.stdout) {
|
|
201
|
-
stream.write(`${result.stdout}\r\n`);
|
|
208
|
+
stream.write(`${toTtyLines(result.stdout)}\r\n`);
|
|
202
209
|
}
|
|
203
210
|
|
|
204
211
|
if (result.stderr) {
|
|
205
|
-
stream.write(`${result.stderr}\r\n`);
|
|
212
|
+
stream.write(`${toTtyLines(result.stderr)}\r\n`);
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
if (result.switchUser) {
|
|
@@ -671,11 +678,11 @@ export function startShell(
|
|
|
671
678
|
}
|
|
672
679
|
|
|
673
680
|
if (result.stdout) {
|
|
674
|
-
stream.write(`${result.stdout}\r\n`);
|
|
681
|
+
stream.write(`${toTtyLines(result.stdout)}\r\n`);
|
|
675
682
|
}
|
|
676
683
|
|
|
677
684
|
if (result.stderr) {
|
|
678
|
-
stream.write(`${result.stderr}\r\n`);
|
|
685
|
+
stream.write(`${toTtyLines(result.stderr)}\r\n`);
|
|
679
686
|
}
|
|
680
687
|
|
|
681
688
|
if (result.closeSession) {
|
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ export type {
|
|
|
10
10
|
CommandResult,
|
|
11
11
|
NanoEditorSession,
|
|
12
12
|
ShellModule,
|
|
13
|
-
SudoChallenge
|
|
13
|
+
SudoChallenge,
|
|
14
14
|
} from "./types/commands";
|
|
15
15
|
export type { ExecStream, ShellStream } from "./types/streams";
|
|
16
16
|
export type {
|
|
@@ -25,10 +25,12 @@ export type {
|
|
|
25
25
|
VfsSnapshotDirectoryNode,
|
|
26
26
|
VfsSnapshotFileNode,
|
|
27
27
|
VfsSnapshotNode,
|
|
28
|
-
WriteFileOptions
|
|
28
|
+
WriteFileOptions,
|
|
29
29
|
} from "./types/vfs";
|
|
30
30
|
|
|
31
31
|
export {
|
|
32
|
-
SshClient,
|
|
32
|
+
SshClient,
|
|
33
|
+
VirtualFileSystem,
|
|
34
|
+
SshMimic as VirtualMachine,
|
|
35
|
+
VirtualUserManager,
|
|
33
36
|
};
|
|
34
|
-
|