copyhub-cli 1.0.5 → 1.0.8

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 CHANGED
@@ -16,7 +16,8 @@ COPYHUB_GOOGLE_CLIENT_ID=
16
16
  COPYHUB_GOOGLE_CLIENT_SECRET=
17
17
  COPYHUB_OAUTH_REDIRECT_PORT=19999
18
18
 
19
- # Overlay accelerator (optional): if set here it OVERRIDES the value saved after copyhub login (config overlayAccelerator).
19
+ # Overlay accelerator (optional): overrides config. Default app-wide is Control+Shift+H (⌃/Ctrl + Shift + H).
20
+ # Example Mac ⌘ shortcut: COPYHUB_OVERLAY_ACCELERATOR=CommandOrControl+Shift+H
20
21
  COPYHUB_OVERLAY_ACCELERATOR=
21
22
 
22
23
  # Set to 1 to NOT show the window when overlay starts (open via shortcut / tray only).
package/README.md CHANGED
@@ -1,228 +1,282 @@
1
1
  # CopyHub
2
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.
3
+ CopyHub theo dõi **clipboard**, lưu **lịch sử cục bộ** (`~/.copyhub/history.jsonl`), tùy chọn đồng bộ lên **Google Sheets** (một tab mỗi ngày), mở **overlay Electron** để xem chọn bản copy gần đây.
4
+ English summary: watches clipboard, local JSONL history, optional daily Google Sheets tabs, floating history overlay.
4
5
 
5
- Runs on **Windows**, **macOS**, and **Linux**.
6
+ Chạy trên **Windows**, **macOS**, **Linux**.
6
7
 
7
- ## Requirements
8
+ ---
8
9
 
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)
10
+ ## Mục lục / Table of contents
13
11
 
14
- ## Installation
12
+ Các mục `##` bên dưới theo thứ tự: **Tính năng** · **Yêu cầu** · **Cài đặt** · **File `.env`** · **Google Cloud & OAuth** · **OAuth config vs env** · **Chạy lần đầu** · **Lệnh CLI** · **Biến môi trường** · **Thư mục dữ liệu** · **Google Sheets** · **Overlay** · **Clipboard & lịch sử** · **Nâng cấp** · **Xử lý sự cố** · **Bảo mật** · **Giấy phép**.
15
13
 
16
- ### Install globally (npm)
14
+ ---
17
15
 
18
- After the package is published to npm:
16
+ ## Tính năng / Features
19
17
 
20
- ```bash
21
- npm install -g copyhub-cli
22
- ```
18
+ - Theo dõi clipboard theo chu kỳ (có thể chỉnh `COPYHUB_POLL_MS`).
19
+ - Không lưu hai lần liên tiếp **cùng một nội dung** vào `history.jsonl` / Sheet.
20
+ - Ghi Sheet theo tab **`COPYHUB-YYYY-MM-DD`** (timezone máy).
21
+ - Overlay: phân trang lịch sử, đồng bộ Sheet theo từng bước (không tải hết một lúc), gợi ý khi đang load Sheet.
22
+
23
+ ---
24
+
25
+ ## Yêu cầu / Requirements
23
26
 
24
- Then run `copyhub` from any directory (ensure Node.js ≥ 18 is on your `PATH`).
27
+ - **Node.js** ≥ 18
28
+ - Một **Google Cloud project** có:
29
+ - **Google Sheets API** bật **trên cùng project** với OAuth client
30
+ - OAuth client kiểu **Web application** và redirect URI đúng (xem dưới)
25
31
 
