copyhub-cli 1.0.0 → 1.0.1

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/src/cli.js CHANGED
@@ -1,337 +1,342 @@
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();
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
+ // Detach + hide console on Windows so closing the terminal does not kill the overlay.
97
+ const child = spawnCopyhubOverlay({ stdio: 'ignore', detached: true });
98
+ child.on('error', (err) => {
99
+ console.error(err.message);
100
+ process.exit(1);
101
+ });
102
+ child.once('spawn', () => {
103
+ child.unref();
104
+ });
105
+ } catch (e) {
106
+ console.error(/** @type {Error} */ (e).message);
107
+ process.exitCode = 1;
108
+ }
109
+ });
110
+
111
+ program
112
+ .command('list')
113
+ .alias('ls')
114
+ .description('Show whether the CopyHub background process (copyhub start) is running')
115
+ .action(() => {
116
+ pruneStaleRunState();
117
+ const s = readRunState();
118
+ if (!s) {
119
+ console.log('No CopyHub background process (no ~/.copyhub/run.json or already cleared).');
120
+ return;
121
+ }
122
+ if (!isPidAlive(s.pid)) {
123
+ console.log(`PID ${s.pid} is not running — removed run.json.`);
124
+ clearRunState();
125
+ return;
126
+ }
127
+ console.log('CopyHub background process is running:');
128
+ console.log(` PID: ${s.pid}`);
129
+ console.log(` Started: ${s.startedAt || '(unknown)'}`);
130
+ console.log(` Stop with: copyhub stop`);
131
+ });
132
+
133
+ program
134
+ .command('stop')
135
+ .description('Stop the background process started by copyhub start (and overlay child)')
136
+ .action(() => {
137
+ pruneStaleRunState();
138
+ const s = readRunState();
139
+ if (!s) {
140
+ console.log('No background process to stop.');
141
+ return;
142
+ }
143
+ if (!isPidAlive(s.pid)) {
144
+ console.log(`PID ${s.pid} is not running — cleared run.json.`);
145
+ clearRunState();
146
+ return;
147
+ }
148
+ killDaemonTree(s.pid);
149
+ clearRunState();
150
+ console.log(`Stopped process PID ${s.pid}.`);
151
+ });
152
+
153
+ program
154
+ .command('status')
155
+ .description('Check OAuth, Sheet, and tokens')
156
+ .action(async () => {
157
+ pruneStaleRunState();
158
+ const cfg = await loadConfig();
159
+ const sheet = await loadSheetSyncTarget();
160
+ const tok = await loadTokens();
161
+ const src = describeOAuthCredentialSource();
162
+
163
+ if (!cfg) {
164
+ console.log('OAuth config: missing');
165
+ console.log(
166
+ ` Set ${ENV_GOOGLE_CLIENT_ID} and ${ENV_GOOGLE_CLIENT_SECRET} in .env (see .env.example), or run: copyhub config`,
167
+ );
168
+ } else {
169
+ const srcLabel =
170
+ src === 'env'
171
+ ? 'environment / .env'
172
+ : src === 'mixed'
173
+ ? 'mixed .env + config file'
174
+ : CONFIG_PATH;
175
+ console.log('OAuth config: ok');
176
+ console.log(` Client ID/Secret source: ${srcLabel}`);
177
+ console.log(` Callback: http://127.0.0.1:${cfg.redirectPort}/oauth2callback`);
178
+ }
179
+
180
+ if (!sheet) {
181
+ console.log(
182
+ 'Google Sheet: not set — run copyhub login (setup page) or copyhub config ... --sheet-id <ID>',
183
+ );
184
+ } else {
185
+ const todayTab = dailySheetTabName();
186
+ console.log(
187
+ `Google Sheet: ok — ID …${sheet.spreadsheetId.slice(-8)} · today's tab: "${todayTab}"`,
188
+ );
189
+ }
190
+
191
+ console.log(
192
+ 'Token:',
193
+ tok?.refresh_token || tok?.access_token ? `present (${TOKENS_PATH})` : 'missing (run copyhub login)',
194
+ );
195
+ if (existsSync(HISTORY_PATH)) {
196
+ console.log('History:', HISTORY_PATH);
197
+ }
198
+
199
+ const plat = loadOverlayPlatformFromConfigSync();
200
+ const platLabel =
201
+ plat === 'mac' ? 'macOS' : plat === 'linux' ? 'Linux' : plat === 'win' ? 'Windows' : '(not set)';
202
+ console.log(`Overlay platform setting: ${platLabel}`);
203
+
204
+ const envAccel = process.env.COPYHUB_OVERLAY_ACCELERATOR?.trim();
205
+ const cfgAccel = loadOverlayAcceleratorFromConfigSync();
206
+ if (envAccel) {
207
+ console.log(`Overlay shortcut (.env): ${envAccel}`);
208
+ } else if (cfgAccel) {
209
+ console.log(`Overlay shortcut (config): ${cfgAccel}`);
210
+ } else {
211
+ console.log(
212
+ 'Overlay shortcut: (default Ctrl+Shift+H — set after copyhub login or COPYHUB_OVERLAY_ACCELERATOR)',
213
+ );
214
+ }
215
+
216
+ const run = readRunState();
217
+ if (run && isPidAlive(run.pid)) {
218
+ console.log(`Background process: yes (PID ${run.pid}) — copyhub list`);
219
+ } else if (run) {
220
+ console.log('Background process: run.json exists but PID is dead — run copyhub stop to clean up.');
221
+ } else {
222
+ console.log('Background process: no — copyhub start to run in background.');
223
+ }
224
+ });
225
+
226
+ program
227
+ .command('start')
228
+ .description(
229
+ 'Run clipboard + Sheet + overlay in background (terminal can close). Blocks if PID already running. Use --foreground to attach to terminal.',
230
+ )
231
+ .option('--no-sheet', 'Local history only, do not write to Sheets')
232
+ .option('--no-overlay', 'Do not launch Electron')
233
+ .option('--foreground', 'Run in foreground (Ctrl+C stops; no background PID file)')
234
+ .action(async (opts) => {
235
+ pruneStaleRunState();
236
+
237
+ const useSheet = opts.sheet !== false;
238
+ const skipOverlay =
239
+ opts.overlay === false || process.env.COPYHUB_START_NO_OVERLAY === '1';
240
+
241
+ const existing = readRunState();
242
+ if (existing && isPidAlive(existing.pid)) {
243
+ console.error(
244
+ `CopyHub already running in background (PID ${existing.pid}). See: copyhub list — Stop: copyhub stop`,
245
+ );
246
+ process.exit(1);
247
+ }
248
+ if (existing && !isPidAlive(existing.pid)) {
249
+ clearRunState();
250
+ }
251
+
252
+ if (opts.foreground) {
253
+ console.log('CopyHub foreground mode. Press Ctrl+C to stop.');
254
+ await ensureDir();
255
+
256
+ const ctrl = await runCopyhubDaemon({ useSheet, skipOverlay });
257
+
258
+ const onStop = () => {
259
+ ctrl.stopSync();
260
+ process.exit(0);
261
+ };
262
+ process.on('SIGINT', onStop);
263
+ process.on('SIGTERM', onStop);
264
+ return;
265
+ }
266
+
267
+ await ensureDir();
268
+ const daemonArgs = [CLI_JS, '_daemon'];
269
+ if (!useSheet) daemonArgs.push('--no-sheet');
270
+ if (skipOverlay) daemonArgs.push('--no-overlay');
271
+
272
+ const child = spawn(process.execPath, daemonArgs, {
273
+ detached: true,
274
+ stdio: 'ignore',
275
+ windowsHide: process.platform === 'win32',
276
+ env: { ...process.env },
277
+ });
278
+ child.unref();
279
+
280
+ if (!child.pid) {
281
+ console.error('Could not spawn background process.');
282
+ process.exit(1);
283
+ }
284
+
285
+ writeRunState({
286
+ pid: child.pid,
287
+ startedAt: new Date().toISOString(),
288
+ foreground: false,
289
+ });
290
+
291
+ console.log(`CopyHub running in background (PID ${child.pid}). You may close this terminal.`);
292
+ console.log('Check: copyhub list | Stop: copyhub stop');
293
+ process.exit(0);
294
+ });
295
+
296
+ program
297
+ .command('_daemon', { hidden: true })
298
+ .option('--no-sheet', 'internal')
299
+ .option('--no-overlay', 'internal')
300
+ .action(async (opts) => {
301
+ const useSheet = opts.sheet !== false;
302
+ const skipOverlay =
303
+ opts.overlay === false || process.env.COPYHUB_START_NO_OVERLAY === '1';
304
+
305
+ function clearMyRunState() {
306
+ try {
307
+ const cur = readRunState();
308
+ if (cur && cur.pid === process.pid) {
309
+ clearRunState();
310
+ }
311
+ } catch {
312
+ /* ignore */
313
+ }
314
+ }
315
+
316
+ const ctrl = await runCopyhubDaemon({ useSheet, skipOverlay });
317
+
318
+ const shutdown = () => {
319
+ ctrl.stopSync();
320
+ clearMyRunState();
321
+ process.exit(0);
322
+ };
323
+
324
+ process.on('SIGINT', shutdown);
325
+ process.on('SIGTERM', shutdown);
326
+ process.on('exit', clearMyRunState);
327
+ });
328
+
329
+ program
330
+ .command('commands')
331
+ .alias('cmds')
332
+ .description('List CLI commands')
333
+ .action(() => {
334
+ console.log(`copyhub config [--client-id ID] [--client-secret SEC] [--redirect-port P] [--sheet-id ID]
335
+ copyhub login | copyhub logout | copyhub status
336
+ copyhub start [--no-sheet] [--no-overlay] [--foreground]
337
+ Default runs in background (terminal can close). Single instance — second start is blocked.
338
+ copyhub list (ls) | copyhub stop
339
+ copyhub overlay | copyhub commands / copyhub --help`);
340
+ });
341
+
342
+ program.parse();