Compare commits

...

3 commits

Author SHA1 Message Date
2227cb60fe Merge feature_comprehensive_fixes into vibes 2026-05-21 06:38:23 +00:00
701daa9a39 refactor: replace detectClipboard with try-fallback-cached clipboard strategy
- Remove detectClipboard Process and clipboardCmd property
- Add try-fallback-cached copy logic: attempt cached tool first,
  then cycle through hardcoded safe tools (dms, wl-copy, clipmanctl)
- Add stderr StdioCollector to all Process components for error details
- Update lib.js: replace parseClipboardDetection with validateClipboardTool,
  buildCopyCommand, nextClipboardTool, allClipboardTools
- Update tests to match new lib.js API

Benefits:
- No startup delay from clipboard detection process
- Robust fallback if clipboard tool becomes unavailable
- Better error messages with stderr details from failed commands
2026-05-21 06:37:14 +00:00
cac0ae6bde Merge branch 'feature_sanitize_shell_commands' into vibes 2026-05-21 05:11:17 +00:00
3 changed files with 99 additions and 75 deletions

View file

@ -15,7 +15,10 @@ PluginComponent {
property string tailscaleIP: "" property string tailscaleIP: ""
property string currentExitNode: "" property string currentExitNode: ""
property var peers: [] property var peers: []
property string clipboardCmd: "" property string cachedClipboardTool: ""
property string _copyText: ""
property string _copyCurrentTool: ""
property int _copyAttempted: 0
layerNamespacePlugin: "tailscalectl" layerNamespacePlugin: "tailscalectl"
popoutWidth: 360 popoutWidth: 360
@ -29,16 +32,19 @@ PluginComponent {
} }
Component.onCompleted: { Component.onCompleted: {
detectClipboard.running = true statusCheck.running = true
} }
Process { Process {
id: toggleProcess id: toggleProcess
stderr: StdioCollector {}
onExited: (code, status) => { onExited: (code, status) => {
if (code !== 0) { if (code !== 0) {
var action = root.isConnected ? "disconnect" : "connect" var action = root.isConnected ? "disconnect" : "connect"
ToastService.showError("tailscalectl", TailscaleLib.errorMessage(action)) var detail = toggleProcess.stderr.text.trim().slice(0, 120)
ToastService.showError("tailscalectl", TailscaleLib.errorMessage(action) + (detail ? " — " + detail : ""))
} }
statusCheck.running = true statusCheck.running = true
} }
@ -46,35 +52,40 @@ PluginComponent {
Process { Process {
id: copyProcess id: copyProcess
stderr: StdioCollector {}
onExited: (code, status) => {
if (code === 0) {
root.cachedClipboardTool = root._copyCurrentTool
ToastService.showInfo("Copied " + root._copyText + " to clipboard")
} else {
root._copyAttempted += 1
if (root._copyAttempted < TailscaleLib.allClipboardTools().length) {
root._copyCurrentTool = TailscaleLib.nextClipboardTool(root._copyCurrentTool)
root._executeCopy()
} else {
var detail = copyProcess.stderr.text.trim().slice(0, 120)
ToastService.showError("tailscalectl", "No clipboard tool found" + (detail ? " — " + detail : ""))
}
}
}
} }
Process { Process {
id: exitNodeProcess id: exitNodeProcess
stderr: StdioCollector {}
onExited: (code, status) => { onExited: (code, status) => {
if (code !== 0) { if (code !== 0) {
ToastService.showError("tailscalectl", TailscaleLib.errorMessage("set")) var detail = exitNodeProcess.stderr.text.trim().slice(0, 120)
ToastService.showError("tailscalectl", TailscaleLib.errorMessage("set") + (detail ? " — " + detail : ""))
} }
statusCheck.running = true statusCheck.running = true
} }
} }
Process {
id: detectClipboard
command: ["sh", "-c", "if which dms >/dev/null 2>&1; then echo 'dms cl copy'; elif which wl-copy >/dev/null 2>&1; then echo 'wl-copy'; elif which clipmanctl >/dev/null 2>&1; then echo 'clipmanctl copy'; elif which xclip >/dev/null 2>&1; then echo 'xclip -selection clipboard'; elif which xsel >/dev/null 2>&1; then echo 'xsel --clipboard --input'; else echo none; fi"]
stdout: StdioCollector {
onStreamFinished: {
root.clipboardCmd = TailscaleLib.parseClipboardDetection(this.text)
}
}
onExited: (code, status) => {
statusCheck.running = true
}
}
Process { Process {
id: statusCheck id: statusCheck
@ -125,18 +136,24 @@ PluginComponent {
} }
function copyToClipboard(text) { function copyToClipboard(text) {
if (!root.clipboardCmd || root.clipboardCmd === "none") { root._copyText = text
ToastService.showError("tailscalectl", "No clipboard tool found") root._copyAttempted = 0
return if (root.cachedClipboardTool) {
root._copyCurrentTool = root.cachedClipboardTool
} else {
root._copyCurrentTool = TailscaleLib.allClipboardTools()[0]
} }
var cmd = TailscaleLib.buildCopyCommand(text, root.clipboardCmd) root._executeCopy()
}
function _executeCopy() {
var cmd = TailscaleLib.buildCopyCommand(root._copyText, root._copyCurrentTool)
if (!cmd) { if (!cmd) {
ToastService.showError("tailscalectl", "Invalid clipboard command") ToastService.showError("tailscalectl", "Invalid clipboard command")
return return
} }
copyProcess.command = cmd copyProcess.command = cmd
copyProcess.running = true copyProcess.running = true
ToastService.showInfo("Copied " + text + " to clipboard")
} }
popoutContent: Component { popoutContent: Component {

View file

@ -26,16 +26,34 @@ function findActiveExitNode(peerMap) {
return "" return ""
} }
var safeClipboardCmds = ["dms cl copy", "wl-copy", "clipmanctl copy", "xclip -selection clipboard", "xsel --clipboard --input", "none"] var safeClipboardTools = ["dms", "wl-copy", "clipmanctl"]
function validateClipboardCmd(cmd) { var clipboardCmdMap = {
return typeof cmd === "string" && safeClipboardCmds.includes(cmd) "dms": "dms cl copy",
"wl-copy": "wl-copy",
"clipmanctl": "clipmanctl copy"
} }
function buildCopyCommand(text, clipboardCmd) { function validateClipboardTool(tool) {
if (!validateClipboardCmd(clipboardCmd)) return null return typeof tool === "string" && safeClipboardTools.includes(tool)
}
function buildCopyCommand(text, tool) {
if (!validateClipboardTool(tool)) return null
var cmd = clipboardCmdMap[tool]
if (!cmd) return null
var escaped = text.replace(/'/g, "'\\''") var escaped = text.replace(/'/g, "'\\''")
return ["sh", "-c", "printf '%s' '" + escaped + "' | " + clipboardCmd] return ["sh", "-c", "printf '%s' '" + escaped + "' | " + cmd]
}
function nextClipboardTool(currentTool) {
var idx = safeClipboardTools.indexOf(currentTool)
if (idx < 0) return safeClipboardTools[0]
return safeClipboardTools[(idx + 1) % safeClipboardTools.length]
}
function allClipboardTools() {
return safeClipboardTools.slice()
} }
function parseStatusResult(jsonText) { function parseStatusResult(jsonText) {
@ -56,11 +74,6 @@ function buildToggleCommand(isConnected) {
return isConnected ? ["tailscale", "down"] : ["tailscale", "up"] return isConnected ? ["tailscale", "down"] : ["tailscale", "up"]
} }
function parseClipboardDetection(stdout) {
var trimmed = typeof stdout === "string" ? stdout.trim() : ""
return validateClipboardCmd(trimmed) ? trimmed : "none"
}
function errorMessage(cmd) { function errorMessage(cmd) {
var messages = { var messages = {
"up": "Failed to connect to Tailscale", "up": "Failed to connect to Tailscale",
@ -75,5 +88,5 @@ function errorMessage(cmd) {
// CommonJS export for Node.js tests (ignored by QML) // CommonJS export for Node.js tests (ignored by QML)
if (typeof module !== "undefined" && module.exports) { if (typeof module !== "undefined" && module.exports) {
module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand, parseClipboardDetection, buildToggleCommand, parseStatusResult } module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult }
} }

View file

@ -1,7 +1,7 @@
import { test } from "node:test" import { test } from "node:test"
import assert from "node:assert" import assert from "node:assert"
import lib from "../tailscalectl/lib.js" import lib from "../tailscalectl/lib.js"
const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand, parseClipboardDetection, buildToggleCommand, parseStatusResult } = lib const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardTool, buildCopyCommand, nextClipboardTool, allClipboardTools, buildToggleCommand, parseStatusResult } = lib
test("parsePeers extracts exitNode from ExitNodeOption", () => { test("parsePeers extracts exitNode from ExitNodeOption", () => {
const peerMap = { const peerMap = {
@ -75,28 +75,25 @@ test("errorMessage returns generic message for unknown command", () => {
assert.strictEqual(msg, "Tailscale command failed") assert.strictEqual(msg, "Tailscale command failed")
}) })
// --- validateClipboardCmd --- // --- validateClipboardTool ---
test("validateClipboardCmd accepts whitelisted values", () => { test("validateClipboardTool accepts whitelisted tool names", () => {
assert.strictEqual(validateClipboardCmd("dms cl copy"), true) assert.strictEqual(validateClipboardTool("dms"), true)
assert.strictEqual(validateClipboardCmd("wl-copy"), true) assert.strictEqual(validateClipboardTool("wl-copy"), true)
assert.strictEqual(validateClipboardCmd("clipmanctl copy"), true) assert.strictEqual(validateClipboardTool("clipmanctl"), true)
assert.strictEqual(validateClipboardCmd("xclip -selection clipboard"), true)
assert.strictEqual(validateClipboardCmd("xsel --clipboard --input"), true)
assert.strictEqual(validateClipboardCmd("none"), true)
}) })
test("validateClipboardCmd rejects malicious inputs", () => { test("validateClipboardTool rejects malicious inputs", () => {
assert.strictEqual(validateClipboardCmd("rm -rf /"), false) assert.strictEqual(validateClipboardTool("rm -rf /"), false)
assert.strictEqual(validateClipboardCmd("echo hi; malicious"), false) assert.strictEqual(validateClipboardTool("echo hi; malicious"), false)
assert.strictEqual(validateClipboardCmd("$(whoami)"), false) assert.strictEqual(validateClipboardTool("$(whoami)"), false)
assert.strictEqual(validateClipboardCmd("wl-copy || rm -rf /"), false) assert.strictEqual(validateClipboardTool("wl-copy || rm -rf /"), false)
}) })
test("validateClipboardCmd rejects empty and falsy inputs", () => { test("validateClipboardTool rejects empty and falsy inputs", () => {
assert.strictEqual(validateClipboardCmd(""), false) assert.strictEqual(validateClipboardTool(""), false)
assert.strictEqual(validateClipboardCmd(null), false) assert.strictEqual(validateClipboardTool(null), false)
assert.strictEqual(validateClipboardCmd(undefined), false) assert.strictEqual(validateClipboardTool(undefined), false)
}) })
// --- buildCopyCommand --- // --- buildCopyCommand ---
@ -108,7 +105,7 @@ test("buildCopyCommand returns safe command for whitelisted clipboard tool", ()
assert.strictEqual(cmd[1], "-c") assert.strictEqual(cmd[1], "-c")
}) })
test("buildCopyCommand returns null for invalid clipboard command", () => { test("buildCopyCommand returns null for invalid clipboard tool", () => {
assert.strictEqual(buildCopyCommand("hello", "rm -rf /"), null) assert.strictEqual(buildCopyCommand("hello", "rm -rf /"), null)
assert.strictEqual(buildCopyCommand("hello", ""), null) assert.strictEqual(buildCopyCommand("hello", ""), null)
assert.strictEqual(buildCopyCommand("hello", null), null) assert.strictEqual(buildCopyCommand("hello", null), null)
@ -119,31 +116,28 @@ test("buildCopyCommand safely handles text with special characters", () => {
assert.ok(Array.isArray(cmd)) assert.ok(Array.isArray(cmd))
}) })
// --- parseClipboardDetection --- // --- nextClipboardTool ---
test("parseClipboardDetection passes through expected output strings", () => { test("nextClipboardTool cycles through tools", () => {
assert.strictEqual(parseClipboardDetection("dms cl copy"), "dms cl copy") assert.strictEqual(nextClipboardTool("dms"), "wl-copy")
assert.strictEqual(parseClipboardDetection("wl-copy"), "wl-copy") assert.strictEqual(nextClipboardTool("wl-copy"), "clipmanctl")
assert.strictEqual(parseClipboardDetection("clipmanctl copy"), "clipmanctl copy") assert.strictEqual(nextClipboardTool("clipmanctl"), "dms")
assert.strictEqual(parseClipboardDetection("none"), "none")
}) })
test("parseClipboardDetection trims extra whitespace", () => { test("nextClipboardTool falls back to first tool for unknown input", () => {
assert.strictEqual(parseClipboardDetection(" wl-copy \n"), "wl-copy") assert.strictEqual(nextClipboardTool(""), "dms")
assert.strictEqual(nextClipboardTool("unknown"), "dms")
}) })
test("parseClipboardDetection falls back to none for multi-line output", () => { // --- allClipboardTools ---
assert.strictEqual(parseClipboardDetection("wl-copy\nrm -rf /"), "none")
})
test("parseClipboardDetection falls back to empty output", () => { test("allClipboardTools returns the list of safe tools", () => {
assert.strictEqual(parseClipboardDetection(""), "none") const tools = allClipboardTools()
assert.strictEqual(parseClipboardDetection(" "), "none") assert.ok(Array.isArray(tools))
}) assert.strictEqual(tools.length, 3)
assert.ok(tools.includes("dms"))
test("parseClipboardDetection falls back to none for unexpected strings", () => { assert.ok(tools.includes("wl-copy"))
assert.strictEqual(parseClipboardDetection("pbpaste"), "none") assert.ok(tools.includes("clipmanctl"))
assert.strictEqual(parseClipboardDetection("custom-tool"), "none")
}) })
// --- buildToggleCommand --- // --- buildToggleCommand ---