copyhub-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/.env.example +24 -0
- package/README.md +122 -0
- package/package.json +39 -0
- package/src/cli.js +337 -0
- package/src/clipboard-watcher.js +45 -0
- package/src/config.js +180 -0
- package/src/daemon-state.js +51 -0
- package/src/electron-launcher.js +47 -0
- package/src/oauth.js +579 -0
- package/src/paths.js +9 -0
- package/src/platform.js +17 -0
- package/src/sheet-api-errors.js +50 -0
- package/src/sheet-daily.js +11 -0
- package/src/sheets.js +106 -0
- package/src/start-daemon-logic.js +114 -0
- package/src/stop-process.js +29 -0
- package/src/storage.js +59 -0
- package/src/tokens.js +24 -0
- package/ui/main.mjs +331 -0
- package/ui/preload.cjs +12 -0
- package/ui/renderer/index.html +224 -0
package/.env.example
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copy to .env and fill in values. CopyHub reads .env when you run the CLI from the current directory.
|
|
2
|
+
#
|
|
3
|
+
# Google Cloud Console → APIs: enable "Google Sheets API" on the SAME project as your OAuth client ID
|
|
4
|
+
# (if sync fails with project=..., open the Enable link in the log or Library → Google Sheets API → Enable).
|
|
5
|
+
#
|
|
6
|
+
# Spreadsheet ID + platform + overlay shortcut: set after copyhub login (web setup page),
|
|
7
|
+
# or copyhub config ... --sheet-id <ID> and edit overlayPlatform / overlayAccelerator in ~/.copyhub/config.json
|
|
8
|
+
# One tab per local calendar day: COPYHUB-YYYY-MM-DD (machine timezone).
|
|
9
|
+
#
|
|
10
|
+
# copyhub start opens the Electron overlay by default; set to 1 for clipboard + Sheet only:
|
|
11
|
+
# COPYHUB_START_NO_OVERLAY=1
|
|
12
|
+
|
|
13
|
+
COPYHUB_GOOGLE_CLIENT_ID=
|
|
14
|
+
COPYHUB_GOOGLE_CLIENT_SECRET=
|
|
15
|
+
COPYHUB_OAUTH_REDIRECT_PORT=19999
|
|
16
|
+
|
|
17
|
+
# Overlay accelerator (optional): if set here it OVERRIDES the value saved after copyhub login (config overlayAccelerator).
|
|
18
|
+
COPYHUB_OVERLAY_ACCELERATOR=
|
|
19
|
+
|
|
20
|
+
# Set to 1 to NOT show the window when overlay starts (open via shortcut / tray only).
|
|
21
|
+
# COPYHUB_OVERLAY_HIDE_ON_START=1
|
|
22
|
+
# Set to 1 so the overlay does not close when clicking outside (default: click outside closes).
|
|
23
|
+
# COPYHUB_OVERLAY_STICKY=1
|
|
24
|
+
# Hide overlay icon from the taskbar: COPYHUB_OVERLAY_SKIP_TASKBAR=1
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# CopyHub
|
|
2
|
+
|
|
3
|
+
CopyHub watches your **clipboard**, keeps a **local history** under `~/.copyhub/history.jsonl`, optionally syncs copies to **Google Sheets** (one tab per day), and shows an **Electron overlay** so you can browse recent clips quickly.
|
|
4
|
+
|
|
5
|
+
Runs on **Windows**, **macOS**, and **Linux**.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- **Node.js** ≥ 18
|
|
10
|
+
- A **Google Cloud** project with:
|
|
11
|
+
- **Google Sheets API** enabled for the *same* project as your OAuth client
|
|
12
|
+
- **OAuth 2.0 Client** (Desktop app type works well for localhost redirect)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
From this repository:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Link the CLI globally (optional):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm link
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or run commands with:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node src/cli.js <command>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Google Cloud setup
|
|
35
|
+
|
|
36
|
+
1. Enable **[Google Sheets API](https://console.cloud.google.com/apis/library/sheets.googleapis.com)** on your OAuth project.
|
|
37
|
+
2. Create **OAuth 2.0 credentials** and add this **Authorized redirect URI** (adjust the port if you change it):
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
http://127.0.0.1:19999/oauth2callback
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
3. Copy `.env.example` to `.env` and set:
|
|
44
|
+
|
|
45
|
+
- `COPYHUB_GOOGLE_CLIENT_ID`
|
|
46
|
+
- `COPYHUB_GOOGLE_CLIENT_SECRET`
|
|
47
|
+
- Optionally `COPYHUB_OAUTH_REDIRECT_PORT` (default **19999**)
|
|
48
|
+
|
|
49
|
+
Alternatively, store credentials in `~/.copyhub/config.json` via:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
copyhub config --client-id "<ID>" --client-secret "<SECRET>" [--sheet-id "<SPREADSHEET_ID>"] [--redirect-port 19999]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## First run
|
|
56
|
+
|
|
57
|
+
1. **Login** (opens the browser for OAuth, then a setup page):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
copyhub login
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. On the setup page, enter your **Spreadsheet ID** (from the URL `…/d/<SPREADSHEET_ID>/edit`), choose **platform** (Windows / macOS / Linux) for shortcut hints, set the **overlay accelerator** if you want, and save.
|
|
64
|
+
|
|
65
|
+
3. **Start** the background watcher (clipboard + Sheets + overlay by default):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
copyhub start
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
You can close the terminal; the process keeps running. Check with `copyhub list` and stop with `copyhub stop`.
|
|
72
|
+
|
|
73
|
+
### Useful flags and environment variables
|
|
74
|
+
|
|
75
|
+
| Action | How |
|
|
76
|
+
|--------|-----|
|
|
77
|
+
| Run in terminal (Ctrl+C stops everything) | `copyhub start --foreground` |
|
|
78
|
+
| No Google Sheets | `copyhub start --no-sheet` |
|
|
79
|
+
| No Electron overlay | `copyhub start --no-overlay` or `COPYHUB_START_NO_OVERLAY=1` |
|
|
80
|
+
| Override shortcut | `COPYHUB_OVERLAY_ACCELERATOR` in `.env` (overrides saved config) |
|
|
81
|
+
| Overlay stays open when clicking outside | `COPYHUB_OVERLAY_STICKY=1` |
|
|
82
|
+
|
|
83
|
+
Run `copyhub --help` or `copyhub commands` for the full command list.
|
|
84
|
+
|
|
85
|
+
## CLI overview
|
|
86
|
+
|
|
87
|
+
| Command | Purpose |
|
|
88
|
+
|---------|---------|
|
|
89
|
+
| `copyhub config` | Save OAuth client ID/secret (and optional Sheet ID) to `~/.copyhub/config.json` |
|
|
90
|
+
| `copyhub login` | OAuth flow + setup page (Sheet ID, platform, overlay shortcut) |
|
|
91
|
+
| `copyhub logout` | Remove saved tokens |
|
|
92
|
+
| `copyhub status` | OAuth, Sheet, tokens, overlay platform/shortcut, daemon state |
|
|
93
|
+
| `copyhub start` | Background daemon: clipboard watcher + optional Sheets + overlay |
|
|
94
|
+
| `copyhub list` / `copyhub ls` | Show whether the daemon PID is running |
|
|
95
|
+
| `copyhub stop` | Stop daemon and overlay child |
|
|
96
|
+
| `copyhub overlay` | Launch only the Electron overlay (no clipboard daemon) |
|
|
97
|
+
|
|
98
|
+
## Data locations
|
|
99
|
+
|
|
100
|
+
Everything lives under **`~/.copyhub/`** (or `%USERPROFILE%\.copyhub` on Windows):
|
|
101
|
+
|
|
102
|
+
| File | Contents |
|
|
103
|
+
|------|----------|
|
|
104
|
+
| `config.json` | OAuth credentials (if not only in `.env`), `googleSheetId`, `overlayAccelerator`, `overlayPlatform` |
|
|
105
|
+
| `tokens.json` | OAuth refresh/access tokens |
|
|
106
|
+
| `history.jsonl` | Local clipboard history (JSON Lines) |
|
|
107
|
+
| `run.json` | Daemon PID and metadata (when using `copyhub start` without `--foreground`) |
|
|
108
|
+
|
|
109
|
+
## Google Sheets layout
|
|
110
|
+
|
|
111
|
+
- Rows are appended when Sheet sync is enabled and you are logged in.
|
|
112
|
+
- New tabs are created per **local calendar day**, named: **`COPYHUB-YYYY-MM-DD`**.
|
|
113
|
+
|
|
114
|
+
## Overlay (Electron)
|
|
115
|
+
|
|
116
|
+
- Global shortcut defaults to **`CommandOrControl+Shift+H`** if nothing else is set (`Ctrl+Shift+H` on Windows/Linux, `⌘⇧H` on macOS-style wording in Electron).
|
|
117
|
+
- **macOS**: you may need to grant **Accessibility** permissions for global shortcuts.
|
|
118
|
+
- Some **`Control+Alt+…`** combinations do not register reliably on Windows; prefer alternatives suggested on the setup page.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT — see `package.json`.
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "copyhub-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CopyHub — clipboard, local history, Google Sheets sync (OAuth). Windows, macOS, Linux.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"copyhub": "./src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/cli.js start",
|
|
11
|
+
"login": "node src/cli.js login",
|
|
12
|
+
"overlay": "node src/cli.js overlay"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"clipboard",
|
|
19
|
+
"google-sheets",
|
|
20
|
+
"oauth",
|
|
21
|
+
"sync",
|
|
22
|
+
"cli",
|
|
23
|
+
"cross-platform",
|
|
24
|
+
"windows",
|
|
25
|
+
"linux",
|
|
26
|
+
"macos"
|
|
27
|
+
],
|
|
28
|
+
"files": ["src", "ui", ".env.example"],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"clipboardy": "^4.0.0",
|
|
32
|
+
"dotenv": "^16.4.5",
|
|
33
|
+
"electron": "^33.2.0",
|
|
34
|
+
"commander": "^12.1.0",
|
|
35
|
+
"google-auth-library": "^9.15.0",
|
|
36
|
+
"googleapis": "^144.0.0",
|
|
37
|
+
"open": "^10.1.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { program } from 'commander';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import {
|
|
8
|
+
loadConfig,
|
|
9
|
+
saveConfig,
|
|
10
|
+
loadSheetSyncTarget,
|
|
11
|
+
DEFAULT_OAUTH_REDIRECT_PORT,
|
|
12
|
+
describeOAuthCredentialSource,
|
|
13
|
+
ENV_GOOGLE_CLIENT_ID,
|
|
14
|
+
ENV_GOOGLE_CLIENT_SECRET,
|
|
15
|
+
ENV_OAUTH_REDIRECT_PORT,
|
|
16
|
+
loadOverlayAcceleratorFromConfigSync,
|
|
17
|
+
loadOverlayPlatformFromConfigSync,
|
|
18
|
+
} from './config.js';
|
|
19
|
+
import { runLoginFlow } from './oauth.js';
|
|
20
|
+
import { clearTokens, loadTokens } from './tokens.js';
|
|
21
|
+
import { spawnCopyhubOverlay } from './electron-launcher.js';
|
|
22
|
+
import { CONFIG_PATH, TOKENS_PATH, HISTORY_PATH, DIR } from './paths.js';
|
|
23
|
+
import { dailySheetTabName } from './sheet-daily.js';
|
|
24
|
+
import {
|
|
25
|
+
readRunState,
|
|
26
|
+
writeRunState,
|
|
27
|
+
clearRunState,
|
|
28
|
+
isPidAlive,
|
|
29
|
+
pruneStaleRunState,
|
|
30
|
+
} from './daemon-state.js';
|
|
31
|
+
import { killDaemonTree } from './stop-process.js';
|
|
32
|
+
import { ensureDir } from './storage.js';
|
|
33
|
+
import { runCopyhubDaemon } from './start-daemon-logic.js';
|
|
34
|
+
|
|
35
|
+
const CLI_JS = fileURLToPath(new URL('./cli.js', import.meta.url));
|
|
36
|
+
|
|
37
|
+
program.name('copyhub').description(
|
|
38
|
+
'CopyHub — clipboard, overlay history, Google Sheets sync (COPYHUB-daily tabs). Windows, macOS, Linux.',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
program
|
|
42
|
+
.command('config')
|
|
43
|
+
.description('Save Client ID / Secret (optional Sheet ID) to ~/.copyhub/config.json')
|
|
44
|
+
.requiredOption('--client-id <id>', 'OAuth 2.0 Client ID')
|
|
45
|
+
.requiredOption('--client-secret <secret>', 'OAuth 2.0 Client Secret')
|
|
46
|
+
.option(
|
|
47
|
+
'--redirect-port <port>',
|
|
48
|
+
`Localhost OAuth callback port (default ${DEFAULT_OAUTH_REDIRECT_PORT})`,
|
|
49
|
+
(v) => parseInt(v, 10),
|
|
50
|
+
)
|
|
51
|
+
.option('--sheet-id <id>', 'Google Spreadsheet ID (URL .../d/<ID>/edit); can be set later via copyhub login')
|
|
52
|
+
.action(async (opts) => {
|
|
53
|
+
const port =
|
|
54
|
+
typeof opts.redirectPort === 'number' && !Number.isNaN(opts.redirectPort)
|
|
55
|
+
? opts.redirectPort
|
|
56
|
+
: DEFAULT_OAUTH_REDIRECT_PORT;
|
|
57
|
+
/** @type {Parameters<typeof saveConfig>[0]} */
|
|
58
|
+
const payload = {
|
|
59
|
+
clientId: opts.clientId,
|
|
60
|
+
clientSecret: opts.clientSecret,
|
|
61
|
+
redirectPort: port,
|
|
62
|
+
};
|
|
63
|
+
if (opts.sheetId) payload.googleSheetId = opts.sheetId;
|
|
64
|
+
await saveConfig(payload);
|
|
65
|
+
console.log(`Saved configuration: ${CONFIG_PATH}`);
|
|
66
|
+
console.log(
|
|
67
|
+
`In Google Cloud Console, add redirect URI: http://127.0.0.1:${port}/oauth2callback`,
|
|
68
|
+
);
|
|
69
|
+
console.log('Enable Google Sheets API for the same OAuth project.');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
program
|
|
73
|
+
.command('login')
|
|
74
|
+
.description(
|
|
75
|
+
`Google sign-in (OAuth Sheets), then Spreadsheet ID setup page — port ${DEFAULT_OAUTH_REDIRECT_PORT} or ${ENV_OAUTH_REDIRECT_PORT}`,
|
|
76
|
+
)
|
|
77
|
+
.action(async () => {
|
|
78
|
+
await runLoginFlow();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
program
|
|
82
|
+
.command('logout')
|
|
83
|
+
.description('Remove saved tokens')
|
|
84
|
+
.action(async () => {
|
|
85
|
+
await clearTokens();
|
|
86
|
+
console.log(`Removed tokens: ${TOKENS_PATH}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
program
|
|
90
|
+
.command('overlay')
|
|
91
|
+
.description(
|
|
92
|
+
'Run only the Electron overlay window (without copyhub start). macOS may require Accessibility permissions.',
|
|
93
|
+
)
|
|
94
|
+
.action(() => {
|
|
95
|
+
try {
|
|
96
|
+
const child = spawnCopyhubOverlay();
|
|
97
|
+
child.on('error', (err) => {
|
|
98
|
+
console.error(err.message);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error(/** @type {Error} */ (e).message);
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('list')
|
|
109
|
+
.alias('ls')
|
|
110
|
+
.description('Show whether the CopyHub background process (copyhub start) is running')
|
|
111
|
+
.action(() => {
|
|
112
|
+
pruneStaleRunState();
|
|
113
|
+
const s = readRunState();
|
|
114
|
+
if (!s) {
|
|
115
|
+
console.log('No CopyHub background process (no ~/.copyhub/run.json or already cleared).');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!isPidAlive(s.pid)) {
|
|
119
|
+
console.log(`PID ${s.pid} is not running — removed run.json.`);
|
|
120
|
+
clearRunState();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.log('CopyHub background process is running:');
|
|
124
|
+
console.log(` PID: ${s.pid}`);
|
|
125
|
+
console.log(` Started: ${s.startedAt || '(unknown)'}`);
|
|
126
|
+
console.log(` Stop with: copyhub stop`);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
program
|
|
130
|
+
.command('stop')
|
|
131
|
+
.description('Stop the background process started by copyhub start (and overlay child)')
|
|
132
|
+
.action(() => {
|
|
133
|
+
pruneStaleRunState();
|
|
134
|
+
const s = readRunState();
|
|
135
|
+
if (!s) {
|
|
136
|
+
console.log('No background process to stop.');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!isPidAlive(s.pid)) {
|
|
140
|
+
console.log(`PID ${s.pid} is not running — cleared run.json.`);
|
|
141
|
+
clearRunState();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
killDaemonTree(s.pid);
|
|
145
|
+
clearRunState();
|
|
146
|
+
console.log(`Stopped process PID ${s.pid}.`);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
program
|
|
150
|
+
.command('status')
|
|
151
|
+
.description('Check OAuth, Sheet, and tokens')
|
|
152
|
+
.action(async () => {
|
|
153
|
+
pruneStaleRunState();
|
|
154
|
+
const cfg = await loadConfig();
|
|
155
|
+
const sheet = await loadSheetSyncTarget();
|
|
156
|
+
const tok = await loadTokens();
|
|
157
|
+
const src = describeOAuthCredentialSource();
|
|
158
|
+
|
|
159
|
+
if (!cfg) {
|
|
160
|
+
console.log('OAuth config: missing');
|
|
161
|
+
console.log(
|
|
162
|
+
` Set ${ENV_GOOGLE_CLIENT_ID} and ${ENV_GOOGLE_CLIENT_SECRET} in .env (see .env.example), or run: copyhub config`,
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
const srcLabel =
|
|
166
|
+
src === 'env'
|
|
167
|
+
? 'environment / .env'
|
|
168
|
+
: src === 'mixed'
|
|
169
|
+
? 'mixed .env + config file'
|
|
170
|
+
: CONFIG_PATH;
|
|
171
|
+
console.log('OAuth config: ok');
|
|
172
|
+
console.log(` Client ID/Secret source: ${srcLabel}`);
|
|
173
|
+
console.log(` Callback: http://127.0.0.1:${cfg.redirectPort}/oauth2callback`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!sheet) {
|
|
177
|
+
console.log(
|
|
178
|
+
'Google Sheet: not set — run copyhub login (setup page) or copyhub config ... --sheet-id <ID>',
|
|
179
|
+
);
|
|
180
|
+
} else {
|
|
181
|
+
const todayTab = dailySheetTabName();
|
|
182
|
+
console.log(
|
|
183
|
+
`Google Sheet: ok — ID …${sheet.spreadsheetId.slice(-8)} · today's tab: "${todayTab}"`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log(
|
|
188
|
+
'Token:',
|
|
189
|
+
tok?.refresh_token || tok?.access_token ? `present (${TOKENS_PATH})` : 'missing (run copyhub login)',
|
|
190
|
+
);
|
|
191
|
+
if (existsSync(HISTORY_PATH)) {
|
|
192
|
+
console.log('History:', HISTORY_PATH);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const plat = loadOverlayPlatformFromConfigSync();
|
|
196
|
+
const platLabel =
|
|
197
|
+
plat === 'mac' ? 'macOS' : plat === 'linux' ? 'Linux' : plat === 'win' ? 'Windows' : '(not set)';
|
|
198
|
+
console.log(`Overlay platform setting: ${platLabel}`);
|
|
199
|
+
|
|
200
|
+
const envAccel = process.env.COPYHUB_OVERLAY_ACCELERATOR?.trim();
|
|
201
|
+
const cfgAccel = loadOverlayAcceleratorFromConfigSync();
|
|
202
|
+
if (envAccel) {
|
|
203
|
+
console.log(`Overlay shortcut (.env): ${envAccel}`);
|
|
204
|
+
} else if (cfgAccel) {
|
|
205
|
+
console.log(`Overlay shortcut (config): ${cfgAccel}`);
|
|
206
|
+
} else {
|
|
207
|
+
console.log(
|
|
208
|
+
'Overlay shortcut: (default Ctrl+Shift+H — set after copyhub login or COPYHUB_OVERLAY_ACCELERATOR)',
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const run = readRunState();
|
|
213
|
+
if (run && isPidAlive(run.pid)) {
|
|
214
|
+
console.log(`Background process: yes (PID ${run.pid}) — copyhub list`);
|
|
215
|
+
} else if (run) {
|
|
216
|
+
console.log('Background process: run.json exists but PID is dead — run copyhub stop to clean up.');
|
|
217
|
+
} else {
|
|
218
|
+
console.log('Background process: no — copyhub start to run in background.');
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
program
|
|
223
|
+
.command('start')
|
|
224
|
+
.description(
|
|
225
|
+
'Run clipboard + Sheet + overlay in background (terminal can close). Blocks if PID already running. Use --foreground to attach to terminal.',
|
|
226
|
+
)
|
|
227
|
+
.option('--no-sheet', 'Local history only, do not write to Sheets')
|
|
228
|
+
.option('--no-overlay', 'Do not launch Electron')
|
|
229
|
+
.option('--foreground', 'Run in foreground (Ctrl+C stops; no background PID file)')
|
|
230
|
+
.action(async (opts) => {
|
|
231
|
+
pruneStaleRunState();
|
|
232
|
+
|
|
233
|
+
const useSheet = opts.sheet !== false;
|
|
234
|
+
const skipOverlay =
|
|
235
|
+
opts.overlay === false || process.env.COPYHUB_START_NO_OVERLAY === '1';
|
|
236
|
+
|
|
237
|
+
const existing = readRunState();
|
|
238
|
+
if (existing && isPidAlive(existing.pid)) {
|
|
239
|
+
console.error(
|
|
240
|
+
`CopyHub already running in background (PID ${existing.pid}). See: copyhub list — Stop: copyhub stop`,
|
|
241
|
+
);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
if (existing && !isPidAlive(existing.pid)) {
|
|
245
|
+
clearRunState();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (opts.foreground) {
|
|
249
|
+
console.log('CopyHub foreground mode. Press Ctrl+C to stop.');
|
|
250
|
+
await ensureDir();
|
|
251
|
+
|
|
252
|
+
const ctrl = await runCopyhubDaemon({ useSheet, skipOverlay });
|
|
253
|
+
|
|
254
|
+
const onStop = () => {
|
|
255
|
+
ctrl.stopSync();
|
|
256
|
+
process.exit(0);
|
|
257
|
+
};
|
|
258
|
+
process.on('SIGINT', onStop);
|
|
259
|
+
process.on('SIGTERM', onStop);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await ensureDir();
|
|
264
|
+
const daemonArgs = [CLI_JS, '_daemon'];
|
|
265
|
+
if (!useSheet) daemonArgs.push('--no-sheet');
|
|
266
|
+
if (skipOverlay) daemonArgs.push('--no-overlay');
|
|
267
|
+
|
|
268
|
+
const child = spawn(process.execPath, daemonArgs, {
|
|
269
|
+
detached: true,
|
|
270
|
+
stdio: 'ignore',
|
|
271
|
+
env: { ...process.env },
|
|
272
|
+
});
|
|
273
|
+
child.unref();
|
|
274
|
+
|
|
275
|
+
if (!child.pid) {
|
|
276
|
+
console.error('Could not spawn background process.');
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
writeRunState({
|
|
281
|
+
pid: child.pid,
|
|
282
|
+
startedAt: new Date().toISOString(),
|
|
283
|
+
foreground: false,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
console.log(`CopyHub running in background (PID ${child.pid}). You may close this terminal.`);
|
|
287
|
+
console.log('Check: copyhub list | Stop: copyhub stop');
|
|
288
|
+
process.exit(0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
program
|
|
292
|
+
.command('_daemon', { hidden: true })
|
|
293
|
+
.option('--no-sheet', 'internal')
|
|
294
|
+
.option('--no-overlay', 'internal')
|
|
295
|
+
.action(async (opts) => {
|
|
296
|
+
const useSheet = opts.sheet !== false;
|
|
297
|
+
const skipOverlay =
|
|
298
|
+
opts.overlay === false || process.env.COPYHUB_START_NO_OVERLAY === '1';
|
|
299
|
+
|
|
300
|
+
function clearMyRunState() {
|
|
301
|
+
try {
|
|
302
|
+
const cur = readRunState();
|
|
303
|
+
if (cur && cur.pid === process.pid) {
|
|
304
|
+
clearRunState();
|
|
305
|
+
}
|
|
306
|
+
} catch {
|
|
307
|
+
/* ignore */
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const ctrl = await runCopyhubDaemon({ useSheet, skipOverlay });
|
|
312
|
+
|
|
313
|
+
const shutdown = () => {
|
|
314
|
+
ctrl.stopSync();
|
|
315
|
+
clearMyRunState();
|
|
316
|
+
process.exit(0);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
process.on('SIGINT', shutdown);
|
|
320
|
+
process.on('SIGTERM', shutdown);
|
|
321
|
+
process.on('exit', clearMyRunState);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
program
|
|
325
|
+
.command('commands')
|
|
326
|
+
.alias('cmds')
|
|
327
|
+
.description('List CLI commands')
|
|
328
|
+
.action(() => {
|
|
329
|
+
console.log(`copyhub config [--client-id ID] [--client-secret SEC] [--redirect-port P] [--sheet-id ID]
|
|
330
|
+
copyhub login | copyhub logout | copyhub status
|
|
331
|
+
copyhub start [--no-sheet] [--no-overlay] [--foreground]
|
|
332
|
+
Default runs in background (terminal can close). Single instance — second start is blocked.
|
|
333
|
+
copyhub list (ls) | copyhub stop
|
|
334
|
+
copyhub overlay | copyhub commands / copyhub --help`);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
program.parse();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import clipboardy from 'clipboardy';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { clipboardPollIntervalMs } from './platform.js';
|
|
4
|
+
|
|
5
|
+
function hashText(s) {
|
|
6
|
+
return createHash('sha256').update(s, 'utf8').digest('hex');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {(text: string) => void | Promise<void>} onNewCopy
|
|
11
|
+
* @param {{ pollMs?: number }} [options]
|
|
12
|
+
* @returns {{ stop: () => void }}
|
|
13
|
+
*/
|
|
14
|
+
export function startClipboardWatcher(onNewCopy, options = {}) {
|
|
15
|
+
const pollMs = typeof options.pollMs === 'number' ? options.pollMs : clipboardPollIntervalMs();
|
|
16
|
+
let lastHash = '';
|
|
17
|
+
let stopped = false;
|
|
18
|
+
|
|
19
|
+
const tick = async () => {
|
|
20
|
+
if (stopped) return;
|
|
21
|
+
try {
|
|
22
|
+
const text = await clipboardy.read();
|
|
23
|
+
if (typeof text !== 'string' || text.length === 0) return;
|
|
24
|
+
const h = hashText(text);
|
|
25
|
+
if (h === lastHash) return;
|
|
26
|
+
lastHash = h;
|
|
27
|
+
await onNewCopy(text);
|
|
28
|
+
} catch {
|
|
29
|
+
// Ignore transient clipboard read errors
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const id = setInterval(() => {
|
|
34
|
+
void tick();
|
|
35
|
+
}, pollMs);
|
|
36
|
+
|
|
37
|
+
void tick();
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
stop() {
|
|
41
|
+
stopped = true;
|
|
42
|
+
clearInterval(id);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|