@token-dashboard/codex-usage-uploader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -0
- package/bin/codex-usage-uploader.js +9 -0
- package/package.json +20 -0
- package/src/auth.js +56 -0
- package/src/cli.js +596 -0
- package/src/collector.js +683 -0
- package/src/constants.js +35 -0
- package/src/install.js +101 -0
- package/src/launchd.js +151 -0
- package/src/parser.js +180 -0
- package/src/runtime-config.js +142 -0
- package/src/state-db.js +334 -0
- package/src/utils.js +53 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { CodexUsageUploader } from './collector.js';
|
|
3
|
+
import { identityIsBound, promptConfirm } from './auth.js';
|
|
4
|
+
import { CLI_NAME, DEFAULT_CONFIG_FILE, PRODUCT_NAME } from './constants.js';
|
|
5
|
+
import { findPackageRoot, installCurrentPackage } from './install.js';
|
|
6
|
+
import { LaunchdServiceManager } from './launchd.js';
|
|
7
|
+
import { formatStatusOutput, mergeRuntimeConfig } from './runtime-config.js';
|
|
8
|
+
import { StateDb } from './state-db.js';
|
|
9
|
+
|
|
10
|
+
const HELP_TEXT = `${PRODUCT_NAME}
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
${CLI_NAME} init [--backend-url <url>] [--interval 30] [--yes] [--package-spec <spec>]
|
|
14
|
+
${CLI_NAME} bind [--email <email>] [--employee-name <name>] [--employee-id <id>] [--yes]
|
|
15
|
+
${CLI_NAME} clear [--yes]
|
|
16
|
+
${CLI_NAME} start
|
|
17
|
+
${CLI_NAME} stop
|
|
18
|
+
${CLI_NAME} restart
|
|
19
|
+
${CLI_NAME} status
|
|
20
|
+
${CLI_NAME} logs [--lines 100]
|
|
21
|
+
${CLI_NAME} uninstall
|
|
22
|
+
|
|
23
|
+
Common options:
|
|
24
|
+
--config-file <path> Runtime config path. Default: ${DEFAULT_CONFIG_FILE}
|
|
25
|
+
--backend-url <url> Dashboard backend URL
|
|
26
|
+
--interval <seconds> Scan interval in seconds
|
|
27
|
+
--codex-auth <path> Codex auth.json path
|
|
28
|
+
--sessions-dir <path> Codex rollout root directory
|
|
29
|
+
--state-db <path> Local SQLite state DB path
|
|
30
|
+
--employee-id <id> Bind employee ID
|
|
31
|
+
--email <email> Bind employee email
|
|
32
|
+
--employee-name <name> Bind employee name
|
|
33
|
+
--device-id <id> Override device ID
|
|
34
|
+
--hostname <name> Override hostname
|
|
35
|
+
--yes Skip interactive prompts
|
|
36
|
+
--package-spec <spec> npm install spec used by init
|
|
37
|
+
--lines <n> Number of log lines for service logs
|
|
38
|
+
-h, --help Show help
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
class CliOperationalError extends Error {
|
|
42
|
+
constructor(message) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.name = 'CliOperationalError';
|
|
45
|
+
this.showUsage = false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const cliDeps = {
|
|
50
|
+
findPackageRoot,
|
|
51
|
+
installCurrentPackage,
|
|
52
|
+
createUploader(runtime) {
|
|
53
|
+
return new CodexUsageUploader({
|
|
54
|
+
sessionsDir: runtime.sessionsDir,
|
|
55
|
+
stateDbPath: runtime.stateDbPath,
|
|
56
|
+
backendUrl: runtime.backendUrl,
|
|
57
|
+
intervalSeconds: runtime.intervalSeconds,
|
|
58
|
+
codexAuthPath: runtime.codexAuthPath,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
createServiceManager(runtime) {
|
|
62
|
+
return new LaunchdServiceManager(runtime);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function parseCliArgs(argv) {
|
|
67
|
+
const options = {
|
|
68
|
+
configFile: DEFAULT_CONFIG_FILE,
|
|
69
|
+
interval: undefined,
|
|
70
|
+
yes: false,
|
|
71
|
+
lines: 100,
|
|
72
|
+
};
|
|
73
|
+
const positionals = [];
|
|
74
|
+
|
|
75
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
76
|
+
const token = argv[index];
|
|
77
|
+
if (!token.startsWith('-')) {
|
|
78
|
+
positionals.push(token);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (token === '-h' || token === '--help') {
|
|
83
|
+
options.help = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (token === '--yes') {
|
|
87
|
+
options.yes = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const [key, inlineValue] = token.split('=', 2);
|
|
92
|
+
const takesValue = new Set([
|
|
93
|
+
'--config-file',
|
|
94
|
+
'--backend-url',
|
|
95
|
+
'--interval',
|
|
96
|
+
'--codex-auth',
|
|
97
|
+
'--sessions-dir',
|
|
98
|
+
'--state-db',
|
|
99
|
+
'--employee-id',
|
|
100
|
+
'--email',
|
|
101
|
+
'--employee-name',
|
|
102
|
+
'--device-id',
|
|
103
|
+
'--hostname',
|
|
104
|
+
'--package-spec',
|
|
105
|
+
'--lines',
|
|
106
|
+
]);
|
|
107
|
+
if (!takesValue.has(key)) {
|
|
108
|
+
throw new Error(`Unknown argument: ${token}`);
|
|
109
|
+
}
|
|
110
|
+
const value = inlineValue ?? argv[++index];
|
|
111
|
+
if (value == null || value.startsWith('-')) {
|
|
112
|
+
throw new Error(`Missing value for ${key}`);
|
|
113
|
+
}
|
|
114
|
+
assignOption(options, key, value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
command: positionals[0] ?? null,
|
|
119
|
+
subcommand: positionals[1] ?? null,
|
|
120
|
+
extraPositionals: positionals.slice(2),
|
|
121
|
+
options,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assignOption(options, key, value) {
|
|
126
|
+
switch (key) {
|
|
127
|
+
case '--config-file':
|
|
128
|
+
options.configFile = value;
|
|
129
|
+
break;
|
|
130
|
+
case '--backend-url':
|
|
131
|
+
options.backendUrl = value;
|
|
132
|
+
break;
|
|
133
|
+
case '--interval':
|
|
134
|
+
options.interval = parsePositiveInt(value, '--interval');
|
|
135
|
+
break;
|
|
136
|
+
case '--codex-auth':
|
|
137
|
+
options.codexAuthPath = value;
|
|
138
|
+
break;
|
|
139
|
+
case '--sessions-dir':
|
|
140
|
+
options.sessionsDir = value;
|
|
141
|
+
break;
|
|
142
|
+
case '--state-db':
|
|
143
|
+
options.stateDbPath = value;
|
|
144
|
+
break;
|
|
145
|
+
case '--employee-id':
|
|
146
|
+
options.employeeId = value;
|
|
147
|
+
break;
|
|
148
|
+
case '--email':
|
|
149
|
+
options.employeeEmail = value;
|
|
150
|
+
break;
|
|
151
|
+
case '--employee-name':
|
|
152
|
+
options.employeeName = value;
|
|
153
|
+
break;
|
|
154
|
+
case '--device-id':
|
|
155
|
+
options.deviceId = value;
|
|
156
|
+
break;
|
|
157
|
+
case '--hostname':
|
|
158
|
+
options.hostname = value;
|
|
159
|
+
break;
|
|
160
|
+
case '--package-spec':
|
|
161
|
+
options.packageSpec = value;
|
|
162
|
+
break;
|
|
163
|
+
case '--lines':
|
|
164
|
+
options.lines = parsePositiveInt(value, '--lines');
|
|
165
|
+
break;
|
|
166
|
+
default:
|
|
167
|
+
throw new Error(`Unknown argument: ${key}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function parsePositiveInt(value, label) {
|
|
172
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
173
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
174
|
+
throw new Error(`${label} must be a positive integer`);
|
|
175
|
+
}
|
|
176
|
+
return parsed;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runtimeOverrides(options) {
|
|
180
|
+
const overrides = {};
|
|
181
|
+
if (options.backendUrl !== undefined) overrides.backendUrl = options.backendUrl;
|
|
182
|
+
if (options.interval !== undefined) overrides.intervalSeconds = options.interval;
|
|
183
|
+
if (options.codexAuthPath !== undefined) overrides.codexAuthPath = options.codexAuthPath;
|
|
184
|
+
if (options.sessionsDir !== undefined) overrides.sessionsDir = options.sessionsDir;
|
|
185
|
+
if (options.stateDbPath !== undefined) overrides.stateDbPath = options.stateDbPath;
|
|
186
|
+
return overrides;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function identityOverrides(options) {
|
|
190
|
+
const overrides = {};
|
|
191
|
+
if (options.employeeId !== undefined) overrides.employeeId = options.employeeId;
|
|
192
|
+
if (options.employeeEmail !== undefined) overrides.employeeEmail = options.employeeEmail;
|
|
193
|
+
if (options.employeeName !== undefined) overrides.employeeName = options.employeeName;
|
|
194
|
+
if (options.deviceId !== undefined) overrides.deviceId = options.deviceId;
|
|
195
|
+
if (options.hostname !== undefined) overrides.hostname = options.hostname;
|
|
196
|
+
return overrides;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function assertBoundIdentity(uploader) {
|
|
200
|
+
if (!identityIsBound(uploader.identity)) {
|
|
201
|
+
throw new CliOperationalError(
|
|
202
|
+
'No employee identity is bound yet. Run `codex-usage-uploader bind` or rerun `init` with --email.',
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function printUsage() {
|
|
208
|
+
console.log(HELP_TEXT);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function validateCommandShape(command, subcommand, extraPositionals) {
|
|
212
|
+
if (subcommand) {
|
|
213
|
+
throw new Error(`Unexpected subcommand for ${command}: ${subcommand}`);
|
|
214
|
+
}
|
|
215
|
+
if (extraPositionals.length > 0) {
|
|
216
|
+
throw new Error(`Unexpected extra arguments: ${extraPositionals.join(' ')}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function printIdentity(identity) {
|
|
221
|
+
console.log('Identity binding:');
|
|
222
|
+
console.log(` collectorId: ${identity.collectorId}`);
|
|
223
|
+
console.log(` employeeEmail: ${identity.employeeEmail ?? '-'}`);
|
|
224
|
+
console.log(` employeeName: ${identity.employeeName ?? '-'}`);
|
|
225
|
+
console.log(` employeeId: ${identity.employeeId ?? '-'}`);
|
|
226
|
+
console.log(` deviceId: ${identity.deviceId ?? '-'}`);
|
|
227
|
+
console.log(` hostname: ${identity.hostname ?? '-'}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function tailFile(filePath, lines) {
|
|
231
|
+
if (!fs.existsSync(filePath)) return [];
|
|
232
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
233
|
+
return content
|
|
234
|
+
.split(/\r?\n/)
|
|
235
|
+
.filter((line, index, array) => !(index === array.length - 1 && line === ''))
|
|
236
|
+
.slice(-lines);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function bindIdentity(uploader, options) {
|
|
240
|
+
const overrides = identityOverrides(options);
|
|
241
|
+
if (options.yes) {
|
|
242
|
+
const identity = uploader.configureIdentityWithDefaults(overrides);
|
|
243
|
+
assertBoundIdentity(uploader);
|
|
244
|
+
return identity;
|
|
245
|
+
}
|
|
246
|
+
const identity = await uploader.configureIdentityInteractive(overrides);
|
|
247
|
+
assertBoundIdentity(uploader);
|
|
248
|
+
return identity;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatDuration(durationMs) {
|
|
252
|
+
const seconds = Math.max(0, Math.round(durationMs / 1000));
|
|
253
|
+
return `${seconds}s`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function printCatchUpProgress(event) {
|
|
257
|
+
switch (event.phase) {
|
|
258
|
+
case 'start':
|
|
259
|
+
console.log(
|
|
260
|
+
`[catch-up] start files=${event.totalFiles} queued_batches=${
|
|
261
|
+
event.bufferingBatchCount +
|
|
262
|
+
event.pendingBatchCount +
|
|
263
|
+
event.retryingBatchCount
|
|
264
|
+
} queued_events=${event.queuedEvents}`,
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
case 'file':
|
|
268
|
+
console.log(
|
|
269
|
+
`[catch-up] file ${event.filesProcessed}/${event.totalFiles} current=${event.file} events=${event.eventsParsed} queued_batches=${
|
|
270
|
+
event.bufferingBatchCount +
|
|
271
|
+
event.pendingBatchCount +
|
|
272
|
+
event.retryingBatchCount
|
|
273
|
+
} queued_events=${event.queuedEvents}`,
|
|
274
|
+
);
|
|
275
|
+
return;
|
|
276
|
+
case 'upload':
|
|
277
|
+
console.log(
|
|
278
|
+
`[catch-up] uploaded_batches=${event.batchesUploaded} last_batch=${event.batchKey} remaining_batches=${
|
|
279
|
+
event.bufferingBatchCount +
|
|
280
|
+
event.pendingBatchCount +
|
|
281
|
+
event.retryingBatchCount
|
|
282
|
+
} remaining_events=${event.queuedEvents}`,
|
|
283
|
+
);
|
|
284
|
+
return;
|
|
285
|
+
case 'done':
|
|
286
|
+
console.log(
|
|
287
|
+
`[catch-up] complete files=${event.filesProcessed}/${event.totalFiles} events=${event.eventsParsed} uploaded_batches=${event.batchesUploaded} remaining_batches=${event.remainingQueuedBatches} remaining_events=${event.remainingQueuedEvents} duration=${formatDuration(event.durationMs)}`,
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
case 'error':
|
|
291
|
+
console.error(
|
|
292
|
+
`[catch-up] failed stage=${event.stage} message=${event.message} remaining_batches=${
|
|
293
|
+
event.bufferingBatchCount +
|
|
294
|
+
event.pendingBatchCount +
|
|
295
|
+
event.retryingBatchCount
|
|
296
|
+
} remaining_events=${event.queuedEvents}`,
|
|
297
|
+
);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function wrapCatchUpFailure(error) {
|
|
303
|
+
return new CliOperationalError(
|
|
304
|
+
`Foreground catch-up failed: ${
|
|
305
|
+
error instanceof Error ? error.message : String(error)
|
|
306
|
+
}`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function runInit(options) {
|
|
311
|
+
let runtime = mergeRuntimeConfig(options.configFile, runtimeOverrides(options));
|
|
312
|
+
if (!runtime.backendUrl) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
'--backend-url is required for init unless already configured in config.json',
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
runtime = cliDeps.installCurrentPackage(runtime, {
|
|
319
|
+
packageRoot: cliDeps.findPackageRoot(),
|
|
320
|
+
packageSpec: options.packageSpec,
|
|
321
|
+
nodePath: process.execPath,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const manager = cliDeps.createServiceManager(runtime);
|
|
325
|
+
manager.stop();
|
|
326
|
+
|
|
327
|
+
const uploader = cliDeps.createUploader(runtime);
|
|
328
|
+
try {
|
|
329
|
+
const identity = await bindIdentity(uploader, options);
|
|
330
|
+
console.log(`${PRODUCT_NAME} install is ready.`);
|
|
331
|
+
console.log(`Backend: ${runtime.backendUrl}`);
|
|
332
|
+
console.log(`Install root: ${runtime.installRoot}`);
|
|
333
|
+
console.log(`Config file: ${runtime.configFile}`);
|
|
334
|
+
printIdentity(identity);
|
|
335
|
+
console.log('Starting foreground historical catch-up.');
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
await uploader.runForegroundCatchUp({
|
|
339
|
+
onProgress: printCatchUpProgress,
|
|
340
|
+
});
|
|
341
|
+
} catch (error) {
|
|
342
|
+
throw wrapCatchUpFailure(error);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
manager.start();
|
|
346
|
+
console.log(`${PRODUCT_NAME} initialized and started.`);
|
|
347
|
+
console.log(`Use \`${CLI_NAME} status\` to check the local service.`);
|
|
348
|
+
} finally {
|
|
349
|
+
uploader.close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function runBind(options) {
|
|
354
|
+
const runtime = mergeRuntimeConfig(options.configFile, runtimeOverrides(options));
|
|
355
|
+
const uploader = cliDeps.createUploader(runtime);
|
|
356
|
+
try {
|
|
357
|
+
const identity = await bindIdentity(uploader, options);
|
|
358
|
+
printIdentity(identity);
|
|
359
|
+
if (await restartRunningServiceIfNeeded(runtime)) {
|
|
360
|
+
console.log(
|
|
361
|
+
'Background service restarted to apply the updated identity.',
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
} finally {
|
|
365
|
+
uploader.close();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function runInternalWorker(options) {
|
|
370
|
+
const runtime = mergeRuntimeConfig(options.configFile, runtimeOverrides(options));
|
|
371
|
+
const uploader = cliDeps.createUploader(runtime);
|
|
372
|
+
try {
|
|
373
|
+
assertBoundIdentity(uploader);
|
|
374
|
+
console.log(
|
|
375
|
+
`[run] backend=${runtime.backendUrl ?? '-'} interval=${runtime.intervalSeconds}s sessions_dir=${runtime.sessionsDir}`,
|
|
376
|
+
);
|
|
377
|
+
await uploader.watch();
|
|
378
|
+
} finally {
|
|
379
|
+
uploader.close();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function loadQueueStats(stateDbPath) {
|
|
384
|
+
if (!fs.existsSync(stateDbPath)) {
|
|
385
|
+
return {
|
|
386
|
+
bufferingBatchCount: 0,
|
|
387
|
+
pendingBatchCount: 0,
|
|
388
|
+
retryingBatchCount: 0,
|
|
389
|
+
queuedSessions: 0,
|
|
390
|
+
queuedTurns: 0,
|
|
391
|
+
queuedEvents: 0,
|
|
392
|
+
oldestPendingAgeSeconds: null,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const stateDb = new StateDb(stateDbPath);
|
|
396
|
+
try {
|
|
397
|
+
return stateDb.getQueueStats();
|
|
398
|
+
} finally {
|
|
399
|
+
stateDb.close();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function printStatus(runtime, status) {
|
|
404
|
+
const payload = {
|
|
405
|
+
...formatStatusOutput(runtime, status),
|
|
406
|
+
...loadQueueStats(runtime.stateDbPath),
|
|
407
|
+
};
|
|
408
|
+
const lines = [
|
|
409
|
+
['Config exists', payload.configExists ? 'yes' : 'no'],
|
|
410
|
+
['Loaded', payload.loaded ? 'yes' : 'no'],
|
|
411
|
+
['Running', payload.running ? 'yes' : 'no'],
|
|
412
|
+
['PID', payload.pid ?? '-'],
|
|
413
|
+
['State', payload.state ?? '-'],
|
|
414
|
+
['Last exit code', payload.lastExitCode ?? '-'],
|
|
415
|
+
['Backend URL', payload.backendUrl ?? '-'],
|
|
416
|
+
['Interval', `${payload.intervalSeconds}s`],
|
|
417
|
+
['Buffering batches', payload.bufferingBatchCount],
|
|
418
|
+
['Pending batches', payload.pendingBatchCount],
|
|
419
|
+
['Retrying batches', payload.retryingBatchCount],
|
|
420
|
+
['Queued sessions', payload.queuedSessions],
|
|
421
|
+
['Queued turns', payload.queuedTurns],
|
|
422
|
+
['Queued events', payload.queuedEvents],
|
|
423
|
+
[
|
|
424
|
+
'Oldest pending age',
|
|
425
|
+
payload.oldestPendingAgeSeconds == null
|
|
426
|
+
? '-'
|
|
427
|
+
: `${payload.oldestPendingAgeSeconds}s`,
|
|
428
|
+
],
|
|
429
|
+
['Config file', payload.configFile],
|
|
430
|
+
['State DB', payload.stateDbPath],
|
|
431
|
+
['Stdout log', payload.stdoutLogPath],
|
|
432
|
+
['Stderr log', payload.stderrLogPath],
|
|
433
|
+
['Plist', payload.plistPath],
|
|
434
|
+
['Label', payload.label],
|
|
435
|
+
];
|
|
436
|
+
for (const [label, value] of lines) {
|
|
437
|
+
console.log(`${label}: ${value}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function ensureInitialized(runtime) {
|
|
442
|
+
if (!fs.existsSync(runtime.configFile) || !runtime.entryFile) {
|
|
443
|
+
throw new CliOperationalError(
|
|
444
|
+
`Uploader is not initialized yet. Run \`${CLI_NAME} init\` first.`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function runClear(options) {
|
|
450
|
+
const runtime = mergeRuntimeConfig(options.configFile, runtimeOverrides(options));
|
|
451
|
+
ensureInitialized(runtime);
|
|
452
|
+
|
|
453
|
+
const manager = cliDeps.createServiceManager(runtime);
|
|
454
|
+
const status = manager.status();
|
|
455
|
+
if (status.running) {
|
|
456
|
+
throw new CliOperationalError(
|
|
457
|
+
`Background service is still running. Stop it first with \`${CLI_NAME} stop\`.`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!options.yes) {
|
|
462
|
+
const confirmed = await promptConfirm(
|
|
463
|
+
'Clear local backfill state and queued uploads?',
|
|
464
|
+
false,
|
|
465
|
+
);
|
|
466
|
+
if (!confirmed) {
|
|
467
|
+
console.log('Clear cancelled.');
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const uploader = cliDeps.createUploader(runtime);
|
|
473
|
+
try {
|
|
474
|
+
uploader.resetBackfillState();
|
|
475
|
+
} finally {
|
|
476
|
+
uploader.close();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
console.log('Local backfill state has been cleared.');
|
|
480
|
+
console.log(
|
|
481
|
+
`Run \`${CLI_NAME} init\` when you want to backfill history again.`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function restartRunningServiceIfNeeded(runtime) {
|
|
486
|
+
if (process.platform !== 'darwin') return false;
|
|
487
|
+
if (!fs.existsSync(runtime.configFile) || !runtime.entryFile) return false;
|
|
488
|
+
const manager = cliDeps.createServiceManager(runtime);
|
|
489
|
+
let status;
|
|
490
|
+
try {
|
|
491
|
+
status = manager.status();
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
if (!status.running) return false;
|
|
496
|
+
manager.restart();
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function runLifecycleCommand(action, options) {
|
|
501
|
+
const runtime = mergeRuntimeConfig(options.configFile, runtimeOverrides(options));
|
|
502
|
+
const manager = cliDeps.createServiceManager(runtime);
|
|
503
|
+
|
|
504
|
+
switch (action) {
|
|
505
|
+
case 'start':
|
|
506
|
+
ensureInitialized(runtime);
|
|
507
|
+
manager.start();
|
|
508
|
+
console.log(`Service started: ${runtime.launchdLabel}`);
|
|
509
|
+
return;
|
|
510
|
+
case 'stop':
|
|
511
|
+
manager.stop();
|
|
512
|
+
console.log(`Service stopped: ${runtime.launchdLabel}`);
|
|
513
|
+
return;
|
|
514
|
+
case 'restart':
|
|
515
|
+
ensureInitialized(runtime);
|
|
516
|
+
manager.restart();
|
|
517
|
+
console.log(`Service restarted: ${runtime.launchdLabel}`);
|
|
518
|
+
return;
|
|
519
|
+
case 'status':
|
|
520
|
+
printStatus(runtime, manager.status());
|
|
521
|
+
return;
|
|
522
|
+
case 'logs': {
|
|
523
|
+
const stdoutLines = tailFile(runtime.stdoutLogPath, options.lines);
|
|
524
|
+
const stderrLines = tailFile(runtime.stderrLogPath, options.lines);
|
|
525
|
+
console.log(`== stdout (${runtime.stdoutLogPath}) ==`);
|
|
526
|
+
console.log(stdoutLines.length ? stdoutLines.join('\n') : '(empty)');
|
|
527
|
+
console.log(`== stderr (${runtime.stderrLogPath}) ==`);
|
|
528
|
+
console.log(stderrLines.length ? stderrLines.join('\n') : '(empty)');
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
case 'uninstall':
|
|
532
|
+
manager.uninstall();
|
|
533
|
+
fs.rmSync(runtime.installRoot, { recursive: true, force: true });
|
|
534
|
+
console.log(`${PRODUCT_NAME} uninstalled from ${runtime.installRoot}`);
|
|
535
|
+
return;
|
|
536
|
+
default:
|
|
537
|
+
throw new Error(`Unknown lifecycle command: ${action ?? '(empty)'}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
542
|
+
try {
|
|
543
|
+
const { command, subcommand, extraPositionals, options } = parseCliArgs(argv);
|
|
544
|
+
if (!command || options.help) {
|
|
545
|
+
printUsage();
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const knownCommands = new Set([
|
|
550
|
+
'init',
|
|
551
|
+
'bind',
|
|
552
|
+
'clear',
|
|
553
|
+
'start',
|
|
554
|
+
'stop',
|
|
555
|
+
'restart',
|
|
556
|
+
'status',
|
|
557
|
+
'logs',
|
|
558
|
+
'uninstall',
|
|
559
|
+
'run',
|
|
560
|
+
]);
|
|
561
|
+
if (!knownCommands.has(command)) {
|
|
562
|
+
throw new Error(`Unknown command: ${command}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
validateCommandShape(command, subcommand, extraPositionals);
|
|
566
|
+
|
|
567
|
+
switch (command) {
|
|
568
|
+
case 'init':
|
|
569
|
+
await runInit(options);
|
|
570
|
+
return 0;
|
|
571
|
+
case 'bind':
|
|
572
|
+
await runBind(options);
|
|
573
|
+
return 0;
|
|
574
|
+
case 'clear':
|
|
575
|
+
await runClear(options);
|
|
576
|
+
return 0;
|
|
577
|
+
case 'run':
|
|
578
|
+
await runInternalWorker(options);
|
|
579
|
+
return 0;
|
|
580
|
+
case 'start':
|
|
581
|
+
case 'stop':
|
|
582
|
+
case 'restart':
|
|
583
|
+
case 'status':
|
|
584
|
+
case 'logs':
|
|
585
|
+
case 'uninstall':
|
|
586
|
+
runLifecycleCommand(command, options);
|
|
587
|
+
return 0;
|
|
588
|
+
}
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
591
|
+
if (error?.showUsage !== false) {
|
|
592
|
+
console.error(`Run \`${CLI_NAME} --help\` for usage.`);
|
|
593
|
+
}
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
596
|
+
}
|