[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
463
electron/deepLink.cjs
Normal file
463
electron/deepLink.cjs
Normal file
@@ -0,0 +1,463 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const {
|
||||
parsePuttyCommandLine,
|
||||
redactPuttyCommandLinePasswords,
|
||||
} = require("./puttyCommandLine.cjs");
|
||||
|
||||
const SSH_DEEP_LINK_CHANNEL = "netcatty:deepLink:ssh";
|
||||
const TELNET_DEEP_LINK_CHANNEL = "netcatty:deepLink:telnet";
|
||||
const JMS_DEEP_LINK_CHANNEL = "netcatty:deepLink:jms";
|
||||
const SSH_PROTOCOL = "ssh";
|
||||
const TELNET_PROTOCOL = "telnet";
|
||||
const JMS_PROTOCOL = "jms";
|
||||
const SSH_DEEP_LINK_PREFERENCES_FILE = "ssh-deep-link-preferences.json";
|
||||
const JMS_DEEP_LINK_PREFERENCES_FILE = "jms-deep-link-preferences.json";
|
||||
|
||||
function isDeepLinkUrl(rawUrl, protocol) {
|
||||
if (typeof rawUrl !== "string" || !rawUrl.trim()) return false;
|
||||
return rawUrl.trim().toLowerCase().startsWith(`${protocol}://`);
|
||||
}
|
||||
|
||||
function collectDeepLinkUrls(argv, protocol) {
|
||||
if (!Array.isArray(argv)) return [];
|
||||
return argv.filter((rawUrl) => isDeepLinkUrl(rawUrl, protocol));
|
||||
}
|
||||
|
||||
function isSshDeepLinkUrl(rawUrl) {
|
||||
return isDeepLinkUrl(rawUrl, SSH_PROTOCOL);
|
||||
}
|
||||
|
||||
function isTelnetDeepLinkUrl(rawUrl) {
|
||||
return isDeepLinkUrl(rawUrl, TELNET_PROTOCOL);
|
||||
}
|
||||
|
||||
function isJmsDeepLinkUrl(rawUrl) {
|
||||
return isDeepLinkUrl(rawUrl, JMS_PROTOCOL);
|
||||
}
|
||||
|
||||
function collectSshDeepLinkUrls(argv) {
|
||||
return collectDeepLinkUrls(argv, SSH_PROTOCOL);
|
||||
}
|
||||
|
||||
function collectTelnetDeepLinkUrls(argv) {
|
||||
return collectDeepLinkUrls(argv, TELNET_PROTOCOL);
|
||||
}
|
||||
|
||||
function collectJmsDeepLinkUrls(argv) {
|
||||
return collectDeepLinkUrls(argv, JMS_PROTOCOL);
|
||||
}
|
||||
|
||||
function collectPuttyStyleDeepLinkUrls(argv) {
|
||||
if (
|
||||
collectSshDeepLinkUrls(argv).length > 0
|
||||
|| collectTelnetDeepLinkUrls(argv).length > 0
|
||||
|| collectJmsDeepLinkUrls(argv).length > 0
|
||||
) {
|
||||
return { ssh: [], telnet: [] };
|
||||
}
|
||||
|
||||
const parsed = parsePuttyCommandLine(argv);
|
||||
if (!parsed?.url) return { ssh: [], telnet: [] };
|
||||
if (parsed.protocol === TELNET_PROTOCOL) {
|
||||
return { ssh: [], telnet: [parsed.url] };
|
||||
}
|
||||
return { ssh: [parsed.url], telnet: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the pending-queue items for ssh/telnet connections requested by the
|
||||
* current launch arguments. PuTTY-style CLI args (-ssh user@host -P 22 -pw
|
||||
* pass) are an explicit launch method and must connect regardless of whether
|
||||
* Netcatty is the registered ssh:// protocol client, so those items are only
|
||||
* gated by includeSchemeUrls through the scheme-URL part of the queue.
|
||||
*/
|
||||
function collectSshDeepLinkQueueItems(argv, { includeSchemeUrls = true } = {}) {
|
||||
const puttyStyleDeepLinks = collectPuttyStyleDeepLinkUrls(argv);
|
||||
const queueItems = {
|
||||
ssh: [],
|
||||
telnet: [],
|
||||
};
|
||||
if (includeSchemeUrls) {
|
||||
collectSshDeepLinkUrls(argv).forEach((rawUrl) => {
|
||||
queueItems.ssh.push({ rawUrl, viaCommandLine: false });
|
||||
});
|
||||
collectTelnetDeepLinkUrls(argv).forEach((rawUrl) => {
|
||||
queueItems.telnet.push({ rawUrl, viaCommandLine: false });
|
||||
});
|
||||
}
|
||||
puttyStyleDeepLinks.ssh.forEach((rawUrl) => {
|
||||
queueItems.ssh.push({ rawUrl, viaCommandLine: true });
|
||||
});
|
||||
puttyStyleDeepLinks.telnet.forEach((rawUrl) => {
|
||||
queueItems.telnet.push({ rawUrl, viaCommandLine: true });
|
||||
});
|
||||
return queueItems;
|
||||
}
|
||||
|
||||
function registerProtocolClient({
|
||||
app,
|
||||
protocol,
|
||||
isDev,
|
||||
argv = process.argv,
|
||||
execPath = process.execPath,
|
||||
logWarn = console.warn,
|
||||
} = {}) {
|
||||
if (!app || typeof app.setAsDefaultProtocolClient !== "function") return false;
|
||||
try {
|
||||
if (isDev && process.defaultApp && argv[1]) {
|
||||
return app.setAsDefaultProtocolClient(protocol, execPath, [argv[1]]);
|
||||
}
|
||||
return app.setAsDefaultProtocolClient(protocol);
|
||||
} catch (err) {
|
||||
logWarn(`[Main] Failed to register ${protocol}:// protocol handler:`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function removeProtocolClient({
|
||||
app,
|
||||
protocol,
|
||||
isDev,
|
||||
argv = process.argv,
|
||||
execPath = process.execPath,
|
||||
logWarn = console.warn,
|
||||
} = {}) {
|
||||
if (!app || typeof app.removeAsDefaultProtocolClient !== "function") return false;
|
||||
try {
|
||||
if (isDev && process.defaultApp && argv[1]) {
|
||||
return app.removeAsDefaultProtocolClient(protocol, execPath, [argv[1]]);
|
||||
}
|
||||
return app.removeAsDefaultProtocolClient(protocol);
|
||||
} catch (err) {
|
||||
logWarn(`[Main] Failed to remove ${protocol}:// protocol handler:`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function registerSshProtocolClient(options = {}) {
|
||||
return registerProtocolClient({ ...options, protocol: SSH_PROTOCOL });
|
||||
}
|
||||
|
||||
function registerTelnetProtocolClient(options = {}) {
|
||||
return registerProtocolClient({ ...options, protocol: TELNET_PROTOCOL });
|
||||
}
|
||||
|
||||
function removeSshProtocolClient(options = {}) {
|
||||
return removeProtocolClient({ ...options, protocol: SSH_PROTOCOL });
|
||||
}
|
||||
|
||||
function removeTelnetProtocolClient(options = {}) {
|
||||
return removeProtocolClient({ ...options, protocol: TELNET_PROTOCOL });
|
||||
}
|
||||
|
||||
function registerJmsProtocolClient(options = {}) {
|
||||
return registerProtocolClient({ ...options, protocol: JMS_PROTOCOL });
|
||||
}
|
||||
|
||||
function removeJmsProtocolClient(options = {}) {
|
||||
return removeProtocolClient({ ...options, protocol: JMS_PROTOCOL });
|
||||
}
|
||||
|
||||
function getDeepLinkPreferencePath({ app, preferencesFile, pathModule = path } = {}) {
|
||||
if (!app || typeof app.getPath !== "function") return null;
|
||||
try {
|
||||
return pathModule.join(app.getPath("userData"), preferencesFile);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getSshDeepLinkPreferencePath(options = {}) {
|
||||
return getDeepLinkPreferencePath({
|
||||
...options,
|
||||
preferencesFile: SSH_DEEP_LINK_PREFERENCES_FILE,
|
||||
});
|
||||
}
|
||||
|
||||
function getJmsDeepLinkPreferencePath(options = {}) {
|
||||
return getDeepLinkPreferencePath({
|
||||
...options,
|
||||
preferencesFile: JMS_DEEP_LINK_PREFERENCES_FILE,
|
||||
});
|
||||
}
|
||||
|
||||
function readDeepLinkEnabledPreference({
|
||||
app,
|
||||
preferencesFile,
|
||||
defaultWhenMissing,
|
||||
parseEnabled,
|
||||
fsModule = fs,
|
||||
pathModule = path,
|
||||
logWarn = console.warn,
|
||||
logLabel,
|
||||
} = {}) {
|
||||
const filePath = getDeepLinkPreferencePath({ app, preferencesFile, pathModule });
|
||||
if (!filePath) return defaultWhenMissing;
|
||||
try {
|
||||
if (!fsModule.existsSync(filePath)) return defaultWhenMissing;
|
||||
const parsed = JSON.parse(fsModule.readFileSync(filePath, "utf8"));
|
||||
return parseEnabled(parsed);
|
||||
} catch (err) {
|
||||
logWarn(`[Main] Failed to read ${logLabel} deep link preference:`, err);
|
||||
return defaultWhenMissing;
|
||||
}
|
||||
}
|
||||
|
||||
function readSshDeepLinkEnabledPreference(options = {}) {
|
||||
return readDeepLinkEnabledPreference({
|
||||
...options,
|
||||
preferencesFile: SSH_DEEP_LINK_PREFERENCES_FILE,
|
||||
defaultWhenMissing: true,
|
||||
parseEnabled: (parsed) => parsed?.enabled !== false,
|
||||
logLabel: "ssh://",
|
||||
});
|
||||
}
|
||||
|
||||
function readJmsDeepLinkEnabledPreference(options = {}) {
|
||||
return readDeepLinkEnabledPreference({
|
||||
...options,
|
||||
preferencesFile: JMS_DEEP_LINK_PREFERENCES_FILE,
|
||||
defaultWhenMissing: false,
|
||||
parseEnabled: (parsed) => parsed?.enabled === true,
|
||||
logLabel: "jms://",
|
||||
});
|
||||
}
|
||||
|
||||
function writeDeepLinkEnabledPreference({
|
||||
app,
|
||||
enabled,
|
||||
preferencesFile,
|
||||
fsModule = fs,
|
||||
pathModule = path,
|
||||
logWarn = console.warn,
|
||||
logLabel,
|
||||
} = {}) {
|
||||
const filePath = getDeepLinkPreferencePath({ app, preferencesFile, pathModule });
|
||||
if (!filePath) return false;
|
||||
try {
|
||||
fsModule.mkdirSync(pathModule.dirname(filePath), { recursive: true });
|
||||
fsModule.writeFileSync(filePath, JSON.stringify({ enabled: enabled !== false }, null, 2));
|
||||
return true;
|
||||
} catch (err) {
|
||||
logWarn(`[Main] Failed to write ${logLabel} deep link preference:`, err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function writeSshDeepLinkEnabledPreference(options = {}) {
|
||||
return writeDeepLinkEnabledPreference({
|
||||
...options,
|
||||
preferencesFile: SSH_DEEP_LINK_PREFERENCES_FILE,
|
||||
logLabel: "ssh://",
|
||||
});
|
||||
}
|
||||
|
||||
function writeJmsDeepLinkEnabledPreference(options = {}) {
|
||||
return writeDeepLinkEnabledPreference({
|
||||
...options,
|
||||
preferencesFile: JMS_DEEP_LINK_PREFERENCES_FILE,
|
||||
logLabel: "jms://",
|
||||
});
|
||||
}
|
||||
|
||||
function applyDeepLinkProtocolClientPreference({
|
||||
enabled,
|
||||
registerClient,
|
||||
removeClient,
|
||||
options = {},
|
||||
} = {}) {
|
||||
if (enabled === false) {
|
||||
return removeClient(options);
|
||||
}
|
||||
return registerClient(options);
|
||||
}
|
||||
|
||||
function applySshProtocolClientPreference(options = {}) {
|
||||
const sshApplied = applyDeepLinkProtocolClientPreference({
|
||||
enabled: options.enabled,
|
||||
registerClient: registerSshProtocolClient,
|
||||
removeClient: removeSshProtocolClient,
|
||||
options,
|
||||
});
|
||||
const telnetApplied = applyDeepLinkProtocolClientPreference({
|
||||
enabled: options.enabled,
|
||||
registerClient: registerTelnetProtocolClient,
|
||||
removeClient: removeTelnetProtocolClient,
|
||||
options,
|
||||
});
|
||||
if (sshApplied !== true && telnetApplied === true) {
|
||||
applyDeepLinkProtocolClientPreference({
|
||||
enabled: options.enabled === false,
|
||||
registerClient: registerTelnetProtocolClient,
|
||||
removeClient: removeTelnetProtocolClient,
|
||||
options,
|
||||
});
|
||||
}
|
||||
return sshApplied === true;
|
||||
}
|
||||
|
||||
function applyJmsProtocolClientPreference(options = {}) {
|
||||
return applyDeepLinkProtocolClientPreference({
|
||||
enabled: options.enabled,
|
||||
registerClient: registerJmsProtocolClient,
|
||||
removeClient: removeJmsProtocolClient,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
function updateDeepLinkEnabledPreference({
|
||||
currentEnabled = true,
|
||||
enabled = true,
|
||||
applyPreference = () => false,
|
||||
writePreference = () => false,
|
||||
clearPending,
|
||||
} = {}) {
|
||||
const nextEnabled = enabled !== false;
|
||||
if (nextEnabled === currentEnabled) {
|
||||
return { enabled: currentEnabled, success: true };
|
||||
}
|
||||
|
||||
const success = applyPreference(nextEnabled) === true;
|
||||
if (!success) {
|
||||
return { enabled: currentEnabled, success: false };
|
||||
}
|
||||
|
||||
const writeSucceeded = writePreference(nextEnabled) === true;
|
||||
if (!writeSucceeded) {
|
||||
const rolledBack = applyPreference(currentEnabled) === true;
|
||||
const finalEnabled = rolledBack ? currentEnabled : nextEnabled;
|
||||
if (!finalEnabled) {
|
||||
clearPending?.();
|
||||
}
|
||||
return { enabled: finalEnabled, success: false };
|
||||
}
|
||||
if (!nextEnabled) {
|
||||
clearPending?.();
|
||||
}
|
||||
return { enabled: nextEnabled, success: true };
|
||||
}
|
||||
|
||||
function updateSshDeepLinkEnabledPreference(options = {}) {
|
||||
return updateDeepLinkEnabledPreference(options);
|
||||
}
|
||||
|
||||
function updateJmsDeepLinkEnabledPreference(options = {}) {
|
||||
return updateDeepLinkEnabledPreference(options);
|
||||
}
|
||||
|
||||
function shouldDeliverDeepLink({ enabled = true, deliveryGeneration = 0, expectedGeneration = 0 } = {}) {
|
||||
return enabled !== false && deliveryGeneration === expectedGeneration;
|
||||
}
|
||||
|
||||
/**
|
||||
* App Lock can suppress rendererReady until unlock. A short readiness timeout
|
||||
* would shift the URL out of the pending queue and drop it if the user takes
|
||||
* longer than that to unlock. Prefer waiting indefinitely for readiness (or
|
||||
* window death); callers re-queue only on non-cancel failures.
|
||||
*/
|
||||
function getSshDeepLinkRendererReadyTimeoutMs({ isDev = false } = {}) {
|
||||
void isDev;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function shouldRequeueFailedSshDeepLinkDelivery({
|
||||
enabled = true,
|
||||
deliveryGeneration = 0,
|
||||
expectedGeneration = 0,
|
||||
result = null,
|
||||
cancelReason = "ssh-deep-link-disabled",
|
||||
} = {}) {
|
||||
if (!shouldDeliverSshDeepLink({ enabled, deliveryGeneration, expectedGeneration })) {
|
||||
return false;
|
||||
}
|
||||
if (!result || result.success === true) return false;
|
||||
if (result.reason === cancelReason) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function shouldDeliverSshDeepLink(options = {}) {
|
||||
return shouldDeliverDeepLink(options);
|
||||
}
|
||||
|
||||
function shouldDeliverTelnetDeepLink(options = {}) {
|
||||
return shouldDeliverDeepLink(options);
|
||||
}
|
||||
|
||||
function shouldDeliverJmsDeepLink(options = {}) {
|
||||
return shouldDeliverDeepLink(options);
|
||||
}
|
||||
|
||||
function applyInitialDeepLinkPreference({
|
||||
enabled = true,
|
||||
applyPreference = () => false,
|
||||
clearPending,
|
||||
logWarn = console.warn,
|
||||
logLabel,
|
||||
warnOnFailure = true,
|
||||
} = {}) {
|
||||
const requestedEnabled = enabled !== false;
|
||||
const success = applyPreference(requestedEnabled) === true;
|
||||
if (success) {
|
||||
return { enabled: requestedEnabled, success: true };
|
||||
}
|
||||
if (warnOnFailure && requestedEnabled) {
|
||||
logWarn(`[Main] Failed to apply saved ${logLabel} deep link preference.`);
|
||||
}
|
||||
if (requestedEnabled) {
|
||||
clearPending?.();
|
||||
}
|
||||
return { enabled: false, success: false };
|
||||
}
|
||||
|
||||
function applyInitialSshDeepLinkPreference(options = {}) {
|
||||
return applyInitialDeepLinkPreference({
|
||||
...options,
|
||||
logLabel: "ssh://",
|
||||
warnOnFailure: true,
|
||||
});
|
||||
}
|
||||
|
||||
function applyInitialJmsDeepLinkPreference(options = {}) {
|
||||
return applyInitialDeepLinkPreference({
|
||||
...options,
|
||||
logLabel: "jms://",
|
||||
warnOnFailure: false,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SSH_DEEP_LINK_CHANNEL,
|
||||
TELNET_DEEP_LINK_CHANNEL,
|
||||
JMS_DEEP_LINK_CHANNEL,
|
||||
applyInitialJmsDeepLinkPreference,
|
||||
applyInitialSshDeepLinkPreference,
|
||||
applyJmsProtocolClientPreference,
|
||||
applySshProtocolClientPreference,
|
||||
getSshDeepLinkRendererReadyTimeoutMs,
|
||||
collectJmsDeepLinkUrls,
|
||||
collectPuttyStyleDeepLinkUrls,
|
||||
collectSshDeepLinkQueueItems,
|
||||
collectSshDeepLinkUrls,
|
||||
collectTelnetDeepLinkUrls,
|
||||
redactPuttyCommandLinePasswords,
|
||||
isJmsDeepLinkUrl,
|
||||
isSshDeepLinkUrl,
|
||||
isTelnetDeepLinkUrl,
|
||||
readJmsDeepLinkEnabledPreference,
|
||||
readSshDeepLinkEnabledPreference,
|
||||
registerJmsProtocolClient,
|
||||
registerSshProtocolClient,
|
||||
registerTelnetProtocolClient,
|
||||
removeJmsProtocolClient,
|
||||
removeSshProtocolClient,
|
||||
removeTelnetProtocolClient,
|
||||
shouldDeliverJmsDeepLink,
|
||||
shouldDeliverSshDeepLink,
|
||||
shouldDeliverTelnetDeepLink,
|
||||
shouldRequeueFailedSshDeepLinkDelivery,
|
||||
updateJmsDeepLinkEnabledPreference,
|
||||
updateSshDeepLinkEnabledPreference,
|
||||
writeJmsDeepLinkEnabledPreference,
|
||||
writeSshDeepLinkEnabledPreference,
|
||||
};
|
||||
Reference in New Issue
Block a user