26
- On Linux/macOS you may need elevated permissions or an npm prefix configured for your user; see [npm docs on global installs](https://docs.npmjs.com/cli/v10/commands/npm-install#global-installation).
32
+ ---
27
33
 
28
- ### From source (this repository)
34
+ ## Cài đặt / Installation
29
35
 
30
- From the repository root:
36
+ ### Cài global (npm)
31
37
 
32
38
  ```bash
33
- npm install
39
+ npm install -g copyhub-cli
34
40
  ```
35
41
 
36
- Register the CLI on your machine:
42
+ Đảm bảo `node` `copyhub` có trong `PATH`. Linux/macOS có thể cần cấu hình prefix npm cho user — xem [npm global installation](https://docs.npmjs.com/cli/v10/commands/npm-install#global-installation).
43
+
44
+ ### Từ mã nguồn (repo)
37
45
 
38
46
  ```bash
47
+ npm install
39
48
  npm link
40
49
  ```
41
50
 
42
- Or run without linking:
51
+ Hoặc không link:
43
52
 
44
53
  ```bash
45
54
  node src/cli.js <command>
46
55
  ```
47
56
 
48
- ## Updating
57
+ ---
49
58
 
50
- Your settings (`~/.copyhub/config.json`, tokens, history) are kept when you upgrade the CLI.
59
+ ## File `.env` thứ tự đọc / Environment files
51
60
 
52
- **Recommended:** stop the background daemon before upgrading so Electron can reinstall cleanly; after upgrading start again:
61
+ CLI và Electron overlay gọi `loadCopyhubEnv()`: đọc lần lượt các file `.env`, parse rồi **gộp một object** **file sau ghi đè key trùng** với file trước. Sau đó mỗi key chỉ được đưa vào `process.env` nếu biến đó **chưa tồn tại** trong môi trường tiến trình (shell đã `export` trước khi chạy Node thì luôn thắng).
53
62
 
54
- ```bash
55
- copyhub stop
56
- ```
63
+ Thứ tự file:
57
64
 
58
- *(Skip `stop` if you only run `--foreground` or `overlay` and nothing is in the background.)*
65
+ 1. `<package>/.env` (thư mục cài package / repo khi dev)
66
+ 2. `~/.copyhub/.env`
67
+ 3. `./.env` của **thư mục làm việc hiện tại** (`cwd`)
59
68
 
60
- ### Global install (`npm install -g copyhub-cli`)
69
+ Nhờ đó `npm install -g` vẫn đọc được biến khi bạn đặt `~/.copyhub/.env`, không phụ thuộc cwd.
61
70
 
62
- Check what npm considers latest vs what you have:
71
+ Tham khảo mẫu: `.env.example`.
63
72
 
64
- ```bash
65
- npm view copyhub-cli version
66
- ```
73
+ ---
67
74
 
68
- Upgrade to the latest published release:
75
+ ## Google Cloud & OAuth
69
76
 
70
- ```bash
71
- npm install -g copyhub-cli@latest
72
- ```
77
+ 1. Bật **[Google Sheets API](https://console.cloud.google.com/apis/library/sheets.googleapis.com)** cho project.
78
+ 2. **Credentials** **Create credentials** → **OAuth client ID** → kiểu **Web application**.
79
+ 3. **Authorized redirect URIs** — thêm **chính xác** (CopyHub dùng `127.0.0.1`, không dùng `localhost` trong redirect mặc định):
73
80
 
74
- Alternatively:
81
+ ```text
82
+ http://127.0.0.1:19999/oauth2callback
83
+ ```
75
84
 
76
- ```bash
77
- npm update -g copyhub-cli
78
- ```
85
+ Nếu đổi port (`COPYHUB_OAUTH_REDIRECT_PORT` hoặc `redirectPort` trong config), URI trong Console phải khớp port đó.
79
86
 
80
- Restart CopyHub:
87
+ 4. **Không** trộn Client ID trong env với Secret trong file (CopyHub đã chặn trộn nửa cặp). Ưu tiên credentials — xem mục sau.
81
88
 
82
- ```bash
83
- copyhub start
84
- ```
89
+ ### Cách đưa Client ID / Secret vào CopyHub
90
+
91
+ | Cách | Ghi chú |
92
+ |------|---------|
93
+ | **`copyhub login`** | Khuyến nghị lần đầu: wizard localhost nhập ID/Secret → lưu `config.json` → Google đăng nhập → trang nhập Spreadsheet ID / phím tắt. |
94
+ | **`copyhub config --client-id … --client-secret …`** | Ghi thẳng `config.json`. |
95
+ | **`.env`** hoặc shell | Chỉ dùng khi **chưa** có đủ cặp trong `config.json`, hoặc bạn cố ý chỉ dùng env (không ghi OAuth vào config). |
85
96
 
86
- To see the installed package version: `npm list -g copyhub-cli --depth=0`.
97
+ Trên wizard (Mac/Safari): nên **Download JSON** từ Console và paste `client_id` / `client_secret`; xóa sạch ô trước khi dán để tránh Keychain điền nhầm secret cũ.
87
98
 
88
- ### From source
99
+ ---
89
100
 
90
- Pull latest commits and reinstall modules:
101
+ ## OAuth: config vs env (quan trọng)
102
+
103
+ - Nếu **`~/.copyhub/config.json` có đủ** `clientId` **và** `clientSecret` → CopyHub **luôn dùng cặp trong file** cho OAuth; **`COPYHUB_GOOGLE_*` trong env/.env bị bỏ qua** cho hai field đó.
104
+ - Nếu file **không** có đủ cặp → dùng **`COPYHUB_GOOGLE_CLIENT_ID`** + **`COPYHUB_GOOGLE_CLIENT_SECRET`** từ env (đã merge từ `.env`).
105
+ - Không bao giờ ghép ID env với Secret file (hoặc ngược lại).
106
+
107
+ Giá trị ID/Secret được **sanitize** khi đọc/ghi (BOM, CRLF, NBSP, ký tự zero-width, ngoặc dư thừa quanh chuỗi).
108
+
109
+ Kiểm tra nguồn đang dùng:
91
110
 
92
111
  ```bash
93
- copyhub stop
94
- git pull
95
- npm install
112
+ copyhub status
96
113
  ```
97
114
 
98
- If you previously ran **`npm link`**, linking stays tied to this folder — after `npm install` you usually **do not** need to link again unless npm warns otherwise:
115
+ ---
116
+
117
+ ## Chạy lần đầu / First run
99
118
 
100
119
  ```bash
101
- npm link
120
+ copyhub login
102
121
  ```
103
122
 
104
- Then:
123
+ 1. Nếu chưa có OAuth trong config/env đủ cặp → trình duyệt mở **`http://127.0.0.1:<port>/credentials`** để nhập Client ID / Secret.
124
+ 2. Sau đó đăng nhập Google; callback **`/oauth2callback`**.
125
+ 3. Trang setup: **Spreadsheet ID** (từ URL `…/d/<ID>/edit`), **platform**, **phím tắt overlay** (tuỳ chọn).
126
+
127
+ Chạy daemon (clipboard + Sheet + overlay mặc định):
105
128
 
106
129
  ```bash
107
130
  copyhub start
108
131
  ```
109
132
 
110
- ## Google Cloud setup
133
+ Terminal thể đóng; tiến trình chạy nền. Xem `copyhub list`, dừng `copyhub stop`.
111
134
 
112
- 1. Enable **[Google Sheets API](https://console.cloud.google.com/apis/library/sheets.googleapis.com)** on your OAuth project.
113
- 2. Create **OAuth 2.0 credentials** and add this **Authorized redirect URI** (adjust the port if you change it):
135
+ Chạy gắn terminal (Ctrl+C tắt hết):
114
136
 
115
- ```text
116
- http://127.0.0.1:19999/oauth2callback
117
- ```
137
+ ```bash
138
+ copyhub start --foreground
139
+ ```
118
140
 
119
- 3. Provide OAuth credentials in **any one** of these ways:
141
+ ---
120
142
 
121
- - **Recommended for first setup:** run **`copyhub login`** with no secrets configured — your browser opens a **localhost wizard** where you paste **Client ID** and **Client secret** (the same values as `COPYHUB_GOOGLE_CLIENT_ID` / `COPYHUB_GOOGLE_CLIENT_SECRET`); they are stored in `~/.copyhub/config.json`.
143
+ ## Lệnh CLI / CLI commands
122
144
 
123
- - Copy `.env.example` to **`~/.copyhub/.env`** and/or a `.env` in your project folder and set `COPYHUB_GOOGLE_CLIENT_ID`, `COPYHUB_GOOGLE_CLIENT_SECRET`, and optionally `COPYHUB_OAUTH_REDIRECT_PORT` (default **19999**). This works even with **`npm install -g`** when your shell is not in the repo directory.
145
+ | Lệnh | tả |
146
+ |------|--------|
147
+ | `copyhub config --client-id ID --client-secret SEC [--redirect-port P] [--sheet-id ID]` | Ghi OAuth (và tuỳ chọn Sheet ID, port) vào `config.json`. |
148
+ | `copyhub login` | Luồng OAuth + setup trình duyệt. |
149
+ | `copyhub logout` | Xóa `tokens.json` (chưa xóa config). |
150
+ | `copyhub status` | OAuth, Sheet, token, overlay, daemon. |
151
+ | `copyhub start [--no-sheet] [--no-overlay] [--foreground]` | Mặc định **nền**; một instance. |
152
+ | `copyhub list` / `copyhub ls` | PID daemon (nếu có). |
153
+ | `copyhub stop` | Dừng daemon và overlay con. |
154
+ | `copyhub overlay` | Chỉ cửa sổ Electron (không clipboard daemon). |
155
+ | `copyhub reset --yes` | **Xóa toàn bộ** `~/.copyhub` (config, token, history, run state). Dữ liệu `.env` ngoài thư mục không đụng. |
156
+ | `copyhub commands` / `copyhub cmds` | Liệt kê lệnh nhanh. |
157
+ | `copyhub --help` | Trợ giúp Commander. |
124
158
 
125
- - Or run **`copyhub config`**:
159
+ ---
126
160
 
127
- ```bash
128
- copyhub config --client-id "<ID>" --client-secret "<SECRET>" [--sheet-id "<SPREADSHEET_ID>"] [--redirect-port 19999]
129
- ```
161
+ ## Biến môi trường / Environment variables
130
162
 
131
- ## First run
163
+ | Biến | Ý nghĩa |
164
+ |------|---------|
165
+ | `COPYHUB_GOOGLE_CLIENT_ID` | OAuth Client ID (chỉ hiệu lực khi config **không** có đủ cặp ID+Secret). |
166
+ | `COPYHUB_GOOGLE_CLIENT_SECRET` | OAuth Client Secret (cùng điều kiện trên). |
167
+ | `COPYHUB_OAUTH_REDIRECT_PORT` | Port localhost cho OAuth (mặc định `19999`). Phải khớp redirect URI trong Google Console. |
168
+ | `COPYHUB_OVERLAY_ACCELERATOR` | Phím tắt Electron ([Accelerator](https://www.electronjs.org/docs/latest/api/accelerator)); **ghi đè** giá trị trong config nếu set. |
169
+ | `COPYHUB_START_NO_OVERLAY` | `=1` → `copyhub start` không spawn overlay. |
170
+ | `COPYHUB_OVERLAY_STICKY` | `=1` → overlay không tự ẩn khi blur (chỉ Esc / chọn dòng). |
171
+ | `COPYHUB_OVERLAY_HIDE_ON_START` | `=1` → không show cửa sổ lúc khởi động overlay (mở bằng shortcut/tray). |
172
+ | `COPYHUB_OVERLAY_SKIP_TASKBAR` | `=1` → ẩn khỏi taskbar (Windows/Electron). |
173
+ | `COPYHUB_POLL_MS` | Chu kỳ đọc clipboard (ms). |
132
174
 
133
- 1. **Login** opens the browser:
175
+ Electron được spawn kế thừa `process.env` của daemon/CLI nên các biến trên áp dụng sau khi có trong môi trường tiến trình cha.
134
176
 
135
- ```bash
136
- copyhub login
137
- ```
177
+ ---
138
178
 
139
- - If Client ID / Secret are **not** already in `.env` or `~/.copyhub/config.json`, the first screen collects them (**Google OAuth credentials** page). Submit it to save into config.
140
- - After Google sign-in, another setup page asks for **Spreadsheet ID**, **platform**, and optional **overlay shortcut**.
179
+ ## Thư mục dữ liệu / Data directory
141
180
 
142
- 2. On that spreadsheet/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.
181
+ Mọi thứ dưới **`~/.copyhub/`** (Windows: **`%USERPROFILE%\.copyhub`**):
143
182
 
144
- 3. **Start** the background watcher (clipboard + Sheets + overlay by default):
183
+ | File | Nội dung |
184
+ |------|-----------|
185
+ | `config.json` | OAuth (`clientId`, `clientSecret`, `redirectPort`), `googleSheetId`, `overlayAccelerator`, `overlayPlatform`, … |
186
+ | `tokens.json` | Refresh / access token OAuth |
187
+ | `history.jsonl` | Lịch sử clipboard (JSON Lines) |
188
+ | `run.json` | PID và metadata khi `copyhub start` chạy nền |
145
189
 
146
- ```bash
147
- copyhub start
148
- ```
190
+ ---
191
+
192
+ ## Google Sheets
193
+
194
+ - Append dòng khi bật Sheet và đã login token hợp lệ.
195
+ - Tab mới theo **ngày lịch máy**: `COPYHUB-YYYY-MM-DD`.
196
+ - Sheet phải được share cho đúng Google account đã OAuth (hoặc là spreadsheet của chính user đó).
197
+ - Nếu API báo disabled / permission: xem log — README có link Enable API trong một số thông báo lỗi được format sẵn trong code.
198
+
199
+ ---
200
+
201
+ ## Overlay (Electron)
202
+
203
+ - Phím mặc định (**mọi hệ**, kể cả macOS): **`Control+Shift+H`** — phím **⌃ Control** (góc dưới trái trên bàn phím Apple) hoặc **Ctrl** (bàn phím PC), **cùng một vị trí quen thuộc**, không phụ thuộc ⌘ / Win.
204
+ - Muốn dùng **⌘ Command + Shift + H** trên Mac: chỉnh `overlayAccelerator` trong `~/.copyhub/config.json`, đặt `COPYHUB_OVERLAY_ACCELERATOR`, hoặc chọn preset trên trang setup (`CommandOrControl+Shift+H` / `Command+Shift+H`).
205
+ - **macOS**: vẫn có thể cần quyền **Accessibility** (*Privacy & Security*) cho **Terminal** / tiến trình chạy Electron nếu phím global không hoạt động.
206
+ - Overlay có phân trang ~10 mục; Sheet load dần khi cần (không gom hết tab một lần).
207
+ - Click ra ngoài cửa sổ thường đóng overlay (trừ khi `COPYHUB_OVERLAY_STICKY=1`). **Esc** đóng.
149
208
 
150
- You can close the terminal; the process keeps running. Check with `copyhub list` and stop with `copyhub stop`.
209
+ ---
151
210
 
152
- ### Useful flags and environment variables
211
+ ## Clipboard & lịch sử
153
212
 
154
- | Action | How |
155
- |--------|-----|
156
- | Run in terminal (Ctrl+C stops everything) | `copyhub start --foreground` |
157
- | No Google Sheets | `copyhub start --no-sheet` |
158
- | No Electron overlay | `copyhub start --no-overlay` or `COPYHUB_START_NO_OVERLAY=1` |
159
- | Override shortcut | `COPYHUB_OVERLAY_ACCELERATOR` in `.env` (overrides saved config) |
160
- | Overlay stays open when clicking outside | `COPYHUB_OVERLAY_STICKY=1` |
213
+ - Watcher bỏ qua clipboard trùng hash liên tiếp.
214
+ - Trước khi ghi file/Sheet, nếu nội dung **trùng hệt bản mới nhất** trong `history.jsonl` thì **bỏ qua** (tránh copy lại cùng chuỗi sau khi đổi clipboard khác rồi copy lại).
161
215
 
162
- Run `copyhub --help` or `copyhub commands` for built-in help.
216
+ ---
163
217
 
164
- ## CLI commands
218
+ ## Nâng cấp / Updating
165
219
 
166
- Quick reference (same as `copyhub commands`):
220
+ Dữ liệu `~/.copyhub` được giữ khi nâng cấp package.
167
221
 
168
222
  ```bash
169
- copyhub config --client-id "<ID>" --client-secret "<SECRET>" [--redirect-port 19999] [--sheet-id "<SPREADSHEET_ID>"]
170
- copyhub login
171
- copyhub logout
172
- copyhub status
173
- copyhub start [--no-sheet] [--no-overlay] [--foreground]
174
- copyhub list # alias: copyhub ls
175
223
  copyhub stop
176
- copyhub overlay
177
- copyhub commands # alias: copyhub cmds
178
- copyhub --help
224
+ npm install -g copyhub-cli@latest
225
+ copyhub start
179
226
  ```
180
227
 
181
- | Command | What it does |
182
- |---------|----------------|
183
- | `copyhub config` | Writes OAuth client ID/secret (and optional Sheet ID, redirect port) to `~/.copyhub/config.json`. `--client-id` and `--client-secret` are required. |
184
- | `copyhub login` | Opens browser: localhost wizard for Client ID/secret if missing, then Google OAuth, then spreadsheet/platform setup. |
185
- | `copyhub logout` | Deletes saved OAuth tokens (`~/.copyhub/tokens.json`). |
186
- | `copyhub status` | Prints OAuth config source, sheet target, tokens, overlay settings, and whether the background daemon is running. |
187
- | `copyhub start` | Starts clipboard watcher + optional Sheets sync + Electron overlay in the **background** (closing the terminal does not stop it). Only one instance at a time. |
188
- | `copyhub start --foreground` | Same as above but attached to the terminal; **Ctrl+C** stops everything. |
189
- | `copyhub start --no-sheet` | Local history only; no Google Sheets writes. |
190
- | `copyhub start --no-overlay` | No Electron window; use env `COPYHUB_START_NO_OVERLAY=1` for the same effect. |
191
- | `copyhub list` / `copyhub ls` | Shows PID and start time if the daemon from `copyhub start` is running. |
192
- | `copyhub stop` | Stops the background daemon and its overlay child process. |
193
- | `copyhub overlay` | Runs **only** the Electron overlay (no clipboard daemon). Useful if you run the daemon separately or for debugging. |
228
+ Từ source: `git pull`, `npm install`, rồi `copyhub start` (hoặc `npm link` nếu dev).
194
229
 
195
- ## Data locations
230
+ ---
196
231
 
197
- Everything lives under **`~/.copyhub/`** (or `%USERPROFILE%\.copyhub` on Windows):
232
+ ## Xử sự cố / Troubleshooting
198
233
 
199
- | File | Contents |
200
- |------|----------|
201
- | `config.json` | OAuth credentials (if not only in `.env`), `googleSheetId`, `overlayAccelerator`, `overlayPlatform` |
202
- | `tokens.json` | OAuth refresh/access tokens |
203
- | `history.jsonl` | Local clipboard history (JSON Lines) |
204
- | `run.json` | Daemon PID and metadata (when using `copyhub start` without `--foreground`) |
234
+ ### `invalid_client` hoặc “client secret is invalid” sau khi đăng nhập Google
205
235
 
206
- ## Google Sheets layout
236
+ - Dùng OAuth client **Web application**, redirect đúng `http://127.0.0.1:<port>/oauth2callback`.
237
+ - Reset secret hoặc **Download JSON** client mới; nhập lại qua wizard; trên Mac **xóa ô** trước khi dán.
238
+ - `copyhub status` — xem nguồn Client ID/Secret.
239
+ - Chi tiết thêm: CopyHub có trang lỗi HTML khi đổi `code` token thất bại.
207
240
 
208
- - Rows are appended when Sheet sync is enabled and you are logged in.
209
- - New tabs are created per **local calendar day**, named: **`COPYHUB-YYYY-MM-DD`**.
241
+ ### Port OAuth đã được dùng (`EADDRINUSE`)
210
242
 
211
- ## Overlay (Electron)
243
+ Đổi port: `COPYHUB_OAUTH_REDIRECT_PORT` hoặc `copyhub config … --redirect-port P`, và cập nhật redirect URI trong Google Console.
244
+
245
+ ### `copyhub start` báo đã chạy
246
+
247
+ Một instance nền: `copyhub list` / `copyhub stop` rồi start lại.
248
+
249
+ ### Sheet không ghi / lỗi API
250
+
251
+ - Bật Google Sheets API đúng project.
252
+ - Kiểm tra `copyhub status` (token, Sheet ID).
253
+ - Share spreadsheet cho account đã login.
254
+
255
+ ### Overlay không mở / phím không hoạt động
256
+
257
+ - **macOS**: bật **Accessibility** cho Terminal / Node / Electron.
258
+ - Mặc định là **Control+Shift+H** (⌃ hoặc Ctrl + Shift + H), không dùng phím Win.
259
+ - Đảm bảo daemon đang chạy (`copyhub list`) hoặc thử `copyhub overlay`.
260
+ - Tránh trùng phím với app khác (Spotlight, Alfred, …).
261
+
262
+ ### Xóa sạch cấu hình và làm lại
263
+
264
+ ```bash
265
+ copyhub stop
266
+ copyhub reset --yes
267
+ ```
268
+
269
+ Sau đó xóa hoặc sửa `COPYHUB_GOOGLE_*` trong shell/`~/.copyhub/.env` nếu không muốn dùng env nữa. `.env` **không** bị `reset` xóa.
212
270
 
213
- - 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).
214
- - **macOS**: you may need to grant **Accessibility** permissions for global shortcuts.
215
- - Some **`Control+Alt+…`** combinations do not register reliably on Windows; prefer alternatives suggested on the setup page.
271
+ ---
216
272
 
217
- ## Troubleshooting: `invalid_client` after Google sign-in
273
+ ## Bảo mật / Security notes
218
274
 
219
- That response comes from Google’s token endpoint when the **Client ID + Client Secret pair** does not match your OAuth client.
275
+ - `config.json` `tokens.json` chứa mật OAuth quyền file thư mục user (`~/.copyhub`).
276
+ - Không commit `.env` hoặc copyhub data vào git công khai.
220
277
 
221
- 1. **Use a “Web application” OAuth client** (not iOS/Android/Desktop-only flows meant for different redirect rules). Add **Authorized redirect URI**: `http://127.0.0.1:19999/oauth2callback` (change the port if you use `COPYHUB_OAUTH_REDIRECT_PORT` or saved `redirectPort`).
222
- 2. **Where credentials come from**: If **`~/.copyhub/config.json` contains both Client ID and Secret** (wizard or `copyhub config`), CopyHub uses **that pair** for OAuth — env vars are **ignored** for those two fields until you remove them from config. If config does **not** have both, CopyHub uses **`COPYHUB_GOOGLE_CLIENT_ID` / `COPYHUB_GOOGLE_CLIENT_SECRET`** from the environment / `.env`. CopyHub never mixes env Client ID with file Secret (that mismatch triggers `invalid_client`).
223
- 3. **Paste cleanly**: Re-open the localhost credential wizard or edit `config.json` so ID and secret have **no extra spaces or line breaks**.
224
- 4. Run **`copyhub status`** and confirm **Client ID/Secret source** matches what you expect.
278
+ ---
225
279
 
226
- ## License
280
+ ## Giấy phép / License
227
281
 
228
- MIT — see `package.json`.
282
+ MIT — xem `package.json`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copyhub-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.8",
4
4
  "description": "CopyHub — clipboard, local history, Google Sheets sync (OAuth). Windows, macOS, Linux.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/config.js CHANGED
@@ -22,6 +22,34 @@ function parseRedirectPortFromEnv() {
22
22
  return n;
23
23
  }
24
24
 
25
+ /**
26
+ * Strip invisible / stray characters from pasted OAuth credentials (fixes many Mac copy-paste issues).
27
+ * @param {unknown} raw
28
+ * @returns {string}
29
+ */
30
+ export function sanitizeOAuthCredentialInput(raw) {
31
+ if (raw == null) return '';
32
+ let s = String(raw);
33
+ s = s.replace(/^\uFEFF/, '');
34
+ s = s.replace(/[\u200b-\u200d\u2060]/g, '');
35
+ s = s.replace(/\r/g, '');
36
+ s = s.replace(/[\u00a0\u202f\u2007]/g, ' ');
37
+ s = s.trim();
38
+ if (
39
+ (s.startsWith('"') && s.endsWith('"')) ||
40
+ (s.startsWith("'") && s.endsWith("'"))
41
+ ) {
42
+ s = s.slice(1, -1).trim();
43
+ }
44
+ if (
45
+ (s.startsWith('\u201c') && s.endsWith('\u201d')) ||
46
+ (s.startsWith('\u2018') && s.endsWith('\u2019'))
47
+ ) {
48
+ s = s.slice(1, -1).trim();
49
+ }
50
+ return s;
51
+ }
52
+
25
53
  /**
26
54
  * Port for the OAuth HTTP listener (env wins, then saved config, then default).
27
55
  * Does not require Client ID / Secret (used before credential bootstrap).
@@ -44,8 +72,8 @@ export function resolveOAuthListenPort() {
44
72
 
45
73
  /** Both Client ID and Secret come from environment (or .env). */
46
74
  export function hasOAuthCredentialsInEnv() {
47
- const id = process.env[ENV_GOOGLE_CLIENT_ID]?.trim();
48
- const sec = process.env[ENV_GOOGLE_CLIENT_SECRET]?.trim();
75
+ const id = sanitizeOAuthCredentialInput(process.env[ENV_GOOGLE_CLIENT_ID]);
76
+ const sec = sanitizeOAuthCredentialInput(process.env[ENV_GOOGLE_CLIENT_SECRET]);
49
77
  return Boolean(id && sec);
50
78
  }
51
79
 
@@ -58,8 +86,10 @@ export function describeEffectiveOAuthCredentialSource() {
58
86
  if (existsSync(CONFIG_PATH)) {
59
87
  try {
60
88
  const j = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
61
- const id = typeof j.clientId === 'string' ? j.clientId.trim() : '';
62
- const sec = typeof j.clientSecret === 'string' ? j.clientSecret.trim() : '';
89
+ const id =
90
+ typeof j.clientId === 'string' ? sanitizeOAuthCredentialInput(j.clientId) : '';
91
+ const sec =
92
+ typeof j.clientSecret === 'string' ? sanitizeOAuthCredentialInput(j.clientSecret) : '';
63
93
  filePair = Boolean(id && sec);
64
94
  } catch {
65
95
  /* ignore */
@@ -88,11 +118,11 @@ export async function loadConfig() {
88
118
  const raw = await readFile(CONFIG_PATH, 'utf8');
89
119
  const j = JSON.parse(raw);
90
120
  if (typeof j.clientId === 'string') {
91
- const id = j.clientId.trim();
121
+ const id = sanitizeOAuthCredentialInput(j.clientId);
92
122
  if (id) fromFile.clientId = id;
93
123
  }
94
124
  if (typeof j.clientSecret === 'string') {
95
- const sec = j.clientSecret.trim();
125
+ const sec = sanitizeOAuthCredentialInput(j.clientSecret);
96
126
  if (sec) fromFile.clientSecret = sec;
97
127
  }
98
128
  if (typeof j.redirectPort === 'number' && Number.isFinite(j.redirectPort)) {
@@ -100,8 +130,8 @@ export async function loadConfig() {
100
130
  }
101
131
  }
102
132
 
103
- const envId = process.env[ENV_GOOGLE_CLIENT_ID]?.trim() ?? '';
104
- const envSecret = process.env[ENV_GOOGLE_CLIENT_SECRET]?.trim() ?? '';
133
+ const envId = sanitizeOAuthCredentialInput(process.env[ENV_GOOGLE_CLIENT_ID]);
134
+ const envSecret = sanitizeOAuthCredentialInput(process.env[ENV_GOOGLE_CLIENT_SECRET]);
105
135
  const envPort = parseRedirectPortFromEnv();
106
136
 
107
137
  const filePort =
@@ -208,10 +238,14 @@ export async function mergeConfigPartial(partial) {
208
238
  const out = { ...existing, ...partial };
209
239
  delete out.sheetTab;
210
240
  delete out.sheetDailyPrefix;
211
- for (const key of ['clientId', 'clientSecret', 'googleSheetId']) {
212
- if (typeof out[key] === 'string') {
213
- out[key] = /** @type {string} */ (out[key]).trim();
214
- }
241
+ if (typeof out.clientId === 'string') {
242
+ out.clientId = sanitizeOAuthCredentialInput(out.clientId);
243
+ }
244
+ if (typeof out.clientSecret === 'string') {
245
+ out.clientSecret = sanitizeOAuthCredentialInput(out.clientSecret);
246
+ }
247
+ if (typeof out.googleSheetId === 'string') {
248
+ out.googleSheetId = sanitizeOAuthCredentialInput(out.googleSheetId);
215
249
  }
216
250
  await writeFile(CONFIG_PATH, JSON.stringify(out, null, 2), 'utf8');
217
251
  }
@@ -232,11 +266,16 @@ export async function saveConfig(cfg) {
232
266
  }
233
267
  const out = {
234
268
  ...existing,
235
- clientId: cfg.clientId.trim(),
236
- clientSecret: cfg.clientSecret.trim(),
269
+ clientId: sanitizeOAuthCredentialInput(cfg.clientId),
270
+ clientSecret: sanitizeOAuthCredentialInput(cfg.clientSecret),
237
271
  redirectPort: cfg.redirectPort ?? DEFAULT_OAUTH_REDIRECT_PORT,
238
272
  };
239
- if (cfg.googleSheetId !== undefined) out.googleSheetId = cfg.googleSheetId;
273
+ if (cfg.googleSheetId !== undefined) {
274
+ out.googleSheetId =
275
+ typeof cfg.googleSheetId === 'string'
276
+ ? sanitizeOAuthCredentialInput(cfg.googleSheetId)
277
+ : cfg.googleSheetId;
278
+ }
240
279
  delete out.sheetTab;
241
280
  delete out.sheetDailyPrefix;
242
281
  await writeFile(CONFIG_PATH, JSON.stringify(out, null, 2), 'utf8');
package/src/oauth.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  loadSheetSyncTarget,
15
15
  loadOverlayAcceleratorFromConfigSync,
16
16
  loadOverlayPlatformFromConfigSync,
17
+ sanitizeOAuthCredentialInput,
17
18
  } from './config.js';
18
19
  import { saveTokens, loadTokens } from './tokens.js';
19
20
  import { TOKENS_PATH } from './paths.js';
@@ -90,10 +91,15 @@ function formatOAuthTokenExchangeMessage(err) {
90
91
  const fallback = /** @type {Error} */ (err)?.message || String(err);
91
92
 
92
93
  if (code === 'invalid_client') {
94
+ const secretInvalid = /client secret is invalid|invalid_client_secret/i.test(desc);
95
+ const secretHint = secretInvalid
96
+ ? 'Google says the Client Secret is wrong for this Client ID. Open Cloud Console → APIs & Services → Credentials → your Web client → reset secret or download JSON again; paste client_id and client_secret from that JSON into the CopyHub wizard (Safari/Chrome may autofill an old secret — clear fields first). '
97
+ : '';
93
98
  return (
94
99
  'OAuth invalid_client: Google rejected the Client ID / Client Secret pair. ' +
95
- 'In Google Cloud Console use OAuth client type "Web application" and add redirect URI http://127.0.0.1:<port>/oauth2callback (port matches CopyHub). Paste fresh credentials into the wizard — no spaces before/after. ' +
96
- 'If COPYHUB_GOOGLE_CLIENT_ID / COPYHUB_GOOGLE_CLIENT_SECRET are set in your shell or ~/.copyhub/.env, set BOTH to match that client or unset both so credentials from ~/.copyhub/config.json are used (CopyHub never mixes env ID with file secret). ' +
100
+ secretHint +
101
+ 'Use OAuth client type "Web application" and add redirect URI http://127.0.0.1:<port>/oauth2callback (port matches CopyHub). Prefer pasting values from the client\'s Download JSON file to avoid typos. ' +
102
+ 'If COPYHUB_GOOGLE_CLIENT_ID / COPYHUB_GOOGLE_CLIENT_SECRET exist in shell or ~/.copyhub/.env, they must match this client or remove both so ~/.copyhub/config.json is used. ' +
97
103
  (desc ? `Google says: ${desc}` : `(${fallback})`)
98
104
  );
99
105
  }
@@ -228,6 +234,7 @@ function credentialSetupPageHtml(bootstrapToken, listenPort) {
228
234
  <div class="brand">CopyHub</div>
229
235
  <h1>Google Cloud OAuth</h1>
230
236
  <p class="sub">Paste your OAuth 2.0 Client ID and Client Secret (same as <code>${ENV_GOOGLE_CLIENT_ID}</code> / <code>${ENV_GOOGLE_CLIENT_SECRET}</code> in <code>.env</code>). Stored in <code>~/.copyhub/config.json</code>. After saving here, CopyHub uses this pair for sign-in — not leftover variables from shell or <code>.env</code>.</p>
237
+ <p class="hint" style="margin-bottom:20px;"><strong>Mac / Safari:</strong> copy from Google Cloud → Credentials → your <strong>Web client</strong> → <strong>Download JSON</strong> and paste <code>client_id</code> / <code>client_secret</code> exactly. Clear both fields if the browser autofills an old secret.</p>
231
238
 
232
239
  <div class="card">
233
240
  <p class="hint" style="margin-top:0;"><strong>Authorized redirect URI</strong> in Google Cloud Console must include:</p>
@@ -235,13 +242,13 @@ function credentialSetupPageHtml(bootstrapToken, listenPort) {
235
242
  <p class="hint">Port comes from <code>${ENV_OAUTH_REDIRECT_PORT}</code> (currently <strong>${listenPort}</strong>) or your saved config.</p>
236
243
  </div>
237
244
 
238
- <form method="POST" action="/credentials">
245
+ <form method="POST" action="/credentials" autocomplete="off">
239
246
  <input type="hidden" name="t" value="${tVal}" />
240
247
  <div class="card">
241
248
  <label class="field-label" for="cid">Client ID</label>
242
- <input id="cid" type="text" name="clientId" autocomplete="off" spellcheck="false" required />
249
+ <input id="cid" type="text" name="clientId" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required />
243
250
  <label class="field-label" for="csec" style="margin-top:16px;">Client secret</label>
244
- <input id="csec" type="password" name="clientSecret" autocomplete="new-password" spellcheck="false" required />
251
+ <input id="csec" type="text" name="clientSecret" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" required placeholder="Usually starts with GOCSPX-" />
245
252
  </div>
246
253
  <button type="submit" class="submit">Save and continue to Google sign-in</button>
247
254
  </form>
@@ -316,8 +323,8 @@ async function runCredentialBootstrap() {
316
323
  res.end('Forbidden');
317
324
  return;
318
325
  }
319
- const clientId = (params.get('clientId') || '').trim();
320
- const clientSecret = (params.get('clientSecret') || '').trim();
326
+ const clientId = sanitizeOAuthCredentialInput(params.get('clientId'));
327
+ const clientSecret = sanitizeOAuthCredentialInput(params.get('clientSecret'));
321
328
  if (!clientId || !clientSecret) {
322
329
  res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
323
330
  res.end('<p>Client ID and Client secret are required.</p>');
@@ -376,18 +383,20 @@ async function runCredentialBootstrap() {
376
383
  /** Shortcut presets (Electron Accelerator) per platform — embedded as JSON in the setup page. */
377
384
  const PLATFORM_PRESETS = {
378
385
  win: [
379
- { label: 'Ctrl + Shift + H · recommended', value: 'CommandOrControl+Shift+H' },
380
- { label: 'Control + Shift + H', value: 'Control+Shift+H' },
386
+ { label: 'Ctrl + Shift + H · recommended', value: 'Control+Shift+H' },
381
387
  { label: 'Alt + Shift + H', value: 'Alt+Shift+H' },
382
388
  ],
383
389
  mac: [
384
- { label: '⌘ + Shift + H · recommended', value: 'CommandOrControl+Shift+H' },
385
- { label: 'Command + Shift + H', value: 'Command+Shift+H' },
390
+ {
391
+ label: ' Control + Shift + H · recommended (Apple & PC keyboards)',
392
+ value: 'Control+Shift+H',
393
+ },
394
+ { label: '⌘ Command + Shift + H', value: 'CommandOrControl+Shift+H' },
395
+ { label: 'Command + Shift + H (explicit ⌘)', value: 'Command+Shift+H' },
386
396
  { label: '⌘ + Shift + V', value: 'Command+Shift+V' },
387
397
  ],
388
398
  linux: [
389
- { label: 'Ctrl + Shift + H · recommended', value: 'CommandOrControl+Shift+H' },
390
- { label: 'Control + Shift + H', value: 'Control+Shift+H' },
399
+ { label: 'Ctrl + Shift + H · recommended', value: 'Control+Shift+H' },
391
400
  { label: 'Alt + Shift + H', value: 'Alt+Shift+H' },
392
401
  ],
393
402
  };
@@ -625,7 +634,7 @@ function setupPageHtml(setupToken, currentSheetId, currentAccelerator, currentPl
625
634
  <button type="button" class="platform-btn" data-platform="mac" aria-pressed="false">
626
635
  <span class="ico" aria-hidden="true">⌘</span>
627
636
  <span class="name">macOS</span>
628
- <span class="tag">⌘ Command</span>
637
+ <span class="tag">⌃ Control default</span>
629
638
  </button>
630
639
  <button type="button" class="platform-btn" data-platform="linux" aria-pressed="false">
631
640
  <span class="ico" aria-hidden="true">🐧</span>
@@ -634,10 +643,10 @@ function setupPageHtml(setupToken, currentSheetId, currentAccelerator, currentPl
634
643
  </button>
635
644
  </div>
636
645
 
637
- <label class="field-label" for="acc">Accelerator (blank = default Ctrl/⌘ + Shift + H)</label>
646
+ <label class="field-label" for="acc">Accelerator (blank = Control + Shift + H)</label>
638
647
  <input id="acc" type="text" name="overlayAccelerator" value="${accVal}" placeholder="Pick a preset below or type your own" autocomplete="off" spellcheck="false" />
639
648
 
640
- <p class="hint">On Windows use <code>Control</code> in config, not <code>Ctrl</code>. Avoid <code>Control+Alt+…</code> (often grabbed by drivers).</p>
649
+ <p class="hint">Default everywhere: <code>Control+Shift+H</code> (⌃ or Ctrl + Shift + H — same key on Mac Apple keyboard & PC keyboard). On Windows type <code>Control</code> in config, not <code>Ctrl</code>. Avoid <code>Control+Alt+…</code> (often grabbed by drivers).</p>
641
650
  <p class="hint"><code>COPYHUB_OVERLAY_ACCELERATOR</code> in <code>.env</code>, if set, overrides this value.</p>
642
651
 
643
652
  <div id="chipRegion" class="chips" aria-live="polite"></div>
@@ -663,9 +672,9 @@ function setupPageHtml(setupToken, currentSheetId, currentAccelerator, currentPl
663
672
  var btns = document.querySelectorAll('.platform-btn');
664
673
 
665
674
  var hints = {
666
- win: 'Windows: CommandOrControl = Ctrl.',
667
- mac: 'macOS: CommandOrControl = ⌘ Command.',
668
- linux: 'Linux: same as Windows with Ctrl; clipboard depends on your desktop.'
675
+ win: 'Windows: default Control+Shift+H.',
676
+ mac: 'macOS: default Control+Shift+H (same as Ctrl on a PC keyboard). Use a preset only if you prefer Command.',
677
+ linux: 'Linux: default Control+Shift+H; clipboard depends on your desktop.'
669
678
  };
670
679
 
671
680
  function setPlatform(p) {
package/ui/main.mjs CHANGED
@@ -32,7 +32,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
32
  /** Set to 1 so the window does not hide on blur (only Esc / pick row to copy). */
33
33
  const STICKY_NO_BLUR = process.env.COPYHUB_OVERLAY_STICKY === '1';
34
34
 
35
- /** Electron Accelerator: use `Control`, not `Ctrl`; `CommandOrControl` = Ctrl (Win) / Cmd (Mac). */
35
+ /** Electron Accelerator: use `Control`, not `Ctrl`; avoid bare `Ctrl` in strings Electron expects `Control`. */
36
36
  function normalizeAccelerator(raw) {
37
37
  if (!raw || typeof raw !== 'string') return '';
38
38
  let s = raw.trim();
@@ -42,7 +42,8 @@ function normalizeAccelerator(raw) {
42
42
  return s;
43
43
  }
44
44
 
45
- const DEFAULT_ACCEL = 'CommandOrControl+Shift+H';
45
+ /** Same physical ⌃ / Ctrl key on Mac (Apple & Windows-layout keyboards) and on Win/Linux — one default everywhere. */
46
+ const DEFAULT_ACCEL = 'Control+Shift+H';
46
47
  const HIDE_ON_START = process.env.COPYHUB_OVERLAY_HIDE_ON_START === '1';
47
48
 
48
49
  /** Overlay size (slightly larger than earlier ~70% width). */
@@ -213,7 +214,7 @@ function toggleOverlay() {
213
214
  }
214
215
 
215
216
  /**
216
- * Register global shortcut: try .env (normalized) then default CommandOrControl+Shift+H.
217
+ * Register global shortcut: try .env / saved config (normalized) then default Control+Shift+H.
217
218
  * @returns {{ accelerator: string, usedFallback: boolean }}
218
219
  */
219
220
  function registerHotkeys() {
@@ -546,12 +547,12 @@ if (gotLock) {
546
547
  };
547
548
  if (accelerator) {
548
549
  console.log('CopyHub overlay — shortcut in use:', accelerator);
549
- console.log('Windows tip: Ctrl+Shift+H (CommandOrControl+Shift+H).');
550
+ console.log('Default shortcut: Control+Shift+H (⌃ or Ctrl + Shift + H).');
550
551
  if (usedFallback) {
551
552
  console.warn(
552
- 'COPYHUB_OVERLAY_ACCELERATOR could not be registered. Using default CommandOrControl+Shift+H.',
553
+ 'COPYHUB_OVERLAY_ACCELERATOR could not be registered. Using default Control+Shift+H.',
553
554
  );
554
- console.warn('Leave COPYHUB_OVERLAY_ACCELERATOR unset in .env to always use Ctrl+Shift+H.');
555
+ console.warn('Leave COPYHUB_OVERLAY_ACCELERATOR unset to use the default Control+Shift+H.');
555
556
  }
556
557
  } else {
557
558
  console.error(