nport 2.0.7 → 2.1.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/CHANGELOG.md +156 -0
- package/README.md +42 -32
- package/package.json +18 -7
- package/scripts/postinstall.js +25 -0
- package/index.js +0 -110
- package/src/analytics.js +0 -265
- package/src/api.js +0 -104
- package/src/args.js +0 -122
- package/src/bin-manager.js +0 -379
- package/src/binary.js +0 -126
- package/src/config-manager.js +0 -139
- package/src/config.js +0 -88
- package/src/lang.js +0 -293
- package/src/state.js +0 -115
- package/src/tunnel.js +0 -116
- package/src/ui.js +0 -103
- package/src/version.js +0 -56
package/src/analytics.js
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// Firebase Analytics for CLI
|
|
3
|
-
// Using Google Analytics 4 Measurement Protocol
|
|
4
|
-
// ============================================================================
|
|
5
|
-
|
|
6
|
-
import axios from "axios";
|
|
7
|
-
import { createHash, randomUUID } from "crypto";
|
|
8
|
-
import os from "os";
|
|
9
|
-
import fs from "fs";
|
|
10
|
-
import path from "path";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
12
|
-
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
-
const __dirname = path.dirname(path.dirname(__filename));
|
|
15
|
-
|
|
16
|
-
// Firebase/GA4 Configuration (from website/home.html)
|
|
17
|
-
// Full Firebase config for reference (if needed for future features)
|
|
18
|
-
const FIREBASE_WEB_CONFIG = {
|
|
19
|
-
apiKey: "AIzaSyArRxHZJUt4o2RxiLqX1yDSkuUd6ZFy45I",
|
|
20
|
-
authDomain: "nport-link.firebaseapp.com",
|
|
21
|
-
projectId: "nport-link",
|
|
22
|
-
storageBucket: "nport-link.firebasestorage.app",
|
|
23
|
-
messagingSenderId: "515584605320",
|
|
24
|
-
appId: "1:515584605320:web:88daabc8d77146c6e7f33d",
|
|
25
|
-
measurementId: "G-8MYXZL6PGD"
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// Analytics-specific config (for GA4 Measurement Protocol)
|
|
29
|
-
const FIREBASE_CONFIG = {
|
|
30
|
-
measurementId: FIREBASE_WEB_CONFIG.measurementId,
|
|
31
|
-
apiSecret: process.env.NPORT_ANALYTICS_SECRET || "YOUR_API_SECRET_HERE", // Get from Firebase Console
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Analytics Configuration
|
|
35
|
-
const ANALYTICS_CONFIG = {
|
|
36
|
-
enabled: true, // Can be disabled by environment variable
|
|
37
|
-
debug: process.env.NPORT_DEBUG === "true",
|
|
38
|
-
timeout: 2000, // Don't block CLI for too long
|
|
39
|
-
userIdFile: path.join(os.homedir(), ".nport", "analytics-id"),
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// ============================================================================
|
|
43
|
-
// Analytics Manager
|
|
44
|
-
// ============================================================================
|
|
45
|
-
|
|
46
|
-
class AnalyticsManager {
|
|
47
|
-
constructor() {
|
|
48
|
-
this.userId = null;
|
|
49
|
-
this.sessionId = null;
|
|
50
|
-
this.disabled = false;
|
|
51
|
-
|
|
52
|
-
// Disable analytics if environment variable is set
|
|
53
|
-
if (process.env.NPORT_ANALYTICS === "false") {
|
|
54
|
-
this.disabled = true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Initialize analytics - must be called before tracking
|
|
60
|
-
*/
|
|
61
|
-
async initialize() {
|
|
62
|
-
if (this.disabled) return;
|
|
63
|
-
|
|
64
|
-
// Check if API secret is configured
|
|
65
|
-
if (!FIREBASE_CONFIG.apiSecret || FIREBASE_CONFIG.apiSecret === "YOUR_API_SECRET_HERE") {
|
|
66
|
-
if (ANALYTICS_CONFIG.debug) {
|
|
67
|
-
console.warn("[Analytics] API secret not configured. Analytics disabled.");
|
|
68
|
-
console.warn("[Analytics] Set NPORT_ANALYTICS_SECRET environment variable.");
|
|
69
|
-
}
|
|
70
|
-
this.disabled = true;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
this.userId = await this.getUserId();
|
|
76
|
-
this.sessionId = this.generateSessionId();
|
|
77
|
-
|
|
78
|
-
if (ANALYTICS_CONFIG.debug) {
|
|
79
|
-
console.log("[Analytics] Initialized successfully");
|
|
80
|
-
console.log("[Analytics] User ID:", this.userId.substring(0, 8) + "...");
|
|
81
|
-
}
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (ANALYTICS_CONFIG.debug) {
|
|
84
|
-
console.error("[Analytics] Initialization failed:", error.message);
|
|
85
|
-
}
|
|
86
|
-
this.disabled = true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get or create a persistent user ID
|
|
92
|
-
*/
|
|
93
|
-
async getUserId() {
|
|
94
|
-
try {
|
|
95
|
-
// Ensure .nport directory exists
|
|
96
|
-
const configDir = path.join(os.homedir(), ".nport");
|
|
97
|
-
if (!fs.existsSync(configDir)) {
|
|
98
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Try to read existing user ID
|
|
102
|
-
if (fs.existsSync(ANALYTICS_CONFIG.userIdFile)) {
|
|
103
|
-
const userId = fs.readFileSync(ANALYTICS_CONFIG.userIdFile, "utf8").trim();
|
|
104
|
-
if (userId) return userId;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Generate new anonymous user ID
|
|
108
|
-
const userId = this.generateAnonymousId();
|
|
109
|
-
|
|
110
|
-
// Save for future use
|
|
111
|
-
fs.writeFileSync(ANALYTICS_CONFIG.userIdFile, userId, "utf8");
|
|
112
|
-
|
|
113
|
-
return userId;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
// If file operations fail, use session-based ID
|
|
116
|
-
return this.generateAnonymousId();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Generate anonymous user ID based on machine characteristics
|
|
122
|
-
*/
|
|
123
|
-
generateAnonymousId() {
|
|
124
|
-
const machineId = [
|
|
125
|
-
os.hostname(),
|
|
126
|
-
os.platform(),
|
|
127
|
-
os.arch(),
|
|
128
|
-
os.homedir(),
|
|
129
|
-
].join("-");
|
|
130
|
-
|
|
131
|
-
return createHash("sha256").update(machineId).digest("hex").substring(0, 32);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Generate session ID
|
|
136
|
-
*/
|
|
137
|
-
generateSessionId() {
|
|
138
|
-
return randomUUID();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Track an event
|
|
143
|
-
*/
|
|
144
|
-
async trackEvent(eventName, params = {}) {
|
|
145
|
-
if (this.disabled || !ANALYTICS_CONFIG.enabled) return;
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
const payload = this.buildPayload(eventName, params);
|
|
149
|
-
|
|
150
|
-
// Send to GA4 Measurement Protocol (non-blocking)
|
|
151
|
-
axios.post(
|
|
152
|
-
`https://www.google-analytics.com/mp/collect?measurement_id=${FIREBASE_CONFIG.measurementId}&api_secret=${FIREBASE_CONFIG.apiSecret}`,
|
|
153
|
-
payload,
|
|
154
|
-
{
|
|
155
|
-
timeout: ANALYTICS_CONFIG.timeout,
|
|
156
|
-
headers: { "Content-Type": "application/json" },
|
|
157
|
-
}
|
|
158
|
-
).catch((error) => {
|
|
159
|
-
// Silently fail - don't interrupt CLI operations
|
|
160
|
-
if (ANALYTICS_CONFIG.debug) {
|
|
161
|
-
console.error("[Analytics] Failed to send event:", error.message);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
if (ANALYTICS_CONFIG.debug) {
|
|
166
|
-
console.log("[Analytics] Event tracked:", eventName, params);
|
|
167
|
-
}
|
|
168
|
-
} catch (error) {
|
|
169
|
-
// Silently fail
|
|
170
|
-
if (ANALYTICS_CONFIG.debug) {
|
|
171
|
-
console.error("[Analytics] Error tracking event:", error.message);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Build GA4 Measurement Protocol payload
|
|
178
|
-
*/
|
|
179
|
-
buildPayload(eventName, params) {
|
|
180
|
-
return {
|
|
181
|
-
client_id: this.userId,
|
|
182
|
-
events: [
|
|
183
|
-
{
|
|
184
|
-
name: eventName,
|
|
185
|
-
params: {
|
|
186
|
-
session_id: this.sessionId,
|
|
187
|
-
engagement_time_msec: "100",
|
|
188
|
-
...this.getSystemInfo(),
|
|
189
|
-
...params,
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
],
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Get system information for context
|
|
198
|
-
*/
|
|
199
|
-
getSystemInfo() {
|
|
200
|
-
return {
|
|
201
|
-
os_platform: os.platform(),
|
|
202
|
-
os_version: os.release(),
|
|
203
|
-
os_arch: os.arch(),
|
|
204
|
-
node_version: process.version,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Track CLI start
|
|
210
|
-
*/
|
|
211
|
-
async trackCliStart(port, subdomain, version) {
|
|
212
|
-
await this.trackEvent("cli_start", {
|
|
213
|
-
port: String(port),
|
|
214
|
-
has_custom_subdomain: subdomain && !subdomain.startsWith("user-"),
|
|
215
|
-
cli_version: version,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Track tunnel creation
|
|
221
|
-
*/
|
|
222
|
-
async trackTunnelCreated(subdomain, port) {
|
|
223
|
-
await this.trackEvent("tunnel_created", {
|
|
224
|
-
subdomain_type: subdomain.startsWith("user-") ? "random" : "custom",
|
|
225
|
-
port: String(port),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Track tunnel error
|
|
231
|
-
*/
|
|
232
|
-
async trackTunnelError(errorType, errorMessage) {
|
|
233
|
-
await this.trackEvent("tunnel_error", {
|
|
234
|
-
error_type: errorType,
|
|
235
|
-
error_message: errorMessage.substring(0, 100), // Limit length
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Track tunnel shutdown
|
|
241
|
-
*/
|
|
242
|
-
async trackTunnelShutdown(reason, durationSeconds) {
|
|
243
|
-
await this.trackEvent("tunnel_shutdown", {
|
|
244
|
-
shutdown_reason: reason, // "manual", "timeout", "error"
|
|
245
|
-
duration_seconds: String(Math.floor(durationSeconds)),
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Track CLI update notification shown
|
|
251
|
-
*/
|
|
252
|
-
async trackUpdateAvailable(currentVersion, latestVersion) {
|
|
253
|
-
await this.trackEvent("update_available", {
|
|
254
|
-
current_version: currentVersion,
|
|
255
|
-
latest_version: latestVersion,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ============================================================================
|
|
261
|
-
// Export singleton instance
|
|
262
|
-
// ============================================================================
|
|
263
|
-
|
|
264
|
-
export const analytics = new AnalyticsManager();
|
|
265
|
-
|
package/src/api.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import axios from "axios";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import { CONFIG } from "./config.js";
|
|
4
|
-
import { state } from "./state.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* API Client
|
|
8
|
-
* Handles communication with the NPort backend service
|
|
9
|
-
*/
|
|
10
|
-
export class APIClient {
|
|
11
|
-
static async createTunnel(subdomain, backendUrl = null) {
|
|
12
|
-
const url = backendUrl || CONFIG.BACKEND_URL;
|
|
13
|
-
try {
|
|
14
|
-
const { data } = await axios.post(url, { subdomain });
|
|
15
|
-
|
|
16
|
-
if (!data.success) {
|
|
17
|
-
throw new Error(data.error || "Unknown error from backend");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
tunnelId: data.tunnelId,
|
|
22
|
-
tunnelToken: data.tunnelToken,
|
|
23
|
-
url: data.url,
|
|
24
|
-
};
|
|
25
|
-
} catch (error) {
|
|
26
|
-
throw this.handleError(error, subdomain);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
static async deleteTunnel(subdomain, tunnelId, backendUrl = null) {
|
|
31
|
-
const url = backendUrl || CONFIG.BACKEND_URL;
|
|
32
|
-
await axios.delete(url, {
|
|
33
|
-
data: { subdomain, tunnelId },
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
static handleError(error, subdomain) {
|
|
38
|
-
if (error.response?.data?.error) {
|
|
39
|
-
const errorMsg = error.response.data.error;
|
|
40
|
-
|
|
41
|
-
// Check for protected subdomain (reserved for production services)
|
|
42
|
-
if (errorMsg.includes("SUBDOMAIN_PROTECTED:")) {
|
|
43
|
-
return new Error(
|
|
44
|
-
`Subdomain "${subdomain}" is already taken or in use.\n\n` +
|
|
45
|
-
chalk.yellow(`💡 Try one of these options:\n`) +
|
|
46
|
-
chalk.gray(` 1. Choose a different subdomain: `) +
|
|
47
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-v2\n`) +
|
|
48
|
-
chalk.gray(` 2. Use a random subdomain: `) +
|
|
49
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT}\n`) +
|
|
50
|
-
chalk.gray(
|
|
51
|
-
` 3. Wait a few minutes and retry if you just stopped a tunnel with this name`
|
|
52
|
-
)
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check for subdomain in use (active tunnel)
|
|
57
|
-
if (
|
|
58
|
-
errorMsg.includes("SUBDOMAIN_IN_USE:") ||
|
|
59
|
-
errorMsg.includes("currently in use") ||
|
|
60
|
-
errorMsg.includes("already exists and is currently active")
|
|
61
|
-
) {
|
|
62
|
-
return new Error(
|
|
63
|
-
chalk.red(`✗ Subdomain "${subdomain}" is already in use!\n\n`) +
|
|
64
|
-
chalk.yellow(`💡 This subdomain is currently being used by another active tunnel.\n\n`) +
|
|
65
|
-
chalk.white(`Choose a different subdomain:\n`) +
|
|
66
|
-
chalk.gray(` 1. Add a suffix: `) +
|
|
67
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-2\n`) +
|
|
68
|
-
chalk.gray(` 2. Try a variation: `) +
|
|
69
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s my-${subdomain}\n`) +
|
|
70
|
-
chalk.gray(` 3. Use random name: `) +
|
|
71
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT}\n`)
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Check for duplicate tunnel error (other Cloudflare errors)
|
|
76
|
-
if (
|
|
77
|
-
errorMsg.includes("already have a tunnel") ||
|
|
78
|
-
errorMsg.includes("[1013]")
|
|
79
|
-
) {
|
|
80
|
-
return new Error(
|
|
81
|
-
`Subdomain "${subdomain}" is already taken or in use.\n\n` +
|
|
82
|
-
chalk.yellow(`💡 Try one of these options:\n`) +
|
|
83
|
-
chalk.gray(` 1. Choose a different subdomain: `) +
|
|
84
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT} -s ${subdomain}-v2\n`) +
|
|
85
|
-
chalk.gray(` 2. Use a random subdomain: `) +
|
|
86
|
-
chalk.cyan(`nport ${state.port || CONFIG.DEFAULT_PORT}\n`) +
|
|
87
|
-
chalk.gray(
|
|
88
|
-
` 3. Wait a few minutes and retry if you just stopped a tunnel with this name`
|
|
89
|
-
)
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return new Error(`Backend Error: ${errorMsg}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (error.response) {
|
|
97
|
-
const errorMsg = JSON.stringify(error.response.data, null, 2);
|
|
98
|
-
return new Error(`Backend Error: ${errorMsg}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return error;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
package/src/args.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { CONFIG } from "./config.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Command Line Argument Parser
|
|
5
|
-
* Handles parsing of CLI arguments for port, subdomain, language, and backend URL
|
|
6
|
-
*/
|
|
7
|
-
export class ArgumentParser {
|
|
8
|
-
static parse(argv) {
|
|
9
|
-
const port = this.parsePort(argv);
|
|
10
|
-
const subdomain = this.parseSubdomain(argv);
|
|
11
|
-
const language = this.parseLanguage(argv);
|
|
12
|
-
const backendUrl = this.parseBackendUrl(argv);
|
|
13
|
-
const setBackend = this.parseSetBackend(argv);
|
|
14
|
-
return { port, subdomain, language, backendUrl, setBackend };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
static parsePort(argv) {
|
|
18
|
-
const portArg = parseInt(argv[0]);
|
|
19
|
-
return portArg || CONFIG.DEFAULT_PORT;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static parseSubdomain(argv) {
|
|
23
|
-
// Try all subdomain formats
|
|
24
|
-
const formats = [
|
|
25
|
-
() => this.findFlagWithEquals(argv, "--subdomain="),
|
|
26
|
-
() => this.findFlagWithEquals(argv, "-s="),
|
|
27
|
-
() => this.findFlagWithValue(argv, "--subdomain"),
|
|
28
|
-
() => this.findFlagWithValue(argv, "-s"),
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
for (const format of formats) {
|
|
32
|
-
const subdomain = format();
|
|
33
|
-
if (subdomain) return subdomain;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return this.generateRandomSubdomain();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
static parseLanguage(argv) {
|
|
40
|
-
// Check if --language flag exists without value (to trigger prompt)
|
|
41
|
-
if (argv.includes('--language') || argv.includes('--lang') || argv.includes('-l')) {
|
|
42
|
-
const langIndex = argv.indexOf('--language') !== -1 ? argv.indexOf('--language') :
|
|
43
|
-
argv.indexOf('--lang') !== -1 ? argv.indexOf('--lang') :
|
|
44
|
-
argv.indexOf('-l');
|
|
45
|
-
|
|
46
|
-
// If flag is present but next arg is another flag or doesn't exist, return 'prompt'
|
|
47
|
-
const nextArg = argv[langIndex + 1];
|
|
48
|
-
if (!nextArg || nextArg.startsWith('-')) {
|
|
49
|
-
return 'prompt';
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Try language flag formats: --language=en, --lang=en, -l en
|
|
54
|
-
const formats = [
|
|
55
|
-
() => this.findFlagWithEquals(argv, "--language="),
|
|
56
|
-
() => this.findFlagWithEquals(argv, "--lang="),
|
|
57
|
-
() => this.findFlagWithEquals(argv, "-l="),
|
|
58
|
-
() => this.findFlagWithValue(argv, "--language"),
|
|
59
|
-
() => this.findFlagWithValue(argv, "--lang"),
|
|
60
|
-
() => this.findFlagWithValue(argv, "-l"),
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
for (const format of formats) {
|
|
64
|
-
const language = format();
|
|
65
|
-
if (language) return language;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return null; // No language specified
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
static parseBackendUrl(argv) {
|
|
72
|
-
// Try backend URL flag formats: --backend=url, --backend url, -b url
|
|
73
|
-
const formats = [
|
|
74
|
-
() => this.findFlagWithEquals(argv, "--backend="),
|
|
75
|
-
() => this.findFlagWithEquals(argv, "-b="),
|
|
76
|
-
() => this.findFlagWithValue(argv, "--backend"),
|
|
77
|
-
() => this.findFlagWithValue(argv, "-b"),
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
for (const format of formats) {
|
|
81
|
-
const url = format();
|
|
82
|
-
if (url) return url;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return null; // No backend URL specified
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
static parseSetBackend(argv) {
|
|
89
|
-
// Try set-backend flag formats: --set-backend=url, --set-backend url
|
|
90
|
-
const formats = [
|
|
91
|
-
() => this.findFlagWithEquals(argv, "--set-backend="),
|
|
92
|
-
() => this.findFlagWithValue(argv, "--set-backend"),
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
for (const format of formats) {
|
|
96
|
-
const url = format();
|
|
97
|
-
if (url) return url;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Check if --set-backend flag exists without value (to clear)
|
|
101
|
-
if (argv.includes('--set-backend')) {
|
|
102
|
-
return 'clear';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return null; // No set-backend specified
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
static findFlagWithEquals(argv, flag) {
|
|
109
|
-
const arg = argv.find((a) => a.startsWith(flag));
|
|
110
|
-
return arg ? arg.split("=")[1] : null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
static findFlagWithValue(argv, flag) {
|
|
114
|
-
const index = argv.indexOf(flag);
|
|
115
|
-
return index !== -1 && argv[index + 1] ? argv[index + 1] : null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
static generateRandomSubdomain() {
|
|
119
|
-
return `${CONFIG.SUBDOMAIN_PREFIX}${Math.floor(Math.random() * 10000)}`;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|