@miraj181/ipingyou 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 skmirajulislam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # πŸ”— iPingYou β€” SecureLink CLI
2
+
3
+ Secure peer-to-peer remote access via SSH & Cloudflare Tunnels. A Node.js CLI tool that lets two machines establish an encrypted SSH connection through Cloudflare's network, coordinated by a lightweight broker.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ Host Machine Broker (Express.js) Client Machine
9
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” POST β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” GET β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
10
+ β”‚ cloudflared │──/register──▢│ In-Memory Map │◀─/resolve──│ ipingyou β”‚
11
+ β”‚ tunnel :22 β”‚ {uid,url} β”‚ AES-256-CBC β”‚ /:uid β”‚ connect β”‚
12
+ β”‚ β”‚ β”‚ 1hr TTL expiry β”‚ β”‚ β”‚
13
+ β”‚ SSH daemon │◀─────────────│──────────────────│─────────────│ SSH via β”‚
14
+ β”‚ β”‚ cloudflared β”‚ β”‚ ProxyCmd β”‚ ProxyCmd β”‚
15
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ TCP proxy β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
16
+ ```
17
+
18
+ ### Flow
19
+ 1. **Host** starts `ipingyou host` β†’ generates an 8-char UID, spins up `cloudflared` tunnel, registers with broker
20
+ 2. **Host** shares UID with the client (verbally, chat, etc.)
21
+ 3. **Client** runs `ipingyou connect` β†’ enters UID, broker resolves it to tunnel URL β†’ SSH connects via cloudflared proxy
22
+ 4. On `Ctrl+C`, all spawned processes are killed via `tree-kill` and the UID is revoked from the broker
23
+
24
+ ## Quick Start
25
+
26
+ ### 1. Start the Broker (self-hosted or Render)
27
+
28
+ ```bash
29
+ cd ipingyou
30
+ cp .env.example .env
31
+ npm install
32
+ npm run broker
33
+ # Broker: http://localhost:4000
34
+ ```
35
+
36
+ ### 2. Host Mode (Machine being accessed)
37
+
38
+ ```bash
39
+ npx ipingyou host
40
+ # or interactively:
41
+ npx ipingyou
42
+ ```
43
+
44
+ ### 3. Client Mode (Machine accessing)
45
+
46
+ ```bash
47
+ npx ipingyou connect
48
+ # or interactively:
49
+ npx ipingyou
50
+ ```
51
+
52
+ ## CLI Commands
53
+
54
+ ```
55
+ ipingyou Interactive mode (prompts for host/client)
56
+ ipingyou host Start as host β€” allow remote access
57
+ ipingyou connect Start as client β€” access a remote machine
58
+ ipingyou broker Start the central broker server
59
+ ipingyou broker -p 5000 Start broker on custom port
60
+ ```
61
+
62
+ ## Prerequisites
63
+
64
+ | Tool | Required | Install |
65
+ |------|----------|---------|
66
+ | Node.js β‰₯18 | βœ… | [nodejs.org](https://nodejs.org) |
67
+ | `ssh` | βœ… | Ships with macOS/Linux; `winget install Microsoft.OpenSSH.Client` on Windows |
68
+ | `cloudflared` | βœ… | `brew install cloudflared` / [download](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) |
69
+
70
+ The CLI auto-detects your OS and will attempt to install missing dependencies on Linux (apt/pacman) and guide you on macOS/Windows.
71
+
72
+ ## Project Structure
73
+
74
+ ```
75
+ ipingyou/
76
+ β”œβ”€β”€ src/
77
+ β”‚ β”œβ”€β”€ cli.js ← Main CLI entry (npx-ready, shebang)
78
+ β”‚ β”œβ”€β”€ server.js ← Central Broker (Express.js)
79
+ β”‚ β”œβ”€β”€ lib/
80
+ β”‚ β”‚ β”œβ”€β”€ cleanup.js ← Graceful shutdown + tree-kill
81
+ β”‚ β”‚ β”œβ”€β”€ crypto.js ← AES-256-CBC encrypt/decrypt
82
+ β”‚ β”‚ β”œβ”€β”€ platform.js ← OS detection + dependency check
83
+ β”‚ β”‚ └── uid.js ← Random 8-char UID generator (nanoid)
84
+ β”‚ └── modes/
85
+ β”‚ β”œβ”€β”€ host.js ← Host mode logic
86
+ β”‚ └── client.js ← Client mode logic
87
+ β”œβ”€β”€ .env.example
88
+ β”œβ”€β”€ .gitignore
89
+ β”œβ”€β”€ package.json
90
+ β”œβ”€β”€ render.yaml ← One-click Render deploy
91
+ └── README.md
92
+ ```
93
+
94
+ ## Security
95
+
96
+ - **AES-256-CBC** β€” Tunnel URLs are encrypted at rest in the broker's memory
97
+ - **Random UIDs** β€” 8-char nanoid, not hardware-based; session dies = door locked forever
98
+ - **1-hour TTL** β€” Broker auto-expires entries; no stale data
99
+ - **Auto-revoke** β€” On `Ctrl+C`, UID is deleted from broker before exit
100
+ - **tree-kill** β€” All child processes (cloudflared, SSH) are recursively killed on shutdown
101
+ - **No persistence** β€” Broker uses in-memory Map only; server restart = clean slate
102
+
103
+ ## Deploy Broker to Render (Free)
104
+
105
+ 1. Push this repo to GitHub
106
+ 2. Go to [render.com/new](https://render.com/new)
107
+ 3. Click **"New Web Service"** β†’ connect your repo
108
+ 4. Render auto-detects `render.yaml` β†’ click **Deploy**
109
+ 5. Set `SECRET_KEY` in Render Dashboard β†’ Environment
110
+ 6. Set `BROKER_URL` in your local `.env` to your Render URL
111
+
112
+ ## Environment Variables
113
+
114
+ | Variable | Default | Description |
115
+ |----------|---------|-------------|
116
+ | `SECRET_KEY` | Dev key | AES-256 hex key (64 chars) |
117
+ | `BROKER_URL` | `http://localhost:4000` | Broker endpoint for CLI |
118
+ | `BROKER_PORT` | `4000` | Port for broker server |
119
+
120
+ ## Differences from remote-penitrator
121
+
122
+ | Aspect | remote-penitrator | iPingYou |
123
+ |--------|-------------------|----------|
124
+ | Interface | Shell scripts + web dashboard | Interactive Node.js CLI |
125
+ | Direction | One-way (runner β†’ C2) | Peer-to-peer (host ↔ client) |
126
+ | UID System | Hardcoded PC IDs | Random per-session UIDs |
127
+ | Process Mgmt | Manual | tree-kill auto-cleanup |
128
+ | Install | Copy scripts | `npx ipingyou` |
129
+ | Modes | Reporter only | Host + Client + Broker |
130
+
131
+ ## License
132
+
133
+ MIT
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@miraj181/ipingyou",
3
+ "version": "1.0.0",
4
+ "description": "SecureLink-CLI β€” Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
5
+ "main": "src/cli.js",
6
+ "bin": {
7
+ "ipingyou": "src/cli.js",
8
+ "securelink": "src/cli.js"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node src/cli.js",
17
+ "broker": "node src/server.js",
18
+ "dev:broker": "nodemon src/server.js",
19
+ "dev:cli": "node src/cli.js",
20
+ "prepublishOnly": "node -e \"require('fs').accessSync('LICENSE')\" && node src/cli.js --help"
21
+ },
22
+ "keywords": [
23
+ "ssh",
24
+ "remote-access",
25
+ "cloudflare-tunnel",
26
+ "cloudflared",
27
+ "cli",
28
+ "peer-to-peer",
29
+ "securelink",
30
+ "tunnel",
31
+ "remote-desktop",
32
+ "p2p"
33
+ ],
34
+ "author": "skmirajulislam",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/skmirajulislam/ipingyou.git"
39
+ },
40
+ "homepage": "https://github.com/skmirajulislam/ipingyou#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/skmirajulislam/ipingyou/issues"
43
+ },
44
+ "dependencies": {
45
+ "chalk": "^5.3.0",
46
+ "commander": "^12.1.0",
47
+ "dotenv": "^16.4.5",
48
+ "execa": "^9.5.2",
49
+ "express": "^4.21.0",
50
+ "inquirer": "^12.3.2",
51
+ "nanoid": "^5.0.9",
52
+ "ora": "^8.1.1",
53
+ "tree-kill": "^1.2.2"
54
+ },
55
+ "devDependencies": {
56
+ "nodemon": "^3.1.4"
57
+ },
58
+ "engines": {
59
+ "node": ">=18.0.0"
60
+ },
61
+ "type": "module"
62
+ }
package/src/cli.js ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ============================================================
5
+ * SecureLink-CLI (ipingyou)
6
+ * ============================================================
7
+ * Secure peer-to-peer remote access via SSH & Cloudflare
8
+ * Tunnels. Designed to run via npx or as a global install.
9
+ *
10
+ * Usage:
11
+ * npx ipingyou β€” Interactive mode
12
+ * npx ipingyou host β€” Start as host directly
13
+ * npx ipingyou connect β€” Start as client directly
14
+ * npx ipingyou broker β€” Start the central broker
15
+ *
16
+ * Security:
17
+ * All tunnel URLs are AES-256-CBC encrypted on the CLI side.
18
+ * The broker is a zero-knowledge relay β€” it NEVER sees plaintext.
19
+ * ============================================================
20
+ */
21
+
22
+ import 'dotenv/config';
23
+ import { Command } from 'commander';
24
+ import inquirer from 'inquirer';
25
+ import chalk from 'chalk';
26
+
27
+ import { detectOS, checkDependencies } from './lib/platform.js';
28
+ import { installShutdownHandlers } from './lib/cleanup.js';
29
+ import { startHostMode } from './modes/host.js';
30
+ import { startClientMode } from './modes/client.js';
31
+
32
+ // ─── ASCII Banner ────────────────────────────────────────────
33
+ function showBanner() {
34
+ console.log('');
35
+ console.log(chalk.cyan.bold(' ╔═══════════════════════════════════════════╗'));
36
+ console.log(chalk.cyan.bold(' β•‘ β•‘'));
37
+ console.log(chalk.cyan.bold(' β•‘') + chalk.white.bold(' πŸ”— iPingYou β€” SecureLink CLI ') + chalk.cyan.bold(' β•‘'));
38
+ console.log(chalk.cyan.bold(' β•‘ β•‘'));
39
+ console.log(chalk.cyan.bold(' β•‘') + chalk.dim(' Secure P2P Remote Access via SSH + ') + chalk.cyan.bold(' β•‘'));
40
+ console.log(chalk.cyan.bold(' β•‘') + chalk.dim(' Cloudflare Tunnels | AES-256-CBC ') + chalk.cyan.bold(' β•‘'));
41
+ console.log(chalk.cyan.bold(' β•‘ β•‘'));
42
+ console.log(chalk.cyan.bold(' β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•'));
43
+ console.log('');
44
+ }
45
+
46
+ function showSystemInfo() {
47
+ const osInfo = detectOS();
48
+ const platform = osInfo.isLinux ? '🐧 Linux' : osInfo.isMac ? '🍎 macOS' : 'πŸͺŸ Windows';
49
+ console.log(chalk.dim(` ${platform} | ${osInfo.arch} | ${osInfo.hostname} | Node ${process.version}`));
50
+ console.log('');
51
+ }
52
+
53
+ function showRichHelp() {
54
+ console.log(chalk.bold.yellow(' ✨ Welcome to iPingYou SecureLink CLI! ✨'));
55
+ console.log(chalk.dim(' ───────────────────────────────────────────────────────'));
56
+ console.log(chalk.cyan(' A zero-knowledge peer-to-peer remote access tool.'));
57
+ console.log(chalk.cyan(' Securely share your local SSH terminal with anyone over the internet.'));
58
+ console.log('');
59
+
60
+ console.log(chalk.bold.white(' πŸš€ Usage Modes:'));
61
+ console.log(` ${chalk.green('host')} : Generates a secure session UID and exposes your local machine.`);
62
+ console.log(` ${chalk.blue('connect')} : Prompts for a UID to connect to a remote host.`);
63
+ console.log(` ${chalk.dim('Supports Interactive SSH Shell & SCP File Transfers')}`);
64
+ console.log(` ${chalk.yellow('broker')} : Start your own relay server (for self-hosting).`);
65
+ console.log('');
66
+
67
+ console.log(chalk.bold.white(' πŸ”’ Security Architecture:'));
68
+ console.log(` β€’ Cloudflare Tunnels punch through NAT/Firewalls securely.`);
69
+ console.log(` β€’ ${chalk.green('End-to-End Encryption')}: Tunnel URLs are AES-256 encrypted locally.`);
70
+ console.log(` β€’ The Broker never sees your plaintext URL, only ciphertext.`);
71
+ console.log('');
72
+
73
+ console.log(chalk.bold.white(' πŸ’‘ Examples:'));
74
+ console.log(` $ npx ipingyou ${chalk.dim('# Interactive wizard (Recommended)')}`);
75
+ console.log(` $ npx ipingyou host ${chalk.dim('# Quick start as Host')}`);
76
+ console.log(` $ npx ipingyou connect ${chalk.dim('# Quick start as Client')}`);
77
+ console.log(` $ npx ipingyou broker ${chalk.dim('# Start Relay')}`);
78
+ console.log('');
79
+ }
80
+
81
+ /**
82
+ * Fatal error handler β€” logs and exits with code 1.
83
+ * @param {string} context β€” which command/mode failed
84
+ * @param {Error} err β€” the error object
85
+ */
86
+ function fatal(context, err) {
87
+ console.error('');
88
+ console.error(chalk.red(` ❌ FATAL [${context}]`));
89
+ console.error(chalk.red(` ${err.message}`));
90
+ if (err.stack) {
91
+ const stackLines = err.stack.split('\n').slice(1, 4);
92
+ stackLines.forEach(line => console.error(chalk.dim(` ${line.trim()}`)));
93
+ }
94
+ console.error('');
95
+ process.exit(1);
96
+ }
97
+
98
+ // ─── Interactive Mode Selection ──────────────────────────────
99
+ async function interactiveMode() {
100
+ showBanner();
101
+ showSystemInfo();
102
+
103
+ // Check dependencies first
104
+ const deps = await checkDependencies();
105
+
106
+ if (!deps.ssh || !deps.cloudflared) {
107
+ const { proceed } = await inquirer.prompt([
108
+ {
109
+ type: 'confirm',
110
+ name: 'proceed',
111
+ message: 'Some dependencies are missing. Continue anyway?',
112
+ default: false,
113
+ },
114
+ ]);
115
+ if (!proceed) {
116
+ console.log(chalk.dim(' Install the missing tools and try again.'));
117
+ process.exit(0);
118
+ }
119
+ }
120
+
121
+ // Mode selection
122
+ const { mode } = await inquirer.prompt([
123
+ {
124
+ type: 'list',
125
+ name: 'mode',
126
+ message: 'What would you like to do?',
127
+ choices: [
128
+ {
129
+ name: `${chalk.green('πŸ›‘οΈ Allow Remote Access')} ${chalk.dim('β€” Let someone connect to this machine')}`,
130
+ value: 'host',
131
+ },
132
+ {
133
+ name: `${chalk.blue('🌐 Access a Remote Machine')} ${chalk.dim('β€” Connect to a host via their UID (SSH/SCP)')}`,
134
+ value: 'client',
135
+ },
136
+ new inquirer.Separator(),
137
+ {
138
+ name: `${chalk.yellow('πŸ“‘ Start Broker Server')} ${chalk.dim('β€” Run the central relay (for self-hosting)')}`,
139
+ value: 'broker',
140
+ },
141
+ {
142
+ name: `${chalk.magenta('πŸ“– Help / Information')} ${chalk.dim('β€” Learn how iPingYou works')}`,
143
+ value: 'help',
144
+ },
145
+ ],
146
+ },
147
+ ]);
148
+
149
+ switch (mode) {
150
+ case 'host':
151
+ await startHostMode();
152
+ break;
153
+ case 'client':
154
+ await startClientMode();
155
+ break;
156
+ case 'broker':
157
+ await startBroker();
158
+ break;
159
+ case 'help':
160
+ showRichHelp();
161
+ break;
162
+ }
163
+ }
164
+
165
+ // ─── Broker Start ────────────────────────────────────────────
166
+ async function startBroker() {
167
+ console.log(chalk.cyan(' Starting broker server...'));
168
+ console.log('');
169
+ // Dynamically import the server (it self-starts on import)
170
+ await import('./server.js');
171
+ }
172
+
173
+ // ─── Commander Setup ─────────────────────────────────────────
174
+ const program = new Command();
175
+
176
+ program
177
+ .name('ipingyou')
178
+ .description('SecureLink-CLI β€” Secure P2P remote access via SSH & Cloudflare Tunnels')
179
+ .version('1.0.0')
180
+ .addHelpText('beforeAll', () => {
181
+ showBanner();
182
+ showRichHelp();
183
+ return '';
184
+ });
185
+
186
+ program
187
+ .command('host')
188
+ .description('Start host mode β€” allow remote access to this machine')
189
+ .action(async () => {
190
+ try {
191
+ showBanner();
192
+ showSystemInfo();
193
+ installShutdownHandlers();
194
+ await checkDependencies();
195
+ await startHostMode();
196
+ } catch (err) {
197
+ fatal('host', err);
198
+ }
199
+ });
200
+
201
+ program
202
+ .command('connect')
203
+ .description('Connect to a remote machine via its UID (SSH or SCP)')
204
+ .option('-u, --uid <uid>', 'The remote host UID')
205
+ .action(async () => {
206
+ try {
207
+ showBanner();
208
+ showSystemInfo();
209
+ installShutdownHandlers();
210
+ await checkDependencies();
211
+ await startClientMode();
212
+ } catch (err) {
213
+ fatal('connect', err);
214
+ }
215
+ });
216
+
217
+ program
218
+ .command('broker')
219
+ .description('Start the central broker server')
220
+ .option('-p, --port <port>', 'Port to listen on', '4000')
221
+ .action(async (opts) => {
222
+ try {
223
+ if (opts.port) process.env.BROKER_PORT = opts.port;
224
+ showBanner();
225
+ await startBroker();
226
+ } catch (err) {
227
+ fatal('broker', err);
228
+ }
229
+ });
230
+
231
+ // ─── Default: interactive mode ──────────────────────────────
232
+ program.action(async () => {
233
+ try {
234
+ installShutdownHandlers();
235
+ await interactiveMode();
236
+ } catch (err) {
237
+ fatal('interactive', err);
238
+ }
239
+ });
240
+
241
+ program.parse();
@@ -0,0 +1,226 @@
1
+ /**
2
+ * ============================================================
3
+ * CLI Animations & Visual Effects
4
+ * ============================================================
5
+ * Custom spinners, progress bars, and animated text effects
6
+ * for a premium CLI experience.
7
+ * ============================================================
8
+ */
9
+
10
+ import chalk from 'chalk';
11
+ import ora from 'ora';
12
+
13
+ // ─── Custom Spinner Frames ────────────────────────────────────
14
+
15
+ /** Cyber-themed spinner for encryption operations */
16
+ export const cryptoSpinner = {
17
+ interval: 100,
18
+ frames: [
19
+ chalk.green('πŸ” β–°β–±β–±β–±β–±β–±β–±'),
20
+ chalk.green('πŸ” β–°β–°β–±β–±β–±β–±β–±'),
21
+ chalk.green('πŸ” β–°β–°β–°β–±β–±β–±β–±'),
22
+ chalk.green('πŸ” β–°β–°β–°β–°β–±β–±β–±'),
23
+ chalk.green('πŸ” β–°β–°β–°β–°β–°β–±β–±'),
24
+ chalk.green('πŸ” β–°β–°β–°β–°β–°β–°β–±'),
25
+ chalk.green('πŸ” β–°β–°β–°β–°β–°β–°β–°'),
26
+ chalk.cyan('πŸ”“ β–°β–°β–°β–°β–°β–°β–°'),
27
+ ],
28
+ };
29
+
30
+ /** Network pulse for connection operations */
31
+ export const networkSpinner = {
32
+ interval: 120,
33
+ frames: [
34
+ chalk.cyan('πŸ“‘ Β· '),
35
+ chalk.cyan('πŸ“‘ Β·Β· '),
36
+ chalk.cyan('πŸ“‘ Β·Β·Β· '),
37
+ chalk.cyan('πŸ“‘ Β·Β·Β·Β· '),
38
+ chalk.cyan('πŸ“‘ Β·Β·Β·Β·Β· '),
39
+ chalk.cyan('πŸ“‘ Β·Β·Β·Β·Β·Β·'),
40
+ chalk.blue('πŸ“‘ ─────→'),
41
+ chalk.green('πŸ“‘ ═════►'),
42
+ ],
43
+ };
44
+
45
+ /** Tunnel establishment animation */
46
+ export const tunnelSpinner = {
47
+ interval: 150,
48
+ frames: [
49
+ chalk.yellow('🌐 β•Έ '),
50
+ chalk.yellow('🌐 ═╸ '),
51
+ chalk.yellow('🌐 ══╸ '),
52
+ chalk.cyan('🌐 ═══╸ '),
53
+ chalk.cyan('🌐 ════╸ '),
54
+ chalk.blue('🌐 ═════╸ '),
55
+ chalk.blue('🌐 ══════╸ '),
56
+ chalk.green('🌐 ═══════►'),
57
+ ],
58
+ };
59
+
60
+ /** File transfer animation */
61
+ export const fileTransferSpinner = {
62
+ interval: 100,
63
+ frames: [
64
+ chalk.yellow('πŸ“¦ [ ] 0% '),
65
+ chalk.yellow('πŸ“¦ [β–ˆ ] 10% '),
66
+ chalk.yellow('πŸ“¦ [β–ˆβ–ˆ ] 20% '),
67
+ chalk.cyan( 'πŸ“¦ [β–ˆβ–ˆβ–ˆ ] 30% '),
68
+ chalk.cyan( 'πŸ“¦ [β–ˆβ–ˆβ–ˆβ–ˆ ] 40% '),
69
+ chalk.cyan( 'πŸ“¦ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ ] 50% '),
70
+ chalk.blue( 'πŸ“¦ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ ] 60% '),
71
+ chalk.blue( 'πŸ“¦ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ ] 70% '),
72
+ chalk.green( 'πŸ“¦ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ ] 80% '),
73
+ chalk.green( 'πŸ“¦ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ ] 90% '),
74
+ chalk.green( 'πŸ“¦ [β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ] 100%'),
75
+ ],
76
+ };
77
+
78
+ /** SSH handshake animation */
79
+ export const sshSpinner = {
80
+ interval: 130,
81
+ frames: [
82
+ chalk.yellow('πŸ”‘ Β· '),
83
+ chalk.yellow('πŸ”‘ Β·Β· '),
84
+ chalk.cyan( 'πŸ”‘ Β·Β·Β· '),
85
+ chalk.cyan( 'πŸ”‘ Β·Β·Β·Β· '),
86
+ chalk.blue( 'πŸ”‘ Β·Β·Β·Β·Β· '),
87
+ chalk.blue( 'πŸ”‘ Β·Β·Β·Β·Β·Β· '),
88
+ chalk.green( 'πŸ”‘ Β·Β·Β·Β·Β·Β·Β· '),
89
+ chalk.green( 'πŸ”‘ βœ“ verified'),
90
+ ],
91
+ };
92
+
93
+ // ─── Animated Spinner Helpers ─────────────────────────────────
94
+
95
+ /**
96
+ * Create a styled ora spinner with a custom animation.
97
+ * @param {string} text β€” spinner label
98
+ * @param {object} spinnerDef β€” { interval, frames }
99
+ * @returns {import('ora').Ora}
100
+ */
101
+ export function createSpinner(text, spinnerDef) {
102
+ return ora({ text, spinner: spinnerDef });
103
+ }
104
+
105
+ /**
106
+ * Animated multi-step progress display.
107
+ * Runs through each step with a delay and animation.
108
+ *
109
+ * @param {Array<{text: string, duration: number, spinner?: object}>} steps
110
+ */
111
+ export async function animatedSteps(steps) {
112
+ for (const step of steps) {
113
+ const spinnerDef = step.spinner || networkSpinner;
114
+ const spinner = ora({ text: step.text, spinner: spinnerDef }).start();
115
+ await sleep(step.duration);
116
+ spinner.succeed(step.text);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Display an animated connection trace.
122
+ * Shows a visual path from local β†’ tunnel β†’ remote.
123
+ */
124
+ export async function showConnectionTrace(localLabel, remoteLabel) {
125
+ const width = 40;
126
+ const steps = [
127
+ { pos: 0, char: 'β•Έ', color: chalk.yellow },
128
+ { pos: 5, char: '══╸', color: chalk.yellow },
129
+ { pos: 10, char: '════╸', color: chalk.cyan },
130
+ { pos: 15, char: '══════╸', color: chalk.cyan },
131
+ { pos: 20, char: '════════╸', color: chalk.blue },
132
+ { pos: 25, char: '══════════╸', color: chalk.blue },
133
+ { pos: 30, char: '════════════╸', color: chalk.green },
134
+ { pos: 35, char: '══════════════►', color: chalk.green },
135
+ ];
136
+
137
+ console.log('');
138
+ console.log(chalk.dim(' β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”'));
139
+ console.log(chalk.dim(' β”‚') + ` ${chalk.cyan(localLabel)} β†’ ${chalk.magenta('☁ Cloudflare')} β†’ ${chalk.green(remoteLabel)} ` + chalk.dim('β”‚'));
140
+ console.log(chalk.dim(' β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜'));
141
+
142
+ for (const step of steps) {
143
+ const line = ` ${step.color(step.char)}`;
144
+ process.stdout.write(`\r${line}`);
145
+ await sleep(120);
146
+ }
147
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
148
+ }
149
+
150
+ /**
151
+ * Show a pulsing "LIVE" indicator.
152
+ * @param {string} message
153
+ * @param {number} pulses β€” how many times to pulse
154
+ */
155
+ export async function pulseText(message, pulses = 3) {
156
+ const colors = [chalk.red.bold, chalk.red, chalk.yellow, chalk.green, chalk.green.bold];
157
+ for (let i = 0; i < pulses; i++) {
158
+ for (const colorFn of colors) {
159
+ process.stdout.write(`\r ${colorFn('●')} ${message}`);
160
+ await sleep(100);
161
+ }
162
+ }
163
+ process.stdout.write(`\r ${chalk.green.bold('●')} ${message}\n`);
164
+ }
165
+
166
+ /**
167
+ * Typing effect β€” prints text character by character.
168
+ * @param {string} text
169
+ * @param {number} charDelay β€” ms per character
170
+ */
171
+ export async function typeText(text, charDelay = 25) {
172
+ for (const char of text) {
173
+ process.stdout.write(char);
174
+ await sleep(charDelay);
175
+ }
176
+ console.log('');
177
+ }
178
+
179
+ /**
180
+ * Draw a gradient progress bar.
181
+ * @param {number} percent β€” 0-100
182
+ * @param {number} width β€” bar width in chars
183
+ */
184
+ export function progressBar(percent, width = 30) {
185
+ const filled = Math.round((percent / 100) * width);
186
+ const empty = width - filled;
187
+ const filledStr = 'β–ˆ'.repeat(filled);
188
+ const emptyStr = 'β–‘'.repeat(empty);
189
+
190
+ let coloredFilled;
191
+ if (percent < 30) coloredFilled = chalk.red(filledStr);
192
+ else if (percent < 60) coloredFilled = chalk.yellow(filledStr);
193
+ else if (percent < 90) coloredFilled = chalk.cyan(filledStr);
194
+ else coloredFilled = chalk.green(filledStr);
195
+
196
+ return `${coloredFilled}${chalk.dim(emptyStr)} ${chalk.white.bold(`${percent}%`)}`;
197
+ }
198
+
199
+ /**
200
+ * Simulate a file transfer progress animation.
201
+ * @param {string} filename
202
+ * @param {string} direction β€” 'upload' or 'download'
203
+ * @param {number} durationMs β€” total animation time
204
+ */
205
+ export async function simulateTransferProgress(filename, direction, durationMs = 2000) {
206
+ const icon = direction === 'upload' ? 'πŸ“€' : 'πŸ“₯';
207
+ const verb = direction === 'upload' ? 'Sending' : 'Receiving';
208
+ const steps = 20;
209
+ const stepDelay = durationMs / steps;
210
+
211
+ console.log('');
212
+ for (let i = 0; i <= steps; i++) {
213
+ const pct = Math.round((i / steps) * 100);
214
+ const bar = progressBar(pct);
215
+ process.stdout.write(`\r ${icon} ${verb} ${chalk.white.bold(filename)} ${bar}`);
216
+ await sleep(stepDelay);
217
+ }
218
+ console.log('');
219
+ console.log(` ${chalk.green('βœ“')} ${verb} complete: ${chalk.cyan(filename)}`);
220
+ }
221
+
222
+ // ─── Utility ──────────────────────────────────────────────────
223
+
224
+ function sleep(ms) {
225
+ return new Promise(resolve => setTimeout(resolve, ms));
226
+ }