shotmon-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/config.js +136 -0
- package/dist/index.js +293 -0
- package/dist/monitor.js +283 -0
- package/dist/prompts.js +33 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# shotmon
|
|
2
|
+
|
|
3
|
+
Screenshot monitor CLI. Watches clipboard for screenshots and uploads to remote server via SSH, or saves locally.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g shotmon
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
shotmon Setup config and start monitoring
|
|
15
|
+
shotmon start Start monitoring (select target)
|
|
16
|
+
shotmon stop Stop monitoring
|
|
17
|
+
shotmon status Show running status and target
|
|
18
|
+
shotmon config Modify remotes configuration
|
|
19
|
+
shotmon uninstall Remove config files
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- Auto-detects SSH remotes from `~/.ssh/config` and shell history
|
|
25
|
+
- **Local mode**: Saves to `~/shotmon-screenshots/`, copies path to clipboard
|
|
26
|
+
- **Remote mode**: Uploads via SSH, copies remote path to clipboard
|
|
27
|
+
- Fast SSH with ControlMaster connection reuse
|
|
28
|
+
- WSL support (reads Windows clipboard)
|
|
29
|
+
|
|
30
|
+
## How it works
|
|
31
|
+
|
|
32
|
+
1. Polls clipboard for new images (200ms interval)
|
|
33
|
+
2. Detects changes via MD5 hash comparison
|
|
34
|
+
3. Uploads via SSH or saves locally
|
|
35
|
+
4. Copies absolute path to clipboard for easy pasting
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getConfigPath = getConfigPath;
|
|
37
|
+
exports.loadConfig = loadConfig;
|
|
38
|
+
exports.saveConfig = saveConfig;
|
|
39
|
+
exports.detectSSHRemotes = detectSSHRemotes;
|
|
40
|
+
exports.detectSSHFromHistory = detectSSHFromHistory;
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
function getConfigPath() {
|
|
45
|
+
return path.join(os.homedir(), ".config", "shotmon", "config.json");
|
|
46
|
+
}
|
|
47
|
+
function loadConfig() {
|
|
48
|
+
const configPath = getConfigPath();
|
|
49
|
+
if (!fs.existsSync(configPath)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
53
|
+
return JSON.parse(content);
|
|
54
|
+
}
|
|
55
|
+
function saveConfig(config) {
|
|
56
|
+
const configPath = getConfigPath();
|
|
57
|
+
const configDir = path.dirname(configPath);
|
|
58
|
+
if (!fs.existsSync(configDir)) {
|
|
59
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
62
|
+
}
|
|
63
|
+
function detectSSHRemotes() {
|
|
64
|
+
const home = os.homedir();
|
|
65
|
+
const sshConfigPath = path.join(home, ".ssh", "config");
|
|
66
|
+
if (!fs.existsSync(sshConfigPath)) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const content = fs.readFileSync(sshConfigPath, "utf-8");
|
|
70
|
+
const lines = content.split("\n");
|
|
71
|
+
const hosts = [];
|
|
72
|
+
let currentHost = null;
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (trimmed.toLowerCase().startsWith("host ")) {
|
|
76
|
+
if (currentHost) {
|
|
77
|
+
hosts.push(currentHost);
|
|
78
|
+
}
|
|
79
|
+
const hostName = trimmed.slice(5).trim().split(/\s+/)[0];
|
|
80
|
+
// Skip wildcard patterns
|
|
81
|
+
if (!hostName.includes("*")) {
|
|
82
|
+
currentHost = { name: hostName };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
currentHost = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (currentHost) {
|
|
89
|
+
if (trimmed.toLowerCase().startsWith("hostname ")) {
|
|
90
|
+
currentHost.hostname = trimmed.slice(9).trim();
|
|
91
|
+
}
|
|
92
|
+
else if (trimmed.toLowerCase().startsWith("user ")) {
|
|
93
|
+
currentHost.user = trimmed.slice(5).trim();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (currentHost) {
|
|
98
|
+
hosts.push(currentHost);
|
|
99
|
+
}
|
|
100
|
+
return hosts;
|
|
101
|
+
}
|
|
102
|
+
function detectSSHFromHistory() {
|
|
103
|
+
const home = os.homedir();
|
|
104
|
+
const historyFiles = [
|
|
105
|
+
path.join(home, ".bash_history"),
|
|
106
|
+
path.join(home, ".zsh_history"),
|
|
107
|
+
];
|
|
108
|
+
const remotes = new Set();
|
|
109
|
+
for (const histFile of historyFiles) {
|
|
110
|
+
if (!fs.existsSync(histFile))
|
|
111
|
+
continue;
|
|
112
|
+
try {
|
|
113
|
+
const content = fs.readFileSync(histFile, "utf-8");
|
|
114
|
+
const lines = content.split("\n");
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
// Match ssh commands: ssh [options] user@host or ssh [options] host
|
|
117
|
+
const match = line.match(/\bssh\s+(?:[^@\s]+\s+)*?(\S+@\S+?)(?:\s|$)/);
|
|
118
|
+
if (match) {
|
|
119
|
+
const remote = match[1];
|
|
120
|
+
// Filter out flags and invalid entries
|
|
121
|
+
if (remote.includes("@") && !remote.startsWith("-")) {
|
|
122
|
+
// Clean up any trailing characters
|
|
123
|
+
const clean = remote.replace(/[;|&].*$/, "");
|
|
124
|
+
if (clean.match(/^[\w.-]+@[\w.-]+$/)) {
|
|
125
|
+
remotes.add(clean);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Ignore read errors
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return Array.from(remotes);
|
|
136
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const config_1 = require("./config");
|
|
42
|
+
const prompts_1 = require("./prompts");
|
|
43
|
+
const monitor_1 = require("./monitor");
|
|
44
|
+
function getVersion() {
|
|
45
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
46
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
47
|
+
return pkg.version;
|
|
48
|
+
}
|
|
49
|
+
async function addRemotes(existing) {
|
|
50
|
+
const remotes = [...existing];
|
|
51
|
+
// Collect all detected remotes
|
|
52
|
+
const allDetected = [];
|
|
53
|
+
// From SSH config
|
|
54
|
+
const sshHosts = (0, config_1.detectSSHRemotes)();
|
|
55
|
+
for (const host of sshHosts) {
|
|
56
|
+
if (!remotes.includes(host.name)) {
|
|
57
|
+
const details = [host.user, host.hostname].filter(Boolean).join("@");
|
|
58
|
+
allDetected.push({
|
|
59
|
+
name: host.name,
|
|
60
|
+
source: details ? `config: ${details}` : "config",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// From bash/zsh history
|
|
65
|
+
const historyRemotes = (0, config_1.detectSSHFromHistory)();
|
|
66
|
+
for (const remote of historyRemotes) {
|
|
67
|
+
if (!remotes.includes(remote) && !allDetected.find(d => d.name === remote)) {
|
|
68
|
+
allDetected.push({
|
|
69
|
+
name: remote,
|
|
70
|
+
source: "history",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (allDetected.length > 0) {
|
|
75
|
+
const choices = allDetected.map(d => `${d.name} (${d.source})`);
|
|
76
|
+
const selected = await (0, prompts_1.promptMultiSelect)("Select SSH remotes to add (space to toggle, enter to confirm)", choices);
|
|
77
|
+
for (const msg of selected) {
|
|
78
|
+
const detected = allDetected.find(d => `${d.name} (${d.source})` === msg);
|
|
79
|
+
if (detected) {
|
|
80
|
+
remotes.push(detected.name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Add custom remotes
|
|
85
|
+
let addMore = await (0, prompts_1.promptConfirm)("Add a custom SSH remote?");
|
|
86
|
+
while (addMore) {
|
|
87
|
+
const remoteName = await (0, prompts_1.promptInput)("Enter SSH remote (e.g., user@host)");
|
|
88
|
+
if (remoteName && !remotes.includes(remoteName)) {
|
|
89
|
+
remotes.push(remoteName);
|
|
90
|
+
console.log(`Added: ${remoteName}`);
|
|
91
|
+
}
|
|
92
|
+
addMore = await (0, prompts_1.promptConfirm)("Add another?");
|
|
93
|
+
}
|
|
94
|
+
return remotes;
|
|
95
|
+
}
|
|
96
|
+
function startBackground(remote) {
|
|
97
|
+
const child = (0, child_process_1.spawn)(process.execPath, [__filename, "--daemon", remote], {
|
|
98
|
+
detached: true,
|
|
99
|
+
stdio: "ignore",
|
|
100
|
+
env: { ...process.env, SHOTMON_BACKGROUND: "1" },
|
|
101
|
+
});
|
|
102
|
+
child.unref();
|
|
103
|
+
console.log(`Started in background (PID: ${child.pid})`);
|
|
104
|
+
console.log(`Logs: ~/.config/shotmon/logs/`);
|
|
105
|
+
}
|
|
106
|
+
function showHelp() {
|
|
107
|
+
console.log(`Usage: shotmon <command>
|
|
108
|
+
|
|
109
|
+
Commands:
|
|
110
|
+
start Start monitoring in background
|
|
111
|
+
stop Stop background process
|
|
112
|
+
status Show if running
|
|
113
|
+
config Modify configuration
|
|
114
|
+
uninstall Remove config and stop process
|
|
115
|
+
|
|
116
|
+
Run without command to setup/configure.
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
function uninstall() {
|
|
120
|
+
// Stop any running process
|
|
121
|
+
try {
|
|
122
|
+
const result = (0, child_process_1.execSync)("pgrep -f 'node.*[s]hotmon.*--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
|
|
133
|
+
}
|
|
134
|
+
// Remove config directory
|
|
135
|
+
const configDir = path.join(os.homedir(), ".config", "shotmon");
|
|
136
|
+
if (fs.existsSync(configDir)) {
|
|
137
|
+
fs.rmSync(configDir, { recursive: true });
|
|
138
|
+
console.log(`Removed ${configDir}`);
|
|
139
|
+
}
|
|
140
|
+
console.log("\nNow run: npm uninstall -g shotmon");
|
|
141
|
+
}
|
|
142
|
+
function stopBackground() {
|
|
143
|
+
try {
|
|
144
|
+
// Use bracket trick to avoid pgrep matching itself
|
|
145
|
+
const result = (0, child_process_1.execSync)("pgrep -f 'node.*[s]hotmon.*--daemon'", { encoding: "utf8" });
|
|
146
|
+
const pids = result.trim().split("\n").filter(Boolean);
|
|
147
|
+
for (const pid of pids) {
|
|
148
|
+
process.kill(parseInt(pid), "SIGTERM");
|
|
149
|
+
console.log(`Stopped process ${pid}`);
|
|
150
|
+
}
|
|
151
|
+
if (pids.length === 0) {
|
|
152
|
+
console.log("No shotmon process running");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
console.log("No shotmon process running");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function showStatus() {
|
|
160
|
+
try {
|
|
161
|
+
// Use bracket trick to avoid pgrep matching itself
|
|
162
|
+
const result = (0, child_process_1.execSync)("pgrep -af 'node.*[s]hotmon.*--daemon'", { encoding: "utf8" });
|
|
163
|
+
const lines = result.trim().split("\n").filter(Boolean);
|
|
164
|
+
if (lines.length > 0) {
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
// Parse "PID command args"
|
|
167
|
+
const match = line.match(/^(\d+)\s+.*--daemon\s+(.+)$/);
|
|
168
|
+
if (match) {
|
|
169
|
+
const pid = match[1];
|
|
170
|
+
const target = match[2];
|
|
171
|
+
console.log(`Running (PID: ${pid}) -> ${target}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const pid = line.split(/\s+/)[0];
|
|
175
|
+
console.log(`Running (PID: ${pid})`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log("Not running");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
console.log("Not running");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function runConfig() {
|
|
188
|
+
let config = (0, config_1.loadConfig)();
|
|
189
|
+
if (!config || config.remotes.length === 0) {
|
|
190
|
+
if (!config) {
|
|
191
|
+
console.log("Welcome! Let's add some SSH remotes.\n");
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log("No remotes configured. Let's add some.\n");
|
|
195
|
+
}
|
|
196
|
+
const remotes = await addRemotes([]);
|
|
197
|
+
config = { remotes };
|
|
198
|
+
(0, config_1.saveConfig)(config);
|
|
199
|
+
if (remotes.length > 0) {
|
|
200
|
+
console.log(`\nSaved ${remotes.length} remote(s).`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(`SSH remotes: ${config.remotes.join(", ")}\n`);
|
|
205
|
+
const modify = await (0, prompts_1.promptConfirm)("Modify remotes?");
|
|
206
|
+
if (modify) {
|
|
207
|
+
const toKeep = await (0, prompts_1.promptMultiSelect)("Select remotes to keep (space to toggle, enter to confirm)", config.remotes);
|
|
208
|
+
const remotes = await addRemotes(toKeep);
|
|
209
|
+
config = { remotes };
|
|
210
|
+
(0, config_1.saveConfig)(config);
|
|
211
|
+
console.log(`\nSaved ${remotes.length} remote(s).`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return config;
|
|
215
|
+
}
|
|
216
|
+
async function startCommand() {
|
|
217
|
+
const config = (0, config_1.loadConfig)();
|
|
218
|
+
if (!config || config.remotes.length === 0) {
|
|
219
|
+
console.log("No remotes configured. Run 'shotmon' first to set up.");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
// Add "local" option to the list
|
|
223
|
+
const options = ["local", ...config.remotes];
|
|
224
|
+
let selected;
|
|
225
|
+
if (options.length === 1) {
|
|
226
|
+
selected = options[0];
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
selected = await (0, prompts_1.promptSelect)("Select target", options);
|
|
230
|
+
}
|
|
231
|
+
// Stop any existing process before starting new one
|
|
232
|
+
try {
|
|
233
|
+
const result = (0, child_process_1.execSync)("pgrep -f 'node.*[s]hotmon.*--daemon'", { encoding: "utf8" });
|
|
234
|
+
const pids = result.trim().split("\n").filter(Boolean);
|
|
235
|
+
for (const pid of pids) {
|
|
236
|
+
process.kill(parseInt(pid), "SIGTERM");
|
|
237
|
+
}
|
|
238
|
+
if (pids.length > 0) {
|
|
239
|
+
console.log(`Stopped previous process`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// Not running, continue
|
|
244
|
+
}
|
|
245
|
+
startBackground(selected);
|
|
246
|
+
}
|
|
247
|
+
async function main() {
|
|
248
|
+
const args = process.argv.slice(2);
|
|
249
|
+
const command = args[0];
|
|
250
|
+
// Handle --daemon (internal use)
|
|
251
|
+
if (command === "--daemon") {
|
|
252
|
+
const remote = args[1];
|
|
253
|
+
if (remote) {
|
|
254
|
+
await (0, monitor_1.startMonitor)(remote);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
console.log(`shotmon v${getVersion()}\n`);
|
|
259
|
+
// Handle commands
|
|
260
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
261
|
+
showHelp();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (command === "stop") {
|
|
265
|
+
stopBackground();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (command === "status") {
|
|
269
|
+
showStatus();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (command === "start") {
|
|
273
|
+
await startCommand();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (command === "config") {
|
|
277
|
+
await runConfig();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (command === "uninstall") {
|
|
281
|
+
uninstall();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// No command - run config flow then auto-start
|
|
285
|
+
const config = await runConfig();
|
|
286
|
+
if (config.remotes.length === 0) {
|
|
287
|
+
console.log("No remotes configured. Run shotmon again to add remotes.");
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
console.log("\n--- Starting monitor ---\n");
|
|
291
|
+
await startCommand();
|
|
292
|
+
}
|
|
293
|
+
main().catch(console.error);
|
package/dist/monitor.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.startMonitor = startMonitor;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const POLL_INTERVAL_MS = 200;
|
|
43
|
+
const LOG_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
44
|
+
let lastImageHash = null;
|
|
45
|
+
let logFile = null;
|
|
46
|
+
let logStartTime = 0;
|
|
47
|
+
function isWSL() {
|
|
48
|
+
try {
|
|
49
|
+
const release = fs.readFileSync("/proc/version", "utf8");
|
|
50
|
+
return release.toLowerCase().includes("microsoft") || release.toLowerCase().includes("wsl");
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function getLogDir() {
|
|
57
|
+
return path.join(os.homedir(), ".config", "shotmon", "logs");
|
|
58
|
+
}
|
|
59
|
+
function ensureLogDir() {
|
|
60
|
+
const logDir = getLogDir();
|
|
61
|
+
if (!fs.existsSync(logDir)) {
|
|
62
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function createNewLogFile() {
|
|
66
|
+
ensureLogDir();
|
|
67
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
68
|
+
const filename = `shotmon-${timestamp}.log`;
|
|
69
|
+
return path.join(getLogDir(), filename);
|
|
70
|
+
}
|
|
71
|
+
function log(message) {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const timestamp = new Date().toISOString();
|
|
74
|
+
const line = `[${timestamp}] ${message}\n`;
|
|
75
|
+
// Check if we need a new log file
|
|
76
|
+
if (!logFile || (now - logStartTime) > LOG_MAX_AGE_MS) {
|
|
77
|
+
logFile = createNewLogFile();
|
|
78
|
+
logStartTime = now;
|
|
79
|
+
}
|
|
80
|
+
// Write to file
|
|
81
|
+
fs.appendFileSync(logFile, line);
|
|
82
|
+
// Also print to console if not in background
|
|
83
|
+
if (!process.env.SHOTMON_BACKGROUND) {
|
|
84
|
+
process.stdout.write(message + "\n");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function getClipboardImageWSL() {
|
|
88
|
+
try {
|
|
89
|
+
// PowerShell script to get clipboard image as base64
|
|
90
|
+
const psScript = `
|
|
91
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
92
|
+
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
93
|
+
if ($img -ne $null) {
|
|
94
|
+
$ms = New-Object System.IO.MemoryStream
|
|
95
|
+
$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
96
|
+
[Convert]::ToBase64String($ms.ToArray())
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
// Encode as UTF-16LE base64 for -EncodedCommand
|
|
100
|
+
const encoded = Buffer.from(psScript, "utf16le").toString("base64");
|
|
101
|
+
const result = (0, child_process_1.execSync)(`powershell.exe -NoProfile -EncodedCommand ${encoded}`, {
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
timeout: 5000,
|
|
104
|
+
}).trim();
|
|
105
|
+
if (result && result.length > 0) {
|
|
106
|
+
return Buffer.from(result, "base64");
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function getClipboardImageNative() {
|
|
115
|
+
try {
|
|
116
|
+
// Try using @crosscopy/clipboard for native Linux/macOS
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
const Clipboard = require("@crosscopy/clipboard").default;
|
|
119
|
+
const hasImage = await Clipboard.hasImage();
|
|
120
|
+
if (!hasImage) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const base64 = await Clipboard.getImageBase64();
|
|
124
|
+
if (!base64) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return Buffer.from(base64, "base64");
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function getClipboardImage() {
|
|
134
|
+
if (isWSL()) {
|
|
135
|
+
return getClipboardImageWSL();
|
|
136
|
+
}
|
|
137
|
+
return getClipboardImageNative();
|
|
138
|
+
}
|
|
139
|
+
function getImageHash(buffer) {
|
|
140
|
+
return crypto.createHash("md5").update(buffer).digest("hex");
|
|
141
|
+
}
|
|
142
|
+
function generateFilename() {
|
|
143
|
+
const now = new Date();
|
|
144
|
+
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
145
|
+
return `screenshot-${timestamp}.png`;
|
|
146
|
+
}
|
|
147
|
+
function getLocalScreenshotDir() {
|
|
148
|
+
return path.join(os.homedir(), "shotmon-screenshots");
|
|
149
|
+
}
|
|
150
|
+
function saveLocal(imageBuffer, filename) {
|
|
151
|
+
const dir = getLocalScreenshotDir();
|
|
152
|
+
const filePath = path.join(dir, filename);
|
|
153
|
+
try {
|
|
154
|
+
if (!fs.existsSync(dir)) {
|
|
155
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
fs.writeFileSync(filePath, imageBuffer);
|
|
158
|
+
return { success: true, path: filePath };
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return { success: false, path: filePath };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function getRemoteHomeDir(remote) {
|
|
165
|
+
// Extract username from user@host format
|
|
166
|
+
const match = remote.match(/^([^@]+)@/);
|
|
167
|
+
const user = match ? match[1] : "root";
|
|
168
|
+
return user === "root" ? "/root" : `/home/${user}`;
|
|
169
|
+
}
|
|
170
|
+
async function pipeToRemote(imageBuffer, remote, filename) {
|
|
171
|
+
const homeDir = getRemoteHomeDir(remote);
|
|
172
|
+
const remotePath = `${homeDir}/shotmon-screenshots/${filename}`;
|
|
173
|
+
return new Promise((resolve) => {
|
|
174
|
+
// Ensure directory exists and write file
|
|
175
|
+
// Use ControlMaster options for faster repeated connections
|
|
176
|
+
const proc = (0, child_process_1.spawn)("ssh", [
|
|
177
|
+
"-o", "ControlMaster=auto",
|
|
178
|
+
"-o", "ControlPath=~/.ssh/shotmon-%r@%h:%p",
|
|
179
|
+
"-o", "ControlPersist=60",
|
|
180
|
+
remote,
|
|
181
|
+
`mkdir -p ${homeDir}/shotmon-screenshots && cat > ${remotePath}`
|
|
182
|
+
]);
|
|
183
|
+
proc.stdin.write(imageBuffer);
|
|
184
|
+
proc.stdin.end();
|
|
185
|
+
proc.on("close", (code) => {
|
|
186
|
+
resolve({ success: code === 0, path: remotePath });
|
|
187
|
+
});
|
|
188
|
+
proc.on("error", () => {
|
|
189
|
+
resolve({ success: false, path: remotePath });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function copyToClipboardWSL(text) {
|
|
194
|
+
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 });
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Ignore clipboard errors
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function copyToClipboardNative(text) {
|
|
203
|
+
try {
|
|
204
|
+
// @ts-ignore
|
|
205
|
+
const Clipboard = require("@crosscopy/clipboard").default;
|
|
206
|
+
await Clipboard.setText(text);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Ignore clipboard errors
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function copyToClipboard(text) {
|
|
213
|
+
if (isWSL()) {
|
|
214
|
+
copyToClipboardWSL(text);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
await copyToClipboardNative(text);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function startMonitor(remote) {
|
|
221
|
+
// Initialize logging
|
|
222
|
+
logFile = createNewLogFile();
|
|
223
|
+
logStartTime = Date.now();
|
|
224
|
+
const wsl = isWSL();
|
|
225
|
+
log(`Starting monitor for: ${remote}`);
|
|
226
|
+
log(`Environment: ${wsl ? "WSL" : "Native"}`);
|
|
227
|
+
log(`Log file: ${logFile}`);
|
|
228
|
+
if (remote === "local") {
|
|
229
|
+
log(`Saving to: ${getLocalScreenshotDir()}`);
|
|
230
|
+
}
|
|
231
|
+
log("");
|
|
232
|
+
log("Monitoring clipboard... (Ctrl+C to stop)");
|
|
233
|
+
log("");
|
|
234
|
+
// Initialize with current clipboard state
|
|
235
|
+
const initialImage = await getClipboardImage();
|
|
236
|
+
if (initialImage) {
|
|
237
|
+
lastImageHash = getImageHash(initialImage);
|
|
238
|
+
}
|
|
239
|
+
const poll = async () => {
|
|
240
|
+
try {
|
|
241
|
+
const imageBuffer = await getClipboardImage();
|
|
242
|
+
if (!imageBuffer) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const currentHash = getImageHash(imageBuffer);
|
|
246
|
+
if (currentHash !== lastImageHash) {
|
|
247
|
+
lastImageHash = currentHash;
|
|
248
|
+
const filename = generateFilename();
|
|
249
|
+
const size = Math.round(imageBuffer.length / 1024);
|
|
250
|
+
log(`New screenshot: ${filename} (${size}KB)`);
|
|
251
|
+
if (remote === "local") {
|
|
252
|
+
const result = saveLocal(imageBuffer, filename);
|
|
253
|
+
if (result.success) {
|
|
254
|
+
log(` -> Saved: ${result.path}`);
|
|
255
|
+
await copyToClipboard(result.path);
|
|
256
|
+
log(` -> Copied to clipboard`);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
log(` -> Failed to save locally`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const result = await pipeToRemote(imageBuffer, remote, filename);
|
|
264
|
+
if (result.success) {
|
|
265
|
+
log(` -> Sent to ${remote}:${result.path}`);
|
|
266
|
+
await copyToClipboard(result.path);
|
|
267
|
+
log(` -> Copied to clipboard`);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
log(` -> Failed to send to ${remote}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
log(`Error: ${err}`);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
// Start polling
|
|
280
|
+
setInterval(poll, POLL_INTERVAL_MS);
|
|
281
|
+
// Keep process running
|
|
282
|
+
await new Promise(() => { });
|
|
283
|
+
}
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.promptConfirm = promptConfirm;
|
|
4
|
+
exports.promptInput = promptInput;
|
|
5
|
+
exports.promptSelect = promptSelect;
|
|
6
|
+
exports.promptMultiSelect = promptMultiSelect;
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
const { Select, Confirm, Input, MultiSelect } = require("enquirer");
|
|
9
|
+
async function promptConfirm(message) {
|
|
10
|
+
const prompt = new Confirm({ name: "confirm", message });
|
|
11
|
+
return prompt.run();
|
|
12
|
+
}
|
|
13
|
+
async function promptInput(message) {
|
|
14
|
+
const prompt = new Input({ name: "input", message });
|
|
15
|
+
return prompt.run();
|
|
16
|
+
}
|
|
17
|
+
async function promptSelect(message, choices) {
|
|
18
|
+
const prompt = new Select({ name: "select", message, choices });
|
|
19
|
+
return prompt.run();
|
|
20
|
+
}
|
|
21
|
+
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();
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shotmon-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Screenshot monitor CLI tool",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shotmon": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"prepublishOnly": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"screenshot",
|
|
15
|
+
"monitor",
|
|
16
|
+
"clipboard"
|
|
17
|
+
],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^25.0.10",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@crosscopy/clipboard": "^0.2.8",
|
|
26
|
+
"enquirer": "^2.4.1"
|
|
27
|
+
}
|
|
28
|
+
}
|