clipshot 1.0.8 → 1.0.9

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.
@@ -0,0 +1,31 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '20'
20
+ registry-url: 'https://registry.npmjs.org'
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Build
26
+ run: npm run build
27
+
28
+ - name: Publish to npm
29
+ run: npm publish
30
+ env:
31
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/dist/config.js CHANGED
@@ -99,13 +99,14 @@ function detectSSHRemotes() {
99
99
  }
100
100
  return hosts;
101
101
  }
102
- function detectSSHFromHistory() {
102
+ function detectSSHFromHistory(limit = 5) {
103
103
  const home = os.homedir();
104
104
  const historyFiles = [
105
105
  path.join(home, ".bash_history"),
106
106
  path.join(home, ".zsh_history"),
107
107
  ];
108
- const remotes = new Set();
108
+ // Track remotes in order of most recent appearance
109
+ const remotes = [];
109
110
  for (const histFile of historyFiles) {
110
111
  if (!fs.existsSync(histFile))
111
112
  continue;
@@ -122,7 +123,12 @@ function detectSSHFromHistory() {
122
123
  // Clean up any trailing characters
123
124
  const clean = remote.replace(/[;|&].*$/, "");
124
125
  if (clean.match(/^[\w.-]+@[\w.-]+$/)) {
125
- remotes.add(clean);
126
+ // Remove if exists, then add to end (most recent)
127
+ const idx = remotes.indexOf(clean);
128
+ if (idx !== -1) {
129
+ remotes.splice(idx, 1);
130
+ }
131
+ remotes.push(clean);
126
132
  }
127
133
  }
128
134
  }
@@ -132,5 +138,6 @@ function detectSSHFromHistory() {
132
138
  // Ignore read errors
133
139
  }
134
140
  }
135
- return Array.from(remotes);
141
+ // Return most recent entries (last N items, reversed so most recent is first)
142
+ return remotes.slice(-limit).reverse();
136
143
  }
package/dist/index.js CHANGED
@@ -41,6 +41,69 @@ const child_process_1 = require("child_process");
41
41
  const config_1 = require("./config");
42
42
  const prompts_1 = require("./prompts");
43
43
  const monitor_1 = require("./monitor");
