chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# SSH Resource Examples
|
||||
|
||||
Mount a remote host over SFTP and read it as a filesystem — either via the in-process VFS patch or a real macFUSE/libfuse mount.
|
||||
|
||||
## Setup
|
||||
|
||||
Set the following in `.env.development` at the repo root:
|
||||
|
||||
```bash
|
||||
SSH_HOST=example.com # required: SFTP host
|
||||
SSH_USER=ubuntu # required: username
|
||||
SSH_KEY=~/.ssh/id_ed25519 # optional: private key path
|
||||
SSH_PASSWORD=... # optional: fallback if no key
|
||||
SSH_PORT=22 # optional, default 22
|
||||
SSH_ROOT=/ # optional, default /
|
||||
SSH_PASSPHRASE=... # optional: for encrypted keys
|
||||
```
|
||||
|
||||
## Run VFS demo
|
||||
|
||||
In-process `fs.*` patch — no kernel mount, no daemon. Reads `/etc/hostname` over SFTP and exits.
|
||||
|
||||
```bash
|
||||
pnpm exec tsx examples/typescript/ssh/ssh_vfs.ts
|
||||
```
|
||||
|
||||
## Run FUSE demo
|
||||
|
||||
Real OS mount via `FuseManager`. After mount, the script prints the mount path and waits for Enter to unmount.
|
||||
|
||||
```bash
|
||||
pnpm exec tsx examples/typescript/ssh/ssh_fuse.ts
|
||||
```
|
||||
|
||||
While running, open a second terminal (or Finder) and explore the remote filesystem:
|
||||
|
||||
```bash
|
||||
open <mp>/ssh # macOS Finder
|
||||
ls <mp>/ssh/
|
||||
cat <mp>/ssh/etc/hostname
|
||||
```
|
||||
|
||||
## Production note
|
||||
|
||||
The SSH connection holds credentials (key material or password) in-process. For shared servers, restrict the SSH user with a per-user `chroot`/`AllowUsers` directive in `sshd_config` so the mount only sees what that user is meant to see. A browser build is impossible — SSH/SFTP is a TCP protocol with no CORS-equivalent and no transport that survives in a browser sandbox; see [`docs/plans/2026-04-29-ssh-resource.md`](../../../docs/plans/2026-04-29-ssh-resource.md) for the full design.
|
||||
@@ -0,0 +1,99 @@
|
||||
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import { homedir } from 'node:os'
|
||||
import { MountMode, SSHResource, type SSHConfig, Workspace } from '@struktoai/mirage-node'
|
||||
|
||||
const config: SSHConfig = {
|
||||
host: 'dev',
|
||||
hostname: 'ec2-18-216-110-204.us-east-2.compute.amazonaws.com',
|
||||
username: 'ubuntu',
|
||||
identityFile: `${homedir()}/.ssh/dev.pem`,
|
||||
root: '/home/ubuntu/mirage-test',
|
||||
}
|
||||
|
||||
const resource = new SSHResource(config)
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const ws = new Workspace({ '/ssh/': resource }, { mode: MountMode.WRITE })
|
||||
|
||||
const show = async (label: string, cmd: string): Promise<void> => {
|
||||
console.log(`=== ${label} ===`)
|
||||
const r = await ws.execute(cmd)
|
||||
console.log(r.stdoutText)
|
||||
}
|
||||
|
||||
try {
|
||||
await show('ls /ssh/', 'ls /ssh/')
|
||||
await show('stat /ssh/', 'stat /ssh/')
|
||||
await show('tree /ssh/', 'tree /ssh/')
|
||||
await show('find /ssh/', 'find /ssh/')
|
||||
await show('du /ssh/', 'du /ssh/')
|
||||
await show('cat /ssh/readme.txt', 'cat /ssh/readme.txt')
|
||||
await show('head -n 1 /ssh/data.txt', 'head -n 1 /ssh/data.txt')
|
||||
await show('wc /ssh/readme.txt', 'wc /ssh/readme.txt')
|
||||
await show('grep hello /ssh/readme.txt', 'grep hello /ssh/readme.txt')
|
||||
|
||||
// ── generic text commands (delegate to shared generics) ──
|
||||
const generics = [
|
||||
'sort /ssh/data.txt',
|
||||
'sort -r /ssh/data.txt',
|
||||
'nl /ssh/data.txt',
|
||||
'rev /ssh/data.txt',
|
||||
'tac /ssh/data.txt',
|
||||
'cut -c1-4 /ssh/data.txt',
|
||||
'uniq /ssh/data.txt',
|
||||
'fold -w 3 /ssh/data.txt',
|
||||
'head -n 2 /ssh/data.txt',
|
||||
'tail -n 1 /ssh/data.txt',
|
||||
'wc -l /ssh/data.txt',
|
||||
'sha256sum /ssh/data.txt',
|
||||
]
|
||||
for (const cmd of generics) await show(cmd, cmd)
|
||||
|
||||
await ws.execute('cd /ssh/')
|
||||
await show('cd /ssh/ && ls', 'ls')
|
||||
await show('pwd', 'pwd')
|
||||
|
||||
await ws.execute('cd /ssh/docs')
|
||||
await show('cd /ssh/docs && cat guide.txt', 'cat guide.txt')
|
||||
await ws.execute('cd ..')
|
||||
await show('cd .. && ls', 'ls')
|
||||
|
||||
await ws.execute('echo hello > /ssh/test.txt')
|
||||
await show('cat /ssh/test.txt', 'cat /ssh/test.txt')
|
||||
|
||||
await ws.execute('cp /ssh/test.txt /ssh/test2.txt')
|
||||
await show('cp /ssh/test.txt /ssh/test2.txt', 'ls /ssh/')
|
||||
|
||||
await ws.execute('mv /ssh/test2.txt /ssh/renamed.txt')
|
||||
await show('mv /ssh/test2.txt /ssh/renamed.txt', 'ls /ssh/')
|
||||
|
||||
await ws.execute('mkdir /ssh/subdir')
|
||||
await ws.execute('echo world > /ssh/subdir/nested.txt')
|
||||
await show('tree /ssh/', 'tree /ssh/')
|
||||
|
||||
await ws.execute('rm /ssh/renamed.txt')
|
||||
await ws.execute('rm -r /ssh/subdir')
|
||||
await ws.execute('rm /ssh/test.txt')
|
||||
await show('final ls /ssh/', 'ls /ssh/')
|
||||
} finally {
|
||||
await ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err: unknown) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import dotenv from "dotenv";
|
||||
import {
|
||||
Mount,
|
||||
MountMode,
|
||||
SSHResource,
|
||||
type SSHConfig,
|
||||
Workspace,
|
||||
} from "@struktoai/mirage-node";
|
||||
|
||||
const __HERE = fileURLToPath(new URL(".", import.meta.url));
|
||||
dotenv.config({ path: resolve(__HERE, "../../../.env.development") });
|
||||
|
||||
function buildConfig(): SSHConfig {
|
||||
const host = process.env.SSH_HOST;
|
||||
const username = process.env.SSH_USER;
|
||||
if (host === undefined || host === "")
|
||||
throw new Error("SSH_HOST env var is required");
|
||||
if (username === undefined || username === "")
|
||||
throw new Error("SSH_USER env var is required");
|
||||
const cfg: SSHConfig = { host, username };
|
||||
if (process.env.SSH_KEY !== undefined && process.env.SSH_KEY !== "") {
|
||||
cfg.identityFile = process.env.SSH_KEY;
|
||||
}
|
||||
if (process.env.SSH_PASSWORD !== undefined)
|
||||
cfg.password = process.env.SSH_PASSWORD;
|
||||
if (process.env.SSH_PASSPHRASE !== undefined)
|
||||
cfg.passphrase = process.env.SSH_PASSPHRASE;
|
||||
if (process.env.SSH_PORT !== undefined && process.env.SSH_PORT !== "") {
|
||||
cfg.port = Number(process.env.SSH_PORT);
|
||||
}
|
||||
if (process.env.SSH_ROOT !== undefined && process.env.SSH_ROOT !== "") {
|
||||
cfg.root = process.env.SSH_ROOT;
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const resource = new SSHResource(buildConfig());
|
||||
const ws = new Workspace({
|
||||
"/ssh": new Mount(resource, { mode: MountMode.READ, fuse: true }),
|
||||
});
|
||||
await ws.fuseReady();
|
||||
const mp = ws.fuseMountpoint as string;
|
||||
try {
|
||||
console.log(`=== FUSE MODE: mounted at ${mp} ===\n`);
|
||||
|
||||
console.log("--- readdir() /ssh ---");
|
||||
for (const r of await readdir(mp)) console.log(` ${r}`);
|
||||
|
||||
const probe = `${mp}/etc/hostname`;
|
||||
console.log(`\n--- readFile() /ssh/etc/hostname ---`);
|
||||
try {
|
||||
const data = await readFile(probe, "utf-8");
|
||||
console.log(` ${data.trim().slice(0, 200)}`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.log(` (skipped: ${msg})`);
|
||||
}
|
||||
|
||||
console.log("\n" + "=".repeat(72));
|
||||
console.log(`>>> FUSE mounted at: ${mp}`);
|
||||
console.log(`>>> SSH root: ${mp}`);
|
||||
console.log("=".repeat(72));
|
||||
console.log("\n>>> Open in Finder:");
|
||||
console.log(`>>> open ${mp}`);
|
||||
console.log(">>> Or in another terminal:");
|
||||
console.log(`>>> ls ${mp}/`);
|
||||
console.log(`>>> cat ${mp}/etc/hostname`);
|
||||
console.log(`>>> tree ${mp}/etc | head`);
|
||||
console.log("\n>>> Press Enter to unmount and exit...");
|
||||
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
await rl.question("");
|
||||
rl.close();
|
||||
|
||||
const records = ws.records;
|
||||
const total = records.reduce((acc, r) => acc + (r.bytes ?? 0), 0);
|
||||
console.log(
|
||||
`\nStats: ${String(records.length)} ops, ${String(total)} bytes transferred`,
|
||||
);
|
||||
} finally {
|
||||
await ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err: unknown) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
import { resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import dotenv from 'dotenv'
|
||||
import { MountMode, patchNodeFs, SSHResource, type SSHConfig, Workspace } from '@struktoai/mirage-node'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const fs = require('fs') as typeof import('fs')
|
||||
|
||||
const __HERE = fileURLToPath(new URL('.', import.meta.url))
|
||||
dotenv.config({ path: resolve(__HERE, '../../../.env.development') })
|
||||
|
||||
function buildConfig(): SSHConfig {
|
||||
const host = process.env.SSH_HOST
|
||||
const username = process.env.SSH_USER
|
||||
if (host === undefined || host === '') throw new Error('SSH_HOST env var is required')
|
||||
if (username === undefined || username === '') throw new Error('SSH_USER env var is required')
|
||||
const cfg: SSHConfig = { host, username }
|
||||
if (process.env.SSH_KEY !== undefined && process.env.SSH_KEY !== '') {
|
||||
cfg.identityFile = process.env.SSH_KEY
|
||||
}
|
||||
if (process.env.SSH_PASSWORD !== undefined) cfg.password = process.env.SSH_PASSWORD
|
||||
if (process.env.SSH_PASSPHRASE !== undefined) cfg.passphrase = process.env.SSH_PASSPHRASE
|
||||
if (process.env.SSH_PORT !== undefined && process.env.SSH_PORT !== '') {
|
||||
cfg.port = Number(process.env.SSH_PORT)
|
||||
}
|
||||
if (process.env.SSH_ROOT !== undefined && process.env.SSH_ROOT !== '') {
|
||||
cfg.root = process.env.SSH_ROOT
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const MOUNT = '/ssh'
|
||||
const resource = new SSHResource(buildConfig())
|
||||
const ws = new Workspace({ [MOUNT]: resource }, { mode: MountMode.WRITE })
|
||||
const restore = patchNodeFs(ws)
|
||||
try {
|
||||
console.log(`=== VFS MODE: mounted at ${MOUNT} (in-process fs patch) ===\n`)
|
||||
|
||||
console.log(`--- fs.readdir() ${MOUNT} ---`)
|
||||
for (const r of await fs.promises.readdir(MOUNT)) console.log(` ${r}`)
|
||||
|
||||
const probe = `${MOUNT}/etc/hostname`
|
||||
console.log(`\n--- fs.stat() ${probe} ---`)
|
||||
try {
|
||||
const st = await fs.promises.stat(probe)
|
||||
console.log(` size=${String(st.size)}`)
|
||||
const data = await fs.promises.readFile(probe, 'utf-8')
|
||||
console.log(`\n--- fs.readFile() ${probe} ---`)
|
||||
console.log(` ${data.trim().slice(0, 200)}`)
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
console.log(` (skipped: ${msg})`)
|
||||
}
|
||||
|
||||
const records = ws.records
|
||||
const total = records.reduce((acc, r) => acc + (r.bytes ?? 0), 0)
|
||||
console.log(`\nStats: ${String(records.length)} ops, ${String(total)} bytes transferred`)
|
||||
} finally {
|
||||
restore()
|
||||
await ws.close()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err: unknown) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user