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/.env.example +24 -24
- package/README.md +154 -122
- package/package.json +39 -39
- package/src/cli.js +342 -337
- package/src/electron-launcher.js +22 -11
- package/ui/main.mjs +331 -331
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
|
-
|
|
97
|
-
child
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (!
|
|
119
|
-
console.log(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (!
|
|
140
|
-
console.log(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
console.log(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
console.log(
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
console.log(
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
.
|
|
228
|
-
.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
copyhub
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
// 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();
|