44
+ const isWindows = process.platform === "win32";
45
+ function findClipshotProcesses() {
46
+ const processes = [];
47
+ try {
48
+ if (isWindows) {
49
+ // Use PowerShell to get node processes with command line (WMIC is deprecated)
50
+ const psScript = `$ProgressPreference = 'SilentlyContinue'; Get-CimInstance Win32_Process -Filter "name = 'node.exe'" | Where-Object { $_.CommandLine -like '*clipshot*' -and $_.CommandLine -like '*--daemon*' } | Select-Object ProcessId,CommandLine | ConvertTo-Csv -NoTypeInformation`;
51
+ const encoded = Buffer.from(psScript, "utf16le").toString("base64");
52
+ const result = (0, child_process_1.execSync)(`powershell -NoProfile -WindowStyle Hidden -EncodedCommand ${encoded}`, { encoding: "utf8", windowsHide: true, stdio: ["pipe", "pipe", "pipe"] });
53
+ for (const line of result.split("\n").slice(1)) { // Skip header
54
+ if (!line.trim())
55
+ continue;
56
+ // CSV format: "ProcessId","CommandLine"
57
+ const match = line.match(/"(\d+)","(.*)"/);
58
+ if (match) {
59
+ const pid = parseInt(match[1]);
60
+ if (!isNaN(pid) && pid !== process.pid) {
61
+ processes.push({ pid, command: match[2] });
62
+ }
63
+ }
64
+ }
65
+ }
66
+ else {
67
+ // Unix: use pgrep
68
+ const result = (0, child_process_1.execSync)("pgrep -af 'node.*[c]lipshot.*--daemon'", { encoding: "utf8" });
69
+ for (const line of result.trim().split("\n").filter(Boolean)) {
70
+ const pid = parseInt(line.split(/\s+/)[0]);
71
+ if (!isNaN(pid)) {
72
+ processes.push({ pid, command: line });
73
+ }
74
+ }
75
+ }
76
+ }
77
+ catch {
78
+ // No processes found
79
+ }
80
+ return processes;
81
+ }
82
+ function killProcess(pid, force = false) {
83
+ try {
84
+ if (isWindows) {
85
+ (0, child_process_1.execSync)(`taskkill /PID ${pid}${force ? " /F" : ""}`, { stdio: "pipe", windowsHide: true });
86
+ }
87
+ else {
88
+ process.kill(pid, force ? "SIGKILL" : "SIGTERM");
89
+ }
90
+ }
91
+ catch {
92
+ // Process may have already exited
93
+ }
94
+ }
95
+ function killAllClipshotProcesses() {
96
+ const processes = findClipshotProcesses();
97
+ for (const proc of processes) {
98
+ killProcess(proc.pid);
99
+ }
100
+ // Check if any survived and force kill
101
+ const remaining = findClipshotProcesses();
102
+ for (const proc of remaining) {
103
+ killProcess(proc.pid, true);
104
+ }
105
+ return processes.length;
106
+ }
44
107
  function getVersion() {
45
108
  const pkgPath = path.join(__dirname, "..", "package.json");
46
109
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -53,11 +116,12 @@ async function addRemotes(existing) {
53
116
  // From SSH config
54
117
  const sshHosts = (0, config_1.detectSSHRemotes)();
55
118
  for (const host of sshHosts) {
56
- if (!remotes.includes(host.name)) {
57
- const details = [host.user, host.hostname].filter(Boolean).join("@");
119
+ // Use user@host format if user is specified, otherwise just host name
120
+ const remoteName = host.user ? `${host.user}@${host.name}` : host.name;
121
+ if (!remotes.includes(remoteName)) {
58
122
  allDetected.push({
59
- name: host.name,
60
- source: details ? `config: ${details}` : "config",
123
+ name: remoteName,
124
+ source: "config",
61
125
  });
62
126
  }
63
127
  }
@@ -118,18 +182,9 @@ Run without command to setup/configure.
118
182
  }
119
183
  function uninstall() {
120
184
  // Stop any running process
121
- try {
122
- const result = (0, child_process_1.execSync)("pgrep -f 'node.*[c]lipshot.*--daemon'", { encoding: "utf8" });
123
- const pids = result.trim().split("\n").filter(Boolean);
124
- for (const pid of pids) {
125
- process.kill(parseInt(pid), "SIGTERM");
126
- }
127
- if (pids.length > 0) {
128
- console.log("Stopped running process");
129
- }
130
- }
131
- catch {
132
- // Not running
185
+ const count = killAllClipshotProcesses();
186
+ if (count > 0) {
187
+ console.log("Stopped running process");
133
188
  }
134
189
  // Remove config directory
135
190
  const configDir = path.join(os.homedir(), ".config", "clipshot");
@@ -140,63 +195,30 @@ function uninstall() {
140
195
  console.log("\nNow run: npm uninstall -g clipshot");
141
196
  }
142
197
  function stopBackground() {
143
- try {
144
- // First check if any processes are running
145
- const result = (0, child_process_1.execSync)("pgrep -f 'node.*[c]lipshot.*--daemon'", { encoding: "utf8" });
146
- const pids = result.trim().split("\n").filter(Boolean);
147
- if (pids.length === 0) {
148
- console.log("No clipshot process running");
149
- return;
150
- }
151
- // Use pkill for reliable process termination
152
- try {
153
- (0, child_process_1.execSync)("pkill -f 'node.*clipshot.*--daemon'", { encoding: "utf8" });
154
- }
155
- catch {
156
- // pkill returns non-zero even on success sometimes
157
- }
158
- // Verify they're stopped
159
- try {
160
- (0, child_process_1.execSync)("pgrep -f 'node.*[c]lipshot.*--daemon'", { encoding: "utf8" });
161
- // If we get here, some processes survived - try SIGKILL
162
- (0, child_process_1.execSync)("pkill -9 -f 'node.*clipshot.*--daemon'", { encoding: "utf8" });
163
- }
164
- catch {
165
- // No processes found - good
166
- }
167
- console.log(`Stopped ${pids.length} process(es)`);
198
+ const count = killAllClipshotProcesses();
199
+ if (count > 0) {
200
+ console.log(`Stopped ${count} process(es)`);
168
201
  }
169
- catch {
202
+ else {
170
203
  console.log("No clipshot process running");
171
204
  }
172
205
  }
173
206
  function showStatus() {
174
- try {
175
- // Use bracket trick to avoid pgrep matching itself
176
- const result = (0, child_process_1.execSync)("pgrep -af 'node.*[c]lipshot.*--daemon'", { encoding: "utf8" });
177
- const lines = result.trim().split("\n").filter(Boolean);
178
- if (lines.length > 0) {
179
- for (const line of lines) {
180
- // Parse "PID command args"
181
- const match = line.match(/^(\d+)\s+.*--daemon\s+(.+)$/);
182
- if (match) {
183
- const pid = match[1];
184
- const target = match[2];
185
- console.log(`Running (PID: ${pid}) -> ${target}`);
186
- }
187
- else {
188
- const pid = line.split(/\s+/)[0];
189
- console.log(`Running (PID: ${pid})`);
190
- }
191
- }
207
+ const processes = findClipshotProcesses();
208
+ if (processes.length === 0) {
209
+ console.log("Not running");
210
+ return;
211
+ }
212
+ for (const proc of processes) {
213
+ // Try to extract target from command line
214
+ const match = proc.command.match(/--daemon\s+(\S+)/);
215
+ if (match) {
216
+ console.log(`Running (PID: ${proc.pid}) -> ${match[1]}`);
192
217
  }
193
218
  else {
194
- console.log("Not running");
219
+ console.log(`Running (PID: ${proc.pid})`);
195
220
  }
196
221
  }
197
- catch {
198
- console.log("Not running");
199
- }
200
222
  }
201
223
  async function runConfig() {
202
224
  let config = (0, config_1.loadConfig)();
@@ -243,18 +265,9 @@ async function startCommand() {
243
265
  selected = await (0, prompts_1.promptSelect)("Select target", options);
244
266
  }
245
267
  // Stop any existing process before starting new one
246
- try {
247
- const result = (0, child_process_1.execSync)("pgrep -f 'node.*[c]lipshot.*--daemon'", { encoding: "utf8" });
248
- const pids = result.trim().split("\n").filter(Boolean);
249
- for (const pid of pids) {
250
- process.kill(parseInt(pid), "SIGTERM");
251
- }
252
- if (pids.length > 0) {
253
- console.log(`Stopped previous process`);
254
- }
255
- }
256
- catch {
257
- // Not running, continue
268
+ const count = killAllClipshotProcesses();
269
+ if (count > 0) {
270
+ console.log(`Stopped previous process`);
258
271
  }
259
272
  startBackground(selected);
260
273
  }
package/dist/monitor.js CHANGED
@@ -44,7 +44,11 @@ const LOG_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
44
44
  let lastImageHash = null;
45
45
  let logFile = null;
46
46
  let logStartTime = 0;
47
+ const isWindows = process.platform === "win32";
47
48
  function isWSL() {
49
+ if (isWindows) {
50
+ return false;
51
+ }
48
52
  try {
49
53
  const release = fs.readFileSync("/proc/version", "utf8");
50
54
  return release.toLowerCase().includes("microsoft") || release.toLowerCase().includes("wsl");
@@ -84,7 +88,7 @@ function log(message) {
84
88
  process.stdout.write(message + "\n");
85
89
  }
86
90
  }
87
- async function getClipboardImageWSL() {
91
+ async function getClipboardImageWindows() {
88
92
  try {
89
93
  // PowerShell script to get clipboard image as base64
90
94
  const psScript = `
@@ -98,9 +102,12 @@ if ($img -ne $null) {
98
102
  `;
99
103
  // Encode as UTF-16LE base64 for -EncodedCommand
100
104
  const encoded = Buffer.from(psScript, "utf16le").toString("base64");
101
- const result = (0, child_process_1.execSync)(`powershell.exe -NoProfile -EncodedCommand ${encoded}`, {
105
+ // Use powershell.exe for WSL, powershell for native Windows
106
+ const psCmd = isWindows ? "powershell" : "powershell.exe";
107
+ const result = (0, child_process_1.execSync)(`${psCmd} -NoProfile -WindowStyle Hidden -EncodedCommand ${encoded}`, {
102
108
  encoding: "utf8",
103
109
  timeout: 5000,
110
+ windowsHide: true,
104
111
  }).trim();
105
112
  if (result && result.length > 0) {
106
113
  return Buffer.from(result, "base64");
@@ -131,8 +138,8 @@ async function getClipboardImageNative() {
131
138
  }
132
139
  }
133
140
  async function getClipboardImage() {
134
- if (isWSL()) {
135
- return getClipboardImageWSL();
141
+ if (isWindows || isWSL()) {
142
+ return getClipboardImageWindows();
136
143
  }
137
144
  return getClipboardImageNative();
138
145
  }
@@ -161,39 +168,53 @@ function saveLocal(imageBuffer, filename) {
161
168
  return { success: false, path: filePath };
162
169
  }
163
170
  }
164
- function getRemoteHomeDir(remote) {
171
+ function getRemoteHomePath(remote) {
165
172
  // Extract username from user@host format
166
173
  const match = remote.match(/^([^@]+)@/);
167
- const user = match ? match[1] : "root";
168
- return user === "root" ? "/root" : `/home/${user}`;
174
+ if (match) {
175
+ const user = match[1];
176
+ return user === "root" ? "/root" : `/home/${user}`;
177
+ }
178
+ // Named host without user - fall back to ~
179
+ return "~";
169
180
  }
170
181
  async function pipeToRemote(imageBuffer, remote, filename) {
171
- const homeDir = getRemoteHomeDir(remote);
182
+ const homeDir = getRemoteHomePath(remote);
172
183
  const remotePath = `${homeDir}/clipshot-screenshots/${filename}`;
173
184
  return new Promise((resolve) => {
174
- // Ensure directory exists and write file
175
- // Use ControlMaster options for faster repeated connections
185
+ // Use ~ in the command so SSH resolves it correctly
176
186
  const proc = (0, child_process_1.spawn)("ssh", [
177
- "-o", "ControlMaster=auto",
178
- "-o", "ControlPath=~/.ssh/clipshot-%r@%h:%p",
179
- "-o", "ControlPersist=60",
180
187
  remote,
181
- `mkdir -p ${homeDir}/clipshot-screenshots && cat > ${remotePath}`
182
- ]);
188
+ `mkdir -p ~/clipshot-screenshots && cat > ~/clipshot-screenshots/${filename}`
189
+ ], {
190
+ windowsHide: true,
191
+ });
192
+ let stderr = "";
193
+ proc.stderr.on("data", (data) => {
194
+ stderr += data.toString();
195
+ });
183
196
  proc.stdin.write(imageBuffer);
184
197
  proc.stdin.end();
185
198
  proc.on("close", (code) => {
186
- resolve({ success: code === 0, path: remotePath });
199
+ // Return the explicit path for clipboard, but command used ~ for reliability
200
+ resolve({ success: code === 0, path: remotePath, error: stderr.trim() || undefined });
187
201
  });
188
- proc.on("error", () => {
189
- resolve({ success: false, path: remotePath });
202
+ proc.on("error", (err) => {
203
+ resolve({ success: false, path: remotePath, error: err.message });
190
204
  });
191
205
  });
192
206
  }
193
- function copyToClipboardWSL(text) {
207
+ function copyToClipboardWindows(text) {
194
208
  try {
195
- // Use clip.exe which is much faster than PowerShell
196
- (0, child_process_1.execSync)(`echo -n '${text.replace(/'/g, "'\\''")}' | clip.exe`, { timeout: 2000 });
209
+ if (isWindows) {
210
+ // On native Windows, use PowerShell's Set-Clipboard
211
+ const escaped = text.replace(/'/g, "''");
212
+ (0, child_process_1.execSync)(`powershell -NoProfile -WindowStyle Hidden -Command "Set-Clipboard -Value '${escaped}'"`, { timeout: 2000, windowsHide: true });
213
+ }
214
+ else {
215
+ // On WSL, use clip.exe
216
+ (0, child_process_1.execSync)(`echo -n '${text.replace(/'/g, "'\\''")}' | clip.exe`, { timeout: 2000 });
217
+ }
197
218
  }
198
219
  catch {
199
220
  // Ignore clipboard errors
@@ -210,8 +231,8 @@ async function copyToClipboardNative(text) {
210
231
  }
211
232
  }
212
233
  async function copyToClipboard(text) {
213
- if (isWSL()) {
214
- copyToClipboardWSL(text);
234
+ if (isWindows || isWSL()) {
235
+ copyToClipboardWindows(text);
215
236
  }
216
237
  else {
217
238
  await copyToClipboardNative(text);
@@ -222,8 +243,9 @@ async function startMonitor(remote) {
222
243
  logFile = createNewLogFile();
223
244
  logStartTime = Date.now();
224
245
  const wsl = isWSL();
246
+ const env = isWindows ? "Windows" : (wsl ? "WSL" : "Native");
225
247
  log(`Starting monitor for: ${remote}`);
226
- log(`Environment: ${wsl ? "WSL" : "Native"}`);
248
+ log(`Environment: ${env}`);
227
249
  log(`Log file: ${logFile}`);
228
250
  if (remote === "local") {
229
251
  log(`Saving to: ${getLocalScreenshotDir()}`);
@@ -268,6 +290,9 @@ async function startMonitor(remote) {
268
290
  }
269
291
  else {
270
292
  log(` -> Failed to send to ${remote}`);
293
+ if (result.error) {
294
+ log(` -> Error: ${result.error}`);
295
+ }
271
296
  }
272
297
  }
273
298
  }
package/dist/prompts.js CHANGED
@@ -6,28 +6,63 @@ exports.promptSelect = promptSelect;
6
6
  exports.promptMultiSelect = promptMultiSelect;
7
7
  // @ts-ignore
8
8
  const { Select, Confirm, Input, MultiSelect } = require("enquirer");
9
+ // Suppress enquirer's readline error on Ctrl+C (Node.js 24+ issue)
10
+ process.on("uncaughtException", (err) => {
11
+ if (err.message?.includes("readline was closed")) {
12
+ console.log("\nCancelled");
13
+ process.exit(0);
14
+ }
15
+ throw err;
16
+ });
17
+ function handleCancel() {
18
+ console.log("\nCancelled");
19
+ process.exit(0);
20
+ }
9
21
  async function promptConfirm(message) {
10
- const prompt = new Confirm({ name: "confirm", message });
11
- return prompt.run();
22
+ try {
23
+ const prompt = new Confirm({
24
+ name: "confirm",
25
+ message,
26
+ format: (v) => v ? "Y" : "N",
27
+ });
28
+ return await prompt.run();
29
+ }
30
+ catch {
31
+ handleCancel();
32
+ }
12
33
  }
13
34
  async function promptInput(message) {
14
- const prompt = new Input({ name: "input", message });
15
- return prompt.run();
35
+ try {
36
+ const prompt = new Input({ name: "input", message });
37
+ return await prompt.run();
38
+ }
39
+ catch {
40
+ handleCancel();
41
+ }
16
42
  }
17
43
  async function promptSelect(message, choices) {
18
- const prompt = new Select({ name: "select", message, choices });
19
- return prompt.run();
44
+ try {
45
+ const prompt = new Select({ name: "select", message, choices });
46
+ return await prompt.run();
47
+ }
48
+ catch {
49
+ handleCancel();
50
+ }
20
51
  }
21
52
  async function promptMultiSelect(message, choices) {
22
- const prompt = new MultiSelect({
23
- name: "multiselect",
24
- message,
25
- choices: choices,
26
- initial: choices,
27
- hint: "(space to toggle, enter to confirm)",
28
- indicator(state, choice) {
29
- return choice.enabled ? "●" : "○";
30
- },
31
- });
32
- return prompt.run();
53
+ try {
54
+ const prompt = new MultiSelect({
55
+ name: "multiselect",
56
+ message,
57
+ choices: choices,
58
+ initial: choices,
59
+ indicator(state, choice) {
60
+ return choice.enabled ? "●" : "○";
61
+ },
62
+ });
63
+ return await prompt.run();
64
+ }
65
+ catch {
66
+ handleCancel();
67
+ }
33
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clipshot",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Screenshot monitor CLI tool",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "build": "tsc",
11
+ "dev": "tsc && node dist/index.js",
11
12
  "prepublishOnly": "npm run build"
12
13
  },
13
14
  "keywords": [