typescript-virtual-container 1.2.3 → 1.2.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/README.md +871 -1231
- package/benchmark-results.txt +21 -21
- package/biome.json +9 -0
- package/dist/SSHMimic/index.d.ts +19 -2
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +127 -15
- package/dist/VirtualFileSystem/index.d.ts +115 -88
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +406 -258
- package/dist/VirtualShell/index.d.ts +3 -4
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +5 -23
- package/dist/VirtualUserManager/index.d.ts +41 -3
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +83 -21
- package/dist/commands/chmod.d.ts +3 -0
- package/dist/commands/chmod.d.ts.map +1 -0
- package/dist/commands/chmod.js +31 -0
- package/dist/commands/cp.d.ts +3 -0
- package/dist/commands/cp.d.ts.map +1 -0
- package/dist/commands/cp.js +68 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +48 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +61 -35
- package/dist/commands/head.d.ts +3 -0
- package/dist/commands/head.d.ts.map +1 -0
- package/dist/commands/head.js +30 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +25 -35
- package/dist/commands/ln.d.ts +3 -0
- package/dist/commands/ln.d.ts.map +1 -0
- package/dist/commands/ln.js +42 -0
- package/dist/commands/mv.d.ts +3 -0
- package/dist/commands/mv.d.ts.map +1 -0
- package/dist/commands/mv.js +35 -0
- package/dist/commands/tail.d.ts +3 -0
- package/dist/commands/tail.d.ts.map +1 -0
- package/dist/commands/tail.js +33 -0
- package/dist/commands/wc.d.ts +3 -0
- package/dist/commands/wc.d.ts.map +1 -0
- package/dist/commands/wc.js +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/standalone.js +7 -9
- package/package.json +7 -3
- package/scripts/publish-package.sh +70 -0
- package/src/SSHMimic/index.ts +159 -17
- package/src/VirtualFileSystem/index.ts +500 -280
- package/src/VirtualShell/index.ts +5 -33
- package/src/VirtualUserManager/index.ts +92 -26
- package/src/commands/chmod.ts +33 -0
- package/src/commands/cp.ts +76 -0
- package/src/commands/find.ts +61 -0
- package/src/commands/grep.ts +54 -38
- package/src/commands/head.ts +35 -0
- package/src/commands/index.ts +25 -43
- package/src/commands/ln.ts +47 -0
- package/src/commands/mv.ts +43 -0
- package/src/commands/tail.ts +37 -0
- package/src/commands/wc.ts +48 -0
- package/src/index.ts +1 -0
- package/src/standalone.ts +12 -9
- package/standalone.js +102 -0
- package/standalone.js.map +7 -0
- package/tests/bun-test-shim.ts +1 -0
- package/tests/sftp.test.ts +115 -191
- package/tests/users.test.ts +66 -83
package/src/commands/index.ts
CHANGED
|
@@ -8,19 +8,25 @@ import type {
|
|
|
8
8
|
import { adduserCommand } from "./adduser";
|
|
9
9
|
import { catCommand } from "./cat";
|
|
10
10
|
import { cdCommand } from "./cd";
|
|
11
|
+
import { chmodCommand } from "./chmod";
|
|
11
12
|
import { clearCommand } from "./clear";
|
|
13
|
+
import { cpCommand } from "./cp";
|
|
12
14
|
import { curlCommand } from "./curl";
|
|
13
15
|
import { deluserCommand } from "./deluser";
|
|
14
16
|
import { echoCommand } from "./echo";
|
|
15
17
|
import { envCommand } from "./env";
|
|
16
18
|
import { exitCommand } from "./exit";
|
|
17
19
|
import { exportCommand } from "./export";
|
|
20
|
+
import { findCommand } from "./find";
|
|
18
21
|
import { grepCommand } from "./grep";
|
|
22
|
+
import { headCommand } from "./head";
|
|
19
23
|
import { createHelpCommand } from "./help";
|
|
20
24
|
import { hostnameCommand } from "./hostname";
|
|
21
25
|
import { htopCommand } from "./htop";
|
|
26
|
+
import { lnCommand } from "./ln";
|
|
22
27
|
import { lsCommand } from "./ls";
|
|
23
28
|
import { mkdirCommand } from "./mkdir";
|
|
29
|
+
import { mvCommand } from "./mv";
|
|
24
30
|
import { nanoCommand } from "./nano";
|
|
25
31
|
import { neofetchCommand } from "./neofetch";
|
|
26
32
|
import { passwdCommand } from "./passwd";
|
|
@@ -30,9 +36,11 @@ import { setCommand } from "./set";
|
|
|
30
36
|
import { shCommand } from "./sh";
|
|
31
37
|
import { suCommand } from "./su";
|
|
32
38
|
import { sudoCommand } from "./sudo";
|
|
39
|
+
import { tailCommand } from "./tail";
|
|
33
40
|
import { touchCommand } from "./touch";
|
|
34
41
|
import { treeCommand } from "./tree";
|
|
35
42
|
import { unsetCommand } from "./unset";
|
|
43
|
+
import { wcCommand } from "./wc";
|
|
36
44
|
import { wgetCommand } from "./wget";
|
|
37
45
|
import { whoCommand } from "./who";
|
|
38
46
|
import { whoamiCommand } from "./whoami";
|
|
@@ -68,6 +76,14 @@ const BASE_COMMANDS: ShellModule[] = [
|
|
|
68
76
|
shCommand,
|
|
69
77
|
clearCommand,
|
|
70
78
|
exitCommand,
|
|
79
|
+
cpCommand,
|
|
80
|
+
mvCommand,
|
|
81
|
+
lnCommand,
|
|
82
|
+
findCommand,
|
|
83
|
+
wcCommand,
|
|
84
|
+
headCommand,
|
|
85
|
+
tailCommand,
|
|
86
|
+
chmodCommand,
|
|
71
87
|
];
|
|
72
88
|
|
|
73
89
|
const customCommands: ShellModule[] = [];
|
|
@@ -80,6 +96,7 @@ const commandRegistry = new Map<string, ShellModule>();
|
|
|
80
96
|
let cachedCommandNames: string[] | null = null;
|
|
81
97
|
|
|
82
98
|
function buildCache(): void {
|
|
99
|
+
commandRegistry.clear();
|
|
83
100
|
for (const mod of getCommandModules()) {
|
|
84
101
|
commandRegistry.set(mod.name, mod);
|
|
85
102
|
for (const alias of mod.aliases ?? []) {
|
|
@@ -90,13 +107,6 @@ function buildCache(): void {
|
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
function getCommandModules(): ShellModule[] {
|
|
93
|
-
// console.log("Loading command modules...");
|
|
94
|
-
// console.log(
|
|
95
|
-
// `Base commands: ${BASE_COMMANDS.map((cmd) => cmd.name).join(", ")}`,
|
|
96
|
-
// );
|
|
97
|
-
// console.log(
|
|
98
|
-
// `Custom commands: ${customCommands.map((cmd) => cmd.name).join(", ")}`,
|
|
99
|
-
// );
|
|
100
110
|
return [...BASE_COMMANDS, ...customCommands, helpCommand];
|
|
101
111
|
}
|
|
102
112
|
|
|
@@ -114,13 +124,7 @@ export function registerCommand(module: ShellModule): void {
|
|
|
114
124
|
);
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
|
|
118
|
-
if (commandRegistry.has(name)) {
|
|
119
|
-
throw new Error(`Command '${name}' already exists`);
|
|
120
|
-
}
|
|
121
|
-
commandRegistry.set(name, normalized);
|
|
122
|
-
}
|
|
123
|
-
|
|
127
|
+
customCommands.push(normalized);
|
|
124
128
|
buildCache();
|
|
125
129
|
}
|
|
126
130
|
|
|
@@ -129,24 +133,16 @@ export function createCustomCommand(
|
|
|
129
133
|
params: string[],
|
|
130
134
|
run: (ctx: CommandContext) => CommandResult | Promise<CommandResult>,
|
|
131
135
|
): ShellModule {
|
|
132
|
-
return {
|
|
133
|
-
name,
|
|
134
|
-
params,
|
|
135
|
-
run,
|
|
136
|
-
};
|
|
136
|
+
return { name, params, run };
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
export function getCommandNames(): string[] {
|
|
140
|
-
if (!cachedCommandNames)
|
|
141
|
-
buildCache();
|
|
142
|
-
}
|
|
140
|
+
if (!cachedCommandNames) buildCache();
|
|
143
141
|
return cachedCommandNames!;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
144
|
export function resolveModule(name: string): ShellModule | undefined {
|
|
147
|
-
if (!cachedCommandNames)
|
|
148
|
-
buildCache();
|
|
149
|
-
}
|
|
145
|
+
if (!cachedCommandNames) buildCache();
|
|
150
146
|
return commandRegistry.get(name.toLowerCase());
|
|
151
147
|
}
|
|
152
148
|
|
|
@@ -166,7 +162,6 @@ function splitArgsRespectingQuotes(input: string): string[] {
|
|
|
166
162
|
quoteChar = ch;
|
|
167
163
|
continue;
|
|
168
164
|
}
|
|
169
|
-
|
|
170
165
|
if (ch === quoteChar) {
|
|
171
166
|
inQuotes = false;
|
|
172
167
|
quoteChar = "";
|
|
@@ -185,10 +180,7 @@ function splitArgsRespectingQuotes(input: string): string[] {
|
|
|
185
180
|
current += ch;
|
|
186
181
|
}
|
|
187
182
|
|
|
188
|
-
if (current.length > 0)
|
|
189
|
-
tokens.push(current);
|
|
190
|
-
}
|
|
191
|
-
|
|
183
|
+
if (current.length > 0) tokens.push(current);
|
|
192
184
|
return tokens;
|
|
193
185
|
}
|
|
194
186
|
|
|
@@ -200,8 +192,6 @@ function parseInput(rawInput: string): { commandName: string; args: string[] } {
|
|
|
200
192
|
};
|
|
201
193
|
}
|
|
202
194
|
|
|
203
|
-
// Internal async function for pipeline execution
|
|
204
|
-
|
|
205
195
|
export async function runCommand(
|
|
206
196
|
rawInput: string,
|
|
207
197
|
authUser: string,
|
|
@@ -213,9 +203,7 @@ export async function runCommand(
|
|
|
213
203
|
): Promise<CommandResult> {
|
|
214
204
|
const trimmed = rawInput.trim();
|
|
215
205
|
|
|
216
|
-
if (trimmed.length === 0) {
|
|
217
|
-
return { exitCode: 0 };
|
|
218
|
-
}
|
|
206
|
+
if (trimmed.length === 0) return { exitCode: 0 };
|
|
219
207
|
|
|
220
208
|
if (trimmed.includes("|") || trimmed.includes(">") || trimmed.includes("<")) {
|
|
221
209
|
const { parseShellPipeline } = await import("../VirtualShell/shellParser");
|
|
@@ -223,10 +211,7 @@ export async function runCommand(
|
|
|
223
211
|
|
|
224
212
|
const pipeline = parseShellPipeline(trimmed);
|
|
225
213
|
if (!pipeline.isValid) {
|
|
226
|
-
return {
|
|
227
|
-
stderr: pipeline.error || "Syntax error",
|
|
228
|
-
exitCode: 1,
|
|
229
|
-
};
|
|
214
|
+
return { stderr: pipeline.error || "Syntax error", exitCode: 1 };
|
|
230
215
|
}
|
|
231
216
|
|
|
232
217
|
try {
|
|
@@ -249,10 +234,7 @@ export async function runCommand(
|
|
|
249
234
|
const mod = resolveModule(commandName);
|
|
250
235
|
|
|
251
236
|
if (!mod) {
|
|
252
|
-
return {
|
|
253
|
-
stderr: `Command '${trimmed}' not found`,
|
|
254
|
-
exitCode: 127,
|
|
255
|
-
};
|
|
237
|
+
return { stderr: `Command '${trimmed}' not found`, exitCode: 127 };
|
|
256
238
|
}
|
|
257
239
|
|
|
258
240
|
try {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const lnCommand: ShellModule = {
|
|
6
|
+
name: "ln",
|
|
7
|
+
params: ["[-s] <target> <link_name>"],
|
|
8
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
9
|
+
const symbolic = ifFlag(args, ["-s", "--symbolic"]);
|
|
10
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
11
|
+
const [targetArg, linkArg] = positionals;
|
|
12
|
+
|
|
13
|
+
if (!targetArg || !linkArg) {
|
|
14
|
+
return { stderr: "ln: missing operand", exitCode: 1 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const linkPath = resolvePath(cwd, linkArg);
|
|
18
|
+
const targetPath = symbolic
|
|
19
|
+
? targetArg // keep relative for symlinks
|
|
20
|
+
: resolvePath(cwd, targetArg);
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
assertPathAccess(authUser, linkPath, "ln");
|
|
24
|
+
|
|
25
|
+
if (!symbolic) {
|
|
26
|
+
// Hard link — copy file contents
|
|
27
|
+
const srcPath = resolvePath(cwd, targetArg);
|
|
28
|
+
assertPathAccess(authUser, srcPath, "ln");
|
|
29
|
+
if (!shell.vfs.exists(srcPath)) {
|
|
30
|
+
return {
|
|
31
|
+
stderr: `ln: ${targetArg}: No such file or directory`,
|
|
32
|
+
exitCode: 1,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const content = shell.vfs.readFile(srcPath);
|
|
36
|
+
shell.writeFileAsUser(authUser, linkPath, content);
|
|
37
|
+
} else {
|
|
38
|
+
shell.vfs.symlink(targetPath, linkPath);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { exitCode: 0 };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44
|
+
return { stderr: `ln: ${msg}`, exitCode: 1 };
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
3
|
+
|
|
4
|
+
export const mvCommand: ShellModule = {
|
|
5
|
+
name: "mv",
|
|
6
|
+
params: ["<source> <dest>"],
|
|
7
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
8
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
9
|
+
const [srcArg, destArg] = positionals;
|
|
10
|
+
|
|
11
|
+
if (!srcArg || !destArg) {
|
|
12
|
+
return { stderr: "mv: missing operand", exitCode: 1 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const srcPath = resolvePath(cwd, srcArg);
|
|
16
|
+
const destPath = resolvePath(cwd, destArg);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
assertPathAccess(authUser, srcPath, "mv");
|
|
20
|
+
assertPathAccess(authUser, destPath, "mv");
|
|
21
|
+
|
|
22
|
+
if (!shell.vfs.exists(srcPath)) {
|
|
23
|
+
return {
|
|
24
|
+
stderr: `mv: ${srcArg}: No such file or directory`,
|
|
25
|
+
exitCode: 1,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// If dest is a directory, move into it
|
|
30
|
+
const finalDest =
|
|
31
|
+
shell.vfs.exists(destPath) &&
|
|
32
|
+
shell.vfs.stat(destPath).type === "directory"
|
|
33
|
+
? `${destPath}/${srcArg.split("/").pop()}`
|
|
34
|
+
: destPath;
|
|
35
|
+
|
|
36
|
+
shell.vfs.move(srcPath, finalDest);
|
|
37
|
+
return { exitCode: 0 };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
40
|
+
return { stderr: `mv: ${msg}`, exitCode: 1 };
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { getFlag } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const tailCommand: ShellModule = {
|
|
6
|
+
name: "tail",
|
|
7
|
+
params: ["[-n <lines>] [file...]"],
|
|
8
|
+
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
9
|
+
const nArg = getFlag(args, ["-n"]);
|
|
10
|
+
const n = typeof nArg === "string" ? parseInt(nArg, 10) : 10;
|
|
11
|
+
const positionals = args.filter((a) => !a.startsWith("-") && a !== nArg);
|
|
12
|
+
|
|
13
|
+
const take = (content: string) => {
|
|
14
|
+
const lines = content.split("\n");
|
|
15
|
+
return lines.slice(Math.max(0, lines.length - n)).join("\n");
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (positionals.length === 0) {
|
|
19
|
+
return { stdout: take(stdin ?? ""), exitCode: 0 };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const results: string[] = [];
|
|
23
|
+
for (const file of positionals) {
|
|
24
|
+
const filePath = resolvePath(cwd, file);
|
|
25
|
+
try {
|
|
26
|
+
assertPathAccess(authUser, filePath, "tail");
|
|
27
|
+
results.push(take(shell.vfs.readFile(filePath)));
|
|
28
|
+
} catch {
|
|
29
|
+
return {
|
|
30
|
+
stderr: `tail: ${file}: No such file or directory`,
|
|
31
|
+
exitCode: 1,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { stdout: results.join("\n"), exitCode: 0 };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ShellModule } from "../types/commands";
|
|
2
|
+
import { ifFlag } from "./command-helpers";
|
|
3
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export const wcCommand: ShellModule = {
|
|
6
|
+
name: "wc",
|
|
7
|
+
params: ["[-l] [-w] [-c] [file...]"],
|
|
8
|
+
run: ({ authUser, shell, cwd, args, stdin }) => {
|
|
9
|
+
const lines = ifFlag(args, ["-l"]);
|
|
10
|
+
const words = ifFlag(args, ["-w"]);
|
|
11
|
+
const bytes = ifFlag(args, ["-c"]);
|
|
12
|
+
const showAll = !lines && !words && !bytes;
|
|
13
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
14
|
+
|
|
15
|
+
const count = (content: string, label: string): string => {
|
|
16
|
+
const l = content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
17
|
+
const w = content.trim().split(/\s+/).filter(Boolean).length;
|
|
18
|
+
const c = Buffer.byteLength(content, "utf8");
|
|
19
|
+
const parts: string[] = [];
|
|
20
|
+
if (showAll || lines) parts.push(String(l).padStart(7));
|
|
21
|
+
if (showAll || words) parts.push(String(w).padStart(7));
|
|
22
|
+
if (showAll || bytes) parts.push(String(c).padStart(7));
|
|
23
|
+
if (label) parts.push(` ${label}`);
|
|
24
|
+
return parts.join("");
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (positionals.length === 0) {
|
|
28
|
+
const content = stdin ?? "";
|
|
29
|
+
return { stdout: count(content, ""), exitCode: 0 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const results: string[] = [];
|
|
33
|
+
for (const file of positionals) {
|
|
34
|
+
const filePath = resolvePath(cwd, file);
|
|
35
|
+
try {
|
|
36
|
+
assertPathAccess(authUser, filePath, "wc");
|
|
37
|
+
const content = shell.vfs.readFile(filePath);
|
|
38
|
+
results.push(count(content, file));
|
|
39
|
+
} catch {
|
|
40
|
+
return {
|
|
41
|
+
stderr: `wc: ${file}: No such file or directory`,
|
|
42
|
+
exitCode: 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { stdout: results.join("\n"), exitCode: 0 };
|
|
47
|
+
},
|
|
48
|
+
};
|
package/src/index.ts
CHANGED
package/src/standalone.ts
CHANGED
|
@@ -16,12 +16,6 @@ new VirtualSshServer({
|
|
|
16
16
|
shell: virtualShell,
|
|
17
17
|
})
|
|
18
18
|
.start()
|
|
19
|
-
.then((port: number) => {
|
|
20
|
-
// if (!sshMimic) console.error("Failed to initialize SSH Mimic shell.");
|
|
21
|
-
// else {
|
|
22
|
-
console.log(`SSH Mimic initialized. Listening on port ${port}.`);
|
|
23
|
-
// }
|
|
24
|
-
})
|
|
25
19
|
.catch((error: unknown) => {
|
|
26
20
|
console.error("Failed to start SSH Mimic:", error);
|
|
27
21
|
process.exit(1);
|
|
@@ -29,10 +23,19 @@ new VirtualSshServer({
|
|
|
29
23
|
|
|
30
24
|
new VirtualSftpServer({ port: 2223, hostname, shell: virtualShell })
|
|
31
25
|
.start()
|
|
32
|
-
.then((port: number) => {
|
|
33
|
-
console.log(`SFTP Mimic initialized. Listening on port ${port}.`);
|
|
34
|
-
})
|
|
35
26
|
.catch((error: unknown) => {
|
|
36
27
|
console.error("Failed to start SFTP Mimic:", error);
|
|
37
28
|
process.exit(1);
|
|
38
29
|
});
|
|
30
|
+
|
|
31
|
+
process.on("uncaughtException", (error) => {
|
|
32
|
+
console.log("Oh my god, something terrible happened: ", error);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
process.on("unhandledRejection", (error, promise) => {
|
|
36
|
+
console.log(
|
|
37
|
+
" Oh Lord! We forgot to handle a promise rejection here: ",
|
|
38
|
+
promise,
|
|
39
|
+
);
|
|
40
|
+
console.log(" The error was: ", error);
|
|
41
|
+
});
|