switchboard-router 0.6.3 → 0.6.4
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 +40 -0
- package/app/.next-cli-build/BUILD_ID +1 -1
- package/app/.next-cli-build/app-path-routes-manifest.json +11 -11
- package/app/.next-cli-build/build-manifest.json +2 -2
- package/app/.next-cli-build/prerender-manifest.json +3 -3
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/agent-library/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/basic-chat/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cli-tools/[toolId]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/cli-tools/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/combos/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/combos/routing/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/console-log/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/endpoint/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/[kind]/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/[kind]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/combo/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/media-providers/web/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/mitm/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/profile/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/new/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/providers/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/quota/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/skills/[id]/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/skills/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/token-saver/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/translator/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/(dashboard)/dashboard/usage/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/_global-error.html +1 -1
- package/app/.next-cli-build/server/app/_global-error.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/_not-found.html +1 -1
- package/app/.next-cli-build/server/app/_not-found.rsc +3 -3
- package/app/.next-cli-build/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/aider-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/all-statuses/route.js +4 -4
- package/app/.next-cli-build/server/app/api/cli-tools/antigravity-mitm/alias/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/claude-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/cline-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/codex-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-mcp-registry/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-mcp-tools/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/cowork-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/deepseek-tui-settings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/droid-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/gemini-cli-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/grok-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/hermes-settings/route.js +3 -3
- package/app/.next-cli-build/server/app/api/cli-tools/jcode-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/kilo-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/openclaw-settings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/cli-tools/opencode-settings/route.js +2 -2
- package/app/.next-cli-build/server/app/api/cli-tools/pi-settings/route.js +1 -1
- package/app/.next-cli-build/server/app/api/version/route.js +1 -1
- package/app/.next-cli-build/server/app/api/version/shutdown/route.js +1 -1
- package/app/.next-cli-build/server/app/api/version/update/route.js +1 -1
- package/app/.next-cli-build/server/app/callback/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/callback.html +1 -1
- package/app/.next-cli-build/server/app/callback.rsc +3 -3
- package/app/.next-cli-build/server/app/callback.segments/_full.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/callback.segments/_head.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/callback.segments/_index.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/callback.segments/_tree.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/callback.segments/callback/__PAGE__.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/callback.segments/callback.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.html +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.rsc +3 -3
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/_full.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/_head.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/_index.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/_tree.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/dashboard/settings/pricing/__PAGE__.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/dashboard/settings/pricing.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/dashboard/settings.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/dashboard/settings/pricing.segments/dashboard.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/index.html +1 -1
- package/app/.next-cli-build/server/app/index.rsc +3 -3
- package/app/.next-cli-build/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/index.segments/_full.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/index.segments/_head.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/index.segments/_index.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/index.segments/_tree.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/landing/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/landing.html +1 -1
- package/app/.next-cli-build/server/app/landing.rsc +3 -3
- package/app/.next-cli-build/server/app/landing.segments/_full.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/landing.segments/_head.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/landing.segments/_index.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/landing.segments/_tree.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/landing.segments/landing/__PAGE__.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/landing.segments/landing.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/login/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app/login.html +1 -1
- package/app/.next-cli-build/server/app/login.rsc +3 -3
- package/app/.next-cli-build/server/app/login.segments/_full.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/login.segments/_head.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/login.segments/_index.segment.rsc +3 -3
- package/app/.next-cli-build/server/app/login.segments/_tree.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/login.segments/login.segment.rsc +1 -1
- package/app/.next-cli-build/server/app/page_client-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/app-paths-manifest.json +11 -11
- package/app/.next-cli-build/server/chunks/194.js +1 -1
- package/app/.next-cli-build/server/chunks/4126.js +1 -1
- package/app/.next-cli-build/server/chunks/4746.js +1 -1
- package/app/.next-cli-build/server/chunks/869.js +1 -1
- package/app/.next-cli-build/server/chunks/8998.js +1 -1
- package/app/.next-cli-build/server/middleware-build-manifest.js +1 -1
- package/app/.next-cli-build/server/middleware.js +3 -3
- package/app/.next-cli-build/server/pages/404.html +1 -1
- package/app/.next-cli-build/server/pages/500.html +1 -1
- package/app/.next-cli-build/server/server-reference-manifest.js +1 -1
- package/app/.next-cli-build/server/server-reference-manifest.json +1 -1
- package/app/.next-cli-build/static/chunks/{1321-c8ffcb14dcf776aa.js → 1321-5429c7d81eb38c6f.js} +1 -1
- package/app/package.json +1 -1
- package/app/src/lib/updater/updater.js +41 -12
- package/cli.js +401 -320
- package/package.json +1 -1
- package/src/cli/cliOptions.js +139 -0
- package/src/cli/menus/settings.js +2 -2
- package/src/cli/processManager.js +186 -0
- package/src/cli/serverStatus.js +90 -0
- package/src/cli/terminalUI.js +12 -11
- package/src/cli/tray/autostart.js +35 -14
- package/src/cli/tray/tray.js +34 -32
- package/src/cli/tray/trayWin.js +23 -9
- package/src/cli/utils/input.js +10 -1
- /package/app/.next-cli-build/static/{emM_vF1dDbzWCO6J4_WjU → 88rQB4DdrBDjq9aWvSEH4}/_buildManifest.js +0 -0
- /package/app/.next-cli-build/static/{emM_vF1dDbzWCO6J4_WjU → 88rQB4DdrBDjq9aWvSEH4}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const DEFAULT_PORT = 20128;
|
|
2
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
3
|
+
const COMMANDS = new Set(["start", "status", "stop", "restart", "help", "version"]);
|
|
4
|
+
|
|
5
|
+
function isLoopbackHost(value) {
|
|
6
|
+
const normalized = String(value).toLowerCase();
|
|
7
|
+
return normalized === "localhost" || normalized === "::1" || normalized === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(normalized);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function requireValue(argv, index, option) {
|
|
11
|
+
const value = argv[index + 1];
|
|
12
|
+
if (!value || value.startsWith("-")) {
|
|
13
|
+
throw new Error(`${option} requires a value`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parsePort(value) {
|
|
19
|
+
if (!/^\d+$/.test(String(value))) {
|
|
20
|
+
throw new Error("--port must be between 1 and 65535");
|
|
21
|
+
}
|
|
22
|
+
const parsed = Number(value);
|
|
23
|
+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
|
|
24
|
+
throw new Error("--port must be between 1 and 65535");
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseHost(value) {
|
|
30
|
+
const host = String(value).trim();
|
|
31
|
+
if (!host || /\s|\/|:\/\//.test(host)) {
|
|
32
|
+
throw new Error("--host must be a hostname or IP address");
|
|
33
|
+
}
|
|
34
|
+
return host;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseCliArgs(argv, defaults = {}) {
|
|
38
|
+
const args = [...argv];
|
|
39
|
+
let command = "start";
|
|
40
|
+
if (args[0] && !args[0].startsWith("-")) {
|
|
41
|
+
command = args.shift();
|
|
42
|
+
if (!COMMANDS.has(command)) throw new Error(`Unknown command: ${command}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const parsed = {
|
|
46
|
+
command,
|
|
47
|
+
port: defaults.port || DEFAULT_PORT,
|
|
48
|
+
host: defaults.host || DEFAULT_HOST,
|
|
49
|
+
showLog: false,
|
|
50
|
+
trayMode: false,
|
|
51
|
+
skipUpdate: false,
|
|
52
|
+
noBrowser: false,
|
|
53
|
+
json: false,
|
|
54
|
+
help: command === "help",
|
|
55
|
+
version: command === "version",
|
|
56
|
+
portProvided: false,
|
|
57
|
+
hostProvided: false,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const arg = args[i];
|
|
62
|
+
if (arg === "--port" || arg === "-p") {
|
|
63
|
+
parsed.port = parsePort(requireValue(args, i, arg));
|
|
64
|
+
parsed.portProvided = true;
|
|
65
|
+
i++;
|
|
66
|
+
} else if (arg === "--host" || arg === "-H") {
|
|
67
|
+
parsed.host = parseHost(requireValue(args, i, arg));
|
|
68
|
+
parsed.hostProvided = true;
|
|
69
|
+
i++;
|
|
70
|
+
} else if (arg === "--no-browser" || arg === "-n") {
|
|
71
|
+
parsed.noBrowser = true;
|
|
72
|
+
} else if (arg === "--log" || arg === "-l") {
|
|
73
|
+
parsed.showLog = true;
|
|
74
|
+
} else if (arg === "--tray" || arg === "-t") {
|
|
75
|
+
parsed.trayMode = true;
|
|
76
|
+
} else if (arg === "--skip-update") {
|
|
77
|
+
parsed.skipUpdate = true;
|
|
78
|
+
} else if (arg === "--json") {
|
|
79
|
+
parsed.json = true;
|
|
80
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
81
|
+
parsed.help = true;
|
|
82
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
83
|
+
parsed.version = true;
|
|
84
|
+
} else {
|
|
85
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (parsed.json && command !== "status") {
|
|
90
|
+
throw new Error("--json is only valid with the status command");
|
|
91
|
+
}
|
|
92
|
+
if (["status", "stop"].includes(command)) {
|
|
93
|
+
const invalid = [parsed.showLog && "--log", parsed.trayMode && "--tray", parsed.skipUpdate && "--skip-update", parsed.noBrowser && "--no-browser"].filter(Boolean);
|
|
94
|
+
if (invalid.length) throw new Error(`${invalid.join(", ")} is not valid with the ${command} command`);
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatHelp({ version = "unknown", defaultPort = DEFAULT_PORT, defaultHost = DEFAULT_HOST } = {}) {
|
|
100
|
+
return `Switchboard ${version}
|
|
101
|
+
|
|
102
|
+
Usage:
|
|
103
|
+
switchboard [start] [options] Start Switchboard (default)
|
|
104
|
+
switchboard status [options] Show server, version, PID, and ownership status
|
|
105
|
+
switchboard stop [options] Stop the owned Switchboard instance gracefully
|
|
106
|
+
switchboard restart [options] Gracefully stop, then start Switchboard
|
|
107
|
+
|
|
108
|
+
Lifecycle:
|
|
109
|
+
In the terminal, press Ctrl+C to stop gracefully.
|
|
110
|
+
In tray mode, choose Quit. You can always use "switchboard stop" if the icon is unavailable.
|
|
111
|
+
A second start never kills an unrelated listener; use "switchboard status" to diagnose conflicts.
|
|
112
|
+
|
|
113
|
+
Options:
|
|
114
|
+
-p, --port <port> Server port (default: ${defaultPort})
|
|
115
|
+
-H, --host <host> Bind host (default: ${defaultHost})
|
|
116
|
+
-l, --log Stream server logs while retaining crash diagnostics
|
|
117
|
+
-t, --tray Run with the system-tray control surface
|
|
118
|
+
-n, --no-browser Compatibility flag; browsers open only when explicitly selected
|
|
119
|
+
--skip-update Skip the startup update check
|
|
120
|
+
--json Machine-readable output for "switchboard status"
|
|
121
|
+
-h, --help Show this help
|
|
122
|
+
-v, --version Show the version
|
|
123
|
+
|
|
124
|
+
Examples:
|
|
125
|
+
switchboard
|
|
126
|
+
switchboard --tray
|
|
127
|
+
switchboard status
|
|
128
|
+
switchboard restart --port ${defaultPort}
|
|
129
|
+
switchboard stop
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
DEFAULT_HOST,
|
|
135
|
+
DEFAULT_PORT,
|
|
136
|
+
formatHelp,
|
|
137
|
+
isLoopbackHost,
|
|
138
|
+
parseCliArgs,
|
|
139
|
+
};
|
|
@@ -18,14 +18,14 @@ const COLORS = {
|
|
|
18
18
|
* there is no dashboard login, password or OIDC to manage here.
|
|
19
19
|
* @param {Array<string>} breadcrumb - Breadcrumb path
|
|
20
20
|
*/
|
|
21
|
-
async function showSettingsMenu(breadcrumb = []) {
|
|
21
|
+
async function showSettingsMenu(breadcrumb = [], { port = 20128, networkExposed = false } = {}) {
|
|
22
22
|
await showMenuWithBack({
|
|
23
23
|
title: "⚙️ Settings",
|
|
24
24
|
breadcrumb,
|
|
25
25
|
headerContent: async (data) => {
|
|
26
26
|
const lines = [];
|
|
27
27
|
|
|
28
|
-
lines.push(` Endpoint: http://localhost
|
|
28
|
+
lines.push(` Endpoint: http://localhost:${port}/v1 ${COLORS.dim}(${networkExposed ? "network-exposed" : "local only"})${COLORS.reset}`);
|
|
29
29
|
|
|
30
30
|
// RTK section
|
|
31
31
|
const rtkOn = data?.settings?.rtkEnabled !== false;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execFileSync, execSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
function normalizePath(value) {
|
|
6
|
+
try { return path.resolve(value); } catch { return ""; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function tokenizeCommand(command) {
|
|
10
|
+
const tokens = [];
|
|
11
|
+
let token = "";
|
|
12
|
+
let quote = "";
|
|
13
|
+
for (const char of String(command)) {
|
|
14
|
+
if (quote) {
|
|
15
|
+
if (char === quote) quote = "";
|
|
16
|
+
else token += char;
|
|
17
|
+
} else if (char === "\"" || char === "'") {
|
|
18
|
+
quote = char;
|
|
19
|
+
} else if (/\s/.test(char)) {
|
|
20
|
+
if (token) {
|
|
21
|
+
tokens.push(token);
|
|
22
|
+
token = "";
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
token += char;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (token) tokens.push(token);
|
|
29
|
+
return tokens;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function matchesRecordedProcess({ command, cwd, expectedPath }) {
|
|
33
|
+
if (!expectedPath || !command) return false;
|
|
34
|
+
const expected = normalizePath(expectedPath);
|
|
35
|
+
const tokens = tokenizeCommand(command);
|
|
36
|
+
const scriptIndex = tokens.findIndex((token) => {
|
|
37
|
+
if (!token) return false;
|
|
38
|
+
if (path.isAbsolute(token)) return normalizePath(token) === expected;
|
|
39
|
+
return !!cwd && normalizePath(path.join(cwd, token)) === expected;
|
|
40
|
+
});
|
|
41
|
+
const runtimeName = path.basename(tokens[0] || "").toLowerCase();
|
|
42
|
+
const isNodeRuntime = runtimeName === "node" || runtimeName === "node.exe" || runtimeName === path.basename(process.execPath).toLowerCase();
|
|
43
|
+
if (isNodeRuntime && scriptIndex > 0 && tokens.slice(1, scriptIndex).every((token) => token.startsWith("-"))) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const expectedName = path.basename(expectedPath);
|
|
48
|
+
const isBundledServer = expectedName === "server.js" || expectedName === "custom-server.js";
|
|
49
|
+
if (!isBundledServer || !/^next-server(?:\s|\(|$)/.test(command.trim())) return false;
|
|
50
|
+
|
|
51
|
+
return !!cwd && normalizePath(cwd) === normalizePath(path.dirname(expectedPath));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getProcessCommand(pid) {
|
|
55
|
+
try {
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
return execFileSync("powershell", [
|
|
58
|
+
"-NonInteractive", "-NoProfile", "-Command",
|
|
59
|
+
`(Get-CimInstance Win32_Process -Filter 'ProcessId = ${Number(pid)}').CommandLine`,
|
|
60
|
+
], { encoding: "utf8", windowsHide: true, timeout: 3000 }).trim();
|
|
61
|
+
}
|
|
62
|
+
return execFileSync("ps", ["-p", String(Number(pid)), "-o", "command="], {
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
timeout: 3000,
|
|
65
|
+
}).trim();
|
|
66
|
+
} catch { return ""; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getProcessCwd(pid) {
|
|
70
|
+
try {
|
|
71
|
+
if (process.platform === "linux") return fs.readlinkSync(`/proc/${Number(pid)}/cwd`);
|
|
72
|
+
if (process.platform === "win32") return "";
|
|
73
|
+
const output = execFileSync("lsof", ["-a", "-p", String(Number(pid)), "-d", "cwd", "-Fn"], {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
76
|
+
timeout: 3000,
|
|
77
|
+
});
|
|
78
|
+
const cwdLine = output.split(/\r?\n/).find((line) => line.startsWith("n"));
|
|
79
|
+
return cwdLine ? cwdLine.slice(1) : "";
|
|
80
|
+
} catch { return ""; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function processMatchesRecordedPath(pid, expectedPath) {
|
|
84
|
+
const command = getProcessCommand(pid);
|
|
85
|
+
if (matchesRecordedProcess({ command, cwd: "", expectedPath })) return true;
|
|
86
|
+
return matchesRecordedProcess({ command, cwd: getProcessCwd(pid), expectedPath });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findListeningPids(port) {
|
|
90
|
+
const numericPort = Number(port);
|
|
91
|
+
if (!Number.isInteger(numericPort) || numericPort < 1 || numericPort > 65535) return [];
|
|
92
|
+
try {
|
|
93
|
+
if (process.platform === "win32") {
|
|
94
|
+
const output = execFileSync("powershell", [
|
|
95
|
+
"-NonInteractive", "-NoProfile", "-Command",
|
|
96
|
+
`Get-NetTCPConnection -LocalPort ${numericPort} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`,
|
|
97
|
+
], { encoding: "utf8", windowsHide: true, timeout: 5000 });
|
|
98
|
+
return output.split(/\s+/).map(Number).filter((pid) => Number.isInteger(pid) && pid > 1);
|
|
99
|
+
}
|
|
100
|
+
const output = execFileSync("lsof", [`-tiTCP:${numericPort}`, "-sTCP:LISTEN"], {
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
103
|
+
timeout: 5000,
|
|
104
|
+
});
|
|
105
|
+
return output.split(/\s+/).map(Number).filter((pid) => Number.isInteger(pid) && pid > 1);
|
|
106
|
+
} catch { return []; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPidAlive(pid) {
|
|
110
|
+
try { process.kill(Number(pid), 0); return true; } catch (error) { return error?.code === "EPERM"; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function sleep(ms) {
|
|
114
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function acquireLifecycleLock(dataDir, { instanceId, timeoutMs = 10000 } = {}) {
|
|
118
|
+
const lockDir = path.join(dataDir, "runtime", "lifecycle.lock");
|
|
119
|
+
const ownerFile = path.join(lockDir, "owner.json");
|
|
120
|
+
const deadline = Date.now() + timeoutMs;
|
|
121
|
+
fs.mkdirSync(path.dirname(lockDir), { recursive: true });
|
|
122
|
+
|
|
123
|
+
while (Date.now() < deadline) {
|
|
124
|
+
try {
|
|
125
|
+
fs.mkdirSync(lockDir);
|
|
126
|
+
fs.writeFileSync(ownerFile, JSON.stringify({ pid: process.pid, instanceId, createdAt: Date.now() }), { mode: 0o600 });
|
|
127
|
+
return () => {
|
|
128
|
+
try {
|
|
129
|
+
const owner = JSON.parse(fs.readFileSync(ownerFile, "utf8"));
|
|
130
|
+
if (owner.instanceId === instanceId) fs.rmSync(lockDir, { recursive: true, force: true });
|
|
131
|
+
} catch { /* lock already released */ }
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error?.code !== "EEXIST") throw error;
|
|
135
|
+
let owner = null;
|
|
136
|
+
try { owner = JSON.parse(fs.readFileSync(ownerFile, "utf8")); } catch { /* incomplete or stale lock */ }
|
|
137
|
+
let ageMs = 0;
|
|
138
|
+
try { ageMs = Date.now() - fs.statSync(lockDir).mtimeMs; } catch { /* retry */ }
|
|
139
|
+
const stale = owner?.pid ? !isPidAlive(owner.pid) : ageMs > 1000;
|
|
140
|
+
if (stale) {
|
|
141
|
+
try { fs.rmSync(lockDir, { recursive: true, force: true }); } catch { /* another waiter won */ }
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
await sleep(100);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw new Error("Another Switchboard lifecycle operation is still in progress; retry shortly");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function terminatePid(pid, { timeoutMs = 2000, processGroup = false } = {}) {
|
|
151
|
+
const numericPid = Number(pid);
|
|
152
|
+
if (!Number.isInteger(numericPid) || numericPid <= 1 || numericPid === process.pid) return false;
|
|
153
|
+
const unixTarget = processGroup && process.platform !== "win32" ? -numericPid : numericPid;
|
|
154
|
+
const targetIsAlive = () => isPidAlive(unixTarget);
|
|
155
|
+
try {
|
|
156
|
+
if (process.platform === "win32") {
|
|
157
|
+
execSync(`taskkill /T /PID ${numericPid}`, { stdio: "ignore", windowsHide: true, timeout: 3000 });
|
|
158
|
+
} else {
|
|
159
|
+
process.kill(unixTarget, "SIGTERM");
|
|
160
|
+
}
|
|
161
|
+
} catch { /* already stopped or inaccessible */ }
|
|
162
|
+
|
|
163
|
+
const deadline = Date.now() + timeoutMs;
|
|
164
|
+
while (targetIsAlive() && Date.now() < deadline) await sleep(100);
|
|
165
|
+
if (!targetIsAlive()) return true;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (process.platform === "win32") {
|
|
169
|
+
execSync(`taskkill /F /T /PID ${numericPid}`, { stdio: "ignore", windowsHide: true, timeout: 3000 });
|
|
170
|
+
} else {
|
|
171
|
+
process.kill(unixTarget, "SIGKILL");
|
|
172
|
+
}
|
|
173
|
+
} catch { /* best effort */ }
|
|
174
|
+
return !targetIsAlive();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
acquireLifecycleLock,
|
|
179
|
+
findListeningPids,
|
|
180
|
+
getProcessCommand,
|
|
181
|
+
getProcessCwd,
|
|
182
|
+
isPidAlive,
|
|
183
|
+
matchesRecordedProcess,
|
|
184
|
+
processMatchesRecordedPath,
|
|
185
|
+
terminatePid,
|
|
186
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const net = require("net");
|
|
3
|
+
const MAX_RESPONSE_BYTES = 64 * 1024;
|
|
4
|
+
|
|
5
|
+
function requestJson({ hostname = "127.0.0.1", port, path, timeoutMs = 1000 }) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
let settled = false;
|
|
8
|
+
const finish = (value) => {
|
|
9
|
+
if (settled) return;
|
|
10
|
+
settled = true;
|
|
11
|
+
resolve(value);
|
|
12
|
+
};
|
|
13
|
+
const req = http.request({
|
|
14
|
+
hostname,
|
|
15
|
+
port,
|
|
16
|
+
path,
|
|
17
|
+
method: "GET",
|
|
18
|
+
headers: { Accept: "application/json" },
|
|
19
|
+
}, (res) => {
|
|
20
|
+
let body = "";
|
|
21
|
+
res.setEncoding("utf8");
|
|
22
|
+
res.on("data", (chunk) => {
|
|
23
|
+
body += chunk;
|
|
24
|
+
if (Buffer.byteLength(body) > MAX_RESPONSE_BYTES) {
|
|
25
|
+
res.destroy(new Error("response too large"));
|
|
26
|
+
finish(null);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
res.on("aborted", () => finish(null));
|
|
30
|
+
res.on("error", () => finish(null));
|
|
31
|
+
res.on("end", () => {
|
|
32
|
+
try {
|
|
33
|
+
finish({ statusCode: res.statusCode, body: JSON.parse(body) });
|
|
34
|
+
} catch {
|
|
35
|
+
finish(null);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
req.setTimeout(timeoutMs, () => {
|
|
40
|
+
req.destroy();
|
|
41
|
+
finish(null);
|
|
42
|
+
});
|
|
43
|
+
req.on("error", () => finish(null));
|
|
44
|
+
req.end();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function probeTcp(port, timeoutMs = 1000, hostname = "127.0.0.1") {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
let settled = false;
|
|
51
|
+
const socket = net.createConnection({ host: hostname, port });
|
|
52
|
+
const finish = (ready) => {
|
|
53
|
+
if (settled) return;
|
|
54
|
+
settled = true;
|
|
55
|
+
socket.destroy();
|
|
56
|
+
resolve(ready);
|
|
57
|
+
};
|
|
58
|
+
socket.setTimeout(timeoutMs, () => finish(false));
|
|
59
|
+
socket.once("connect", () => finish(true));
|
|
60
|
+
socket.once("error", () => finish(false));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function probeSwitchboard(port, timeoutMs = 1000, hostname = "127.0.0.1") {
|
|
65
|
+
const response = await requestJson({ hostname, port, path: "/api/mgmt/v1/version", timeoutMs });
|
|
66
|
+
const data = response?.body?.data;
|
|
67
|
+
if (response?.statusCode === 200 && data?.name === "switchboard-app") return data;
|
|
68
|
+
|
|
69
|
+
// Compatibility with older bundles that predate the management version API.
|
|
70
|
+
const health = await requestJson({ hostname, port, path: "/api/health", timeoutMs });
|
|
71
|
+
if (health?.statusCode === 200 && health?.body?.ok === true) {
|
|
72
|
+
return { name: "switchboard-app", version: null, startedAt: null, legacyHealth: true };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function waitForSwitchboard(port, { timeoutMs = 20000, intervalMs = 250, hostname = "127.0.0.1", acceptTcpFallback = null } = {}) {
|
|
78
|
+
const deadline = Date.now() + timeoutMs;
|
|
79
|
+
while (Date.now() < deadline) {
|
|
80
|
+
const status = await probeSwitchboard(port, Math.min(1000, intervalMs * 2), hostname);
|
|
81
|
+
if (status) return status;
|
|
82
|
+
if (acceptTcpFallback && await probeTcp(port, Math.min(1000, intervalMs * 2), hostname) && await acceptTcpFallback()) {
|
|
83
|
+
return { name: "switchboard-app", version: null, startedAt: null, tcpOnly: true };
|
|
84
|
+
}
|
|
85
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { probeSwitchboard, probeTcp, waitForSwitchboard };
|
package/src/cli/terminalUI.js
CHANGED
|
@@ -18,9 +18,10 @@ const COLORS = {
|
|
|
18
18
|
let cachedHeader = "";
|
|
19
19
|
let fetchingHeader = false;
|
|
20
20
|
|
|
21
|
-
function renderHeader(port, keys) {
|
|
21
|
+
function renderHeader(port, keys, { networkExposed = false } = {}) {
|
|
22
22
|
const lines = [];
|
|
23
|
-
|
|
23
|
+
const exposure = networkExposed ? "network-exposed" : "local only";
|
|
24
|
+
lines.push(`Endpoint: http://localhost:${port}/v1 ${COLORS.dim}(${exposure})${COLORS.reset}`);
|
|
24
25
|
if (!keys || keys.length === 0) {
|
|
25
26
|
lines.push(`Key: ${COLORS.dim}No API keys yet${COLORS.reset}`);
|
|
26
27
|
} else {
|
|
@@ -30,21 +31,21 @@ function renderHeader(port, keys) {
|
|
|
30
31
|
return lines.join("\n");
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
async function refreshHeaderBg(port) {
|
|
34
|
+
async function refreshHeaderBg(port, networkExposed) {
|
|
34
35
|
if (fetchingHeader) return;
|
|
35
36
|
fetchingHeader = true;
|
|
36
37
|
try {
|
|
37
38
|
const keysResult = await api.getApiKeys();
|
|
38
39
|
const keys = keysResult.success ? (keysResult.data.keys || []) : [];
|
|
39
|
-
cachedHeader = renderHeader(port, keys);
|
|
40
|
+
cachedHeader = renderHeader(port, keys, { networkExposed });
|
|
40
41
|
} finally {
|
|
41
42
|
fetchingHeader = false;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
function getHeader(port) {
|
|
46
|
+
function getHeader(port, networkExposed) {
|
|
46
47
|
// Kick off background refresh; return cache (or placeholder on first call).
|
|
47
|
-
refreshHeaderBg(port);
|
|
48
|
+
refreshHeaderBg(port, networkExposed);
|
|
48
49
|
return cachedHeader || `Endpoint: http://localhost:${port}/v1\nKey: ${COLORS.dim}...${COLORS.reset}`;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -52,20 +53,20 @@ function getHeader(port) {
|
|
|
52
53
|
* Start Terminal UI
|
|
53
54
|
* @param {number} port - Server port number
|
|
54
55
|
*/
|
|
55
|
-
async function startTerminalUI(port) {
|
|
56
|
+
async function startTerminalUI(port, { networkExposed = false } = {}) {
|
|
56
57
|
// Configure API client
|
|
57
58
|
api.configure({ port });
|
|
58
59
|
|
|
59
60
|
const basePath = ["Switchboard"];
|
|
60
61
|
|
|
61
62
|
// Prime header cache before first render
|
|
62
|
-
await refreshHeaderBg(port);
|
|
63
|
+
await refreshHeaderBg(port, networkExposed);
|
|
63
64
|
|
|
64
65
|
// Main menu
|
|
65
66
|
await showMenuWithBack({
|
|
66
67
|
title: "📡 Switchboard Terminal UI",
|
|
67
68
|
breadcrumb: basePath,
|
|
68
|
-
headerContent: () => getHeader(port),
|
|
69
|
+
headerContent: () => getHeader(port, networkExposed),
|
|
69
70
|
items: [
|
|
70
71
|
{
|
|
71
72
|
label: "Providers",
|
|
@@ -98,7 +99,7 @@ async function startTerminalUI(port) {
|
|
|
98
99
|
{
|
|
99
100
|
label: "Settings",
|
|
100
101
|
action: async () => {
|
|
101
|
-
await showSettingsMenu([...basePath, "Settings"]);
|
|
102
|
+
await showSettingsMenu([...basePath, "Settings"], { port, networkExposed });
|
|
102
103
|
return true;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
@@ -107,4 +108,4 @@ async function startTerminalUI(port) {
|
|
|
107
108
|
});
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
module.exports = { startTerminalUI };
|
|
111
|
+
module.exports = { renderHeader, startTerminalUI };
|
|
@@ -44,16 +44,16 @@ function getCliJsPath(cliPath) {
|
|
|
44
44
|
* @param {string} cliPath - Optional path to cli.js (defaults to auto-detect)
|
|
45
45
|
* @returns {boolean} success
|
|
46
46
|
*/
|
|
47
|
-
function enableAutoStart(cliPath) {
|
|
47
|
+
function enableAutoStart(cliPath, launchOptions = {}) {
|
|
48
48
|
const platform = process.platform;
|
|
49
49
|
|
|
50
50
|
if (!["darwin", "win32", "linux"].includes(platform)) return false;
|
|
51
51
|
if (platform === "linux" && !process.env.DISPLAY) return false;
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
|
-
if (platform === "darwin") return enableMacOS(cliPath);
|
|
55
|
-
if (platform === "win32") return enableWindows(cliPath);
|
|
56
|
-
if (platform === "linux") return enableLinux(cliPath);
|
|
54
|
+
if (platform === "darwin") return enableMacOS(cliPath, launchOptions);
|
|
55
|
+
if (platform === "win32") return enableWindows(cliPath, launchOptions);
|
|
56
|
+
if (platform === "linux") return enableLinux(cliPath, launchOptions);
|
|
57
57
|
} catch (err) {
|
|
58
58
|
// Silent fail — autostart is optional
|
|
59
59
|
}
|
|
@@ -138,7 +138,19 @@ function isAgentSelfMacOS() {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
function
|
|
141
|
+
function getLaunchArgs({ port = 20128, host = "127.0.0.1" } = {}) {
|
|
142
|
+
return ["--tray", "--skip-update", "--port", String(port), "--host", String(host)];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function xmlEscape(value) {
|
|
146
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function quoteDesktopArg(value) {
|
|
150
|
+
return `"${String(value).replace(/(["\\$])/g, "\\$1")}"`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function enableMacOS(cliPath, launchOptions) {
|
|
142
154
|
const launchAgentsDir = path.join(os.homedir(), "Library", "LaunchAgents");
|
|
143
155
|
const plistPath = path.join(launchAgentsDir, `${APP_LABEL}.plist`);
|
|
144
156
|
|
|
@@ -158,6 +170,10 @@ function enableMacOS(cliPath) {
|
|
|
158
170
|
// EnvironmentVariables.PATH explicitly includes node's bin dir so child
|
|
159
171
|
// processes spawned by cli.js (npm install at runtime, etc.) resolve.
|
|
160
172
|
const launchPath = `${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin`;
|
|
173
|
+
const launchArgs = getLaunchArgs(launchOptions);
|
|
174
|
+
const plistArgs = [nodePath, routerScript, ...launchArgs]
|
|
175
|
+
.map((arg) => ` <string>${xmlEscape(arg)}</string>`)
|
|
176
|
+
.join("\n");
|
|
161
177
|
|
|
162
178
|
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
163
179
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -167,10 +183,7 @@ function enableMacOS(cliPath) {
|
|
|
167
183
|
<string>${APP_LABEL}</string>
|
|
168
184
|
<key>ProgramArguments</key>
|
|
169
185
|
<array>
|
|
170
|
-
|
|
171
|
-
<string>${routerScript}</string>
|
|
172
|
-
<string>--tray</string>
|
|
173
|
-
<string>--skip-update</string>
|
|
186
|
+
${plistArgs}
|
|
174
187
|
</array>
|
|
175
188
|
<key>EnvironmentVariables</key>
|
|
176
189
|
<dict>
|
|
@@ -236,7 +249,7 @@ function disableMacOS() {
|
|
|
236
249
|
|
|
237
250
|
// ============ Windows ============
|
|
238
251
|
|
|
239
|
-
function enableWindows(cliPath) {
|
|
252
|
+
function enableWindows(cliPath, launchOptions) {
|
|
240
253
|
const startupDir = path.join(process.env.APPDATA || "", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
|
|
241
254
|
const vbsPath = path.join(startupDir, `${APP_NAME}.vbs`);
|
|
242
255
|
|
|
@@ -245,11 +258,16 @@ function enableWindows(cliPath) {
|
|
|
245
258
|
const nodePath = process.execPath;
|
|
246
259
|
const routerScript = getCliJsPath(cliPath);
|
|
247
260
|
if (!routerScript) return false;
|
|
261
|
+
const launchArgs = getLaunchArgs(launchOptions);
|
|
262
|
+
const vbsCommand = [nodePath, routerScript, ...launchArgs]
|
|
263
|
+
.map((arg) => `"${String(arg).replace(/"/g, '""')}"`)
|
|
264
|
+
.join(" ")
|
|
265
|
+
.replace(/"/g, '""');
|
|
248
266
|
|
|
249
267
|
// Run node + cli.js directly, hidden window. Avoids the fragile
|
|
250
268
|
// `switchboard.cmd` lookup that depended on the npm prefix path.
|
|
251
269
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")
|
|
252
|
-
WshShell.Run "
|
|
270
|
+
WshShell.Run "${vbsCommand}", 0, False
|
|
253
271
|
`;
|
|
254
272
|
fs.writeFileSync(vbsPath, vbsContent);
|
|
255
273
|
return true;
|
|
@@ -265,7 +283,7 @@ function disableWindows() {
|
|
|
265
283
|
|
|
266
284
|
// ============ Linux ============
|
|
267
285
|
|
|
268
|
-
function enableLinux(cliPath) {
|
|
286
|
+
function enableLinux(cliPath, launchOptions) {
|
|
269
287
|
const autostartDir = path.join(os.homedir(), ".config", "autostart");
|
|
270
288
|
const desktopPath = path.join(autostartDir, `${APP_NAME}.desktop`);
|
|
271
289
|
|
|
@@ -277,12 +295,14 @@ function enableLinux(cliPath) {
|
|
|
277
295
|
const nodePath = process.execPath;
|
|
278
296
|
const routerScript = getCliJsPath(cliPath);
|
|
279
297
|
if (!routerScript) return false;
|
|
298
|
+
const launchArgs = getLaunchArgs(launchOptions);
|
|
299
|
+
const execCommand = [nodePath, routerScript, ...launchArgs].map(quoteDesktopArg).join(" ");
|
|
280
300
|
|
|
281
301
|
const desktopContent = `[Desktop Entry]
|
|
282
302
|
Type=Application
|
|
283
303
|
Name=Switchboard
|
|
284
304
|
Comment=Switchboard API Proxy
|
|
285
|
-
Exec=${
|
|
305
|
+
Exec=${execCommand}
|
|
286
306
|
Hidden=false
|
|
287
307
|
NoDisplay=false
|
|
288
308
|
X-GNOME-Autostart-enabled=true
|
|
@@ -302,5 +322,6 @@ function disableLinux() {
|
|
|
302
322
|
module.exports = {
|
|
303
323
|
enableAutoStart,
|
|
304
324
|
disableAutoStart,
|
|
305
|
-
isAutoStartEnabled
|
|
325
|
+
isAutoStartEnabled,
|
|
326
|
+
getLaunchArgs,
|
|
306
327
|
};
|