health-sync 0.2.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 ADDED
@@ -0,0 +1,30 @@
1
+ # health-sync Node Port
2
+
3
+ This directory contains a Node.js implementation of `health-sync` with parity-oriented features:
4
+
5
+ - CLI commands: `init`, `init-db`, `auth`, `sync`, `providers`, `status`
6
+ - SQLite storage (`records`, `sync_state`, `oauth_tokens`, `sync_runs`)
7
+ - Built-in providers: Oura, Withings, Hevy, Strava, Eight Sleep
8
+ - Plugin loading from package metadata (`healthSyncProviders`) and `[plugins.<id>] module=...`
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ cd node
14
+ npm install
15
+ ```
16
+
17
+ ## Run
18
+
19
+ ```bash
20
+ npm start -- --config ../health-sync.toml providers --verbose
21
+ npm start -- --config ../health-sync.toml status
22
+ ```
23
+
24
+ ## CLI
25
+
26
+ ```bash
27
+ health-sync [--config path] [--db path] <command> [options]
28
+ ```
29
+
30
+ Use the same `health-sync.toml` format as the Python implementation.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ main(process.argv.slice(2)).then(
5
+ (code) => {
6
+ process.exitCode = code ?? 0;
7
+ },
8
+ (err) => {
9
+ const message = err?.stack || err?.message || String(err);
10
+ console.error(message);
11
+ process.exitCode = 1;
12
+ },
13
+ );
@@ -0,0 +1,120 @@
1
+ # health-sync configuration (example)
2
+ #
3
+ # Copy this to `health-sync.toml` and fill in the values you need.
4
+ # This file is meant to be safe to commit; secrets are commented out.
5
+ #
6
+ # Notes:
7
+ # - You can run with a specific config path via `health-sync --config /path/to/health-sync.toml ...`
8
+ # - Any values you leave commented will fall back to built-in defaults.
9
+ #
10
+
11
+ [app]
12
+ # SQLite DB path.
13
+ # db = "./health.sqlite"
14
+
15
+ [oura]
16
+ # Set to true to sync this provider.
17
+ enabled = false
18
+
19
+ # OAuth2 (Authorization Code)
20
+ # client_id = "..."
21
+ # client_secret = "..."
22
+ # redirect_uri = "http://localhost:8080/callback"
23
+ #
24
+ # Oura OAuth endpoints (defaults point to current issuer-backed endpoints).
25
+ # authorize_url = "https://moi.ouraring.com/oauth/v2/ext/oauth-authorize"
26
+ # token_url = "https://moi.ouraring.com/oauth/v2/ext/oauth-token"
27
+ #
28
+ # Keep it broad; adjust if your Oura app is configured with different scopes.
29
+ # scopes = "extapi:daily extapi:heartrate extapi:personal extapi:workout extapi:session extapi:tag extapi:spo2"
30
+ #
31
+ # Sync tuning:
32
+ # start_date = "2010-01-01" # YYYY-MM-DD
33
+ # overlap_days = 7
34
+
35
+ [withings]
36
+ # Set to true to sync this provider.
37
+ enabled = false
38
+
39
+ # OAuth2
40
+ # client_id = "..."
41
+ # client_secret = "..."
42
+ # redirect_uri = "http://127.0.0.1:8485/callback"
43
+ # scopes = "user.metrics,user.activity"
44
+ #
45
+ # Sync tuning:
46
+ # overlap_seconds = 300
47
+ #
48
+ # Optional list of measure type ids to sync.
49
+ # If omitted, a broad default list is used.
50
+ # meastypes = ["1", "4", "5"]
51
+
52
+ [hevy]
53
+ # Set to true to sync this provider.
54
+ enabled = false
55
+
56
+ # Hevy API key is Pro-only; get it from https://hevy.com/settings?developer
57
+ # api_key = "00000000-0000-0000-0000-000000000000"
58
+ #
59
+ # base_url = "https://api.hevyapp.com"
60
+ #
61
+ # Sync tuning:
62
+ # overlap_seconds = 300
63
+ # page_size = 10 # 1-10
64
+ # since = "1970-01-01T00:00:00Z"
65
+
66
+ [strava]
67
+ # Set to true to sync this provider.
68
+ enabled = false
69
+
70
+ # Option A: static access token (advanced; usually short-lived)
71
+ # access_token = "..."
72
+ #
73
+ # Option B: OAuth2 (recommended)
74
+ # client_id = "..."
75
+ # client_secret = "..."
76
+ # redirect_uri = "http://127.0.0.1:8486/callback"
77
+ # scopes = "read,activity:read_all"
78
+ # approval_prompt = "auto"
79
+ #
80
+ # Sync tuning:
81
+ # start_date = "2010-01-01" # YYYY-MM-DD
82
+ # overlap_seconds = 604800 # 7 days
83
+ # page_size = 100 # 1-200
84
+
85
+ [eightsleep]
86
+ # Set to true to sync this provider.
87
+ enabled = false
88
+
89
+ # Option A: static bearer token (advanced/unstable)
90
+ # access_token = "..."
91
+ #
92
+ # Option B: username/password grant (recommended)
93
+ # email = "you@example.com"
94
+ # password = "..."
95
+ #
96
+ # Client credentials used by the official Eight Sleep app.
97
+ # You can keep these defaults unless Eight Sleep changes them.
98
+ client_id = "0894c7f33bb94800a03f1f4df13a4f38"
99
+ client_secret = "f0954a3ed5763ba3d06834c73731a32f15f168f47d4f164751275def86db0c76"
100
+ #
101
+ # Optional overrides for API hosts:
102
+ # auth_url = "https://auth-api.8slp.net/v1/tokens"
103
+ # client_api_url = "https://client-api.8slp.net/v1"
104
+ #
105
+ # Sync tuning:
106
+ # timezone = "UTC"
107
+ # start_date = "2010-01-01" # YYYY-MM-DD
108
+ # overlap_days = 2
109
+
110
+ # Optional external plugins (in-process)
111
+ #
112
+ # Plugin code must be discoverable either via Python entry points
113
+ # (`health_sync.providers`) or explicit module path here.
114
+ #
115
+ # [plugins.garmin]
116
+ # enabled = true
117
+ # module = "health_sync_garmin.plugin:provider"
118
+ # client_id = "..."
119
+ # client_secret = "..."
120
+ # redirect_uri = "http://localhost:8487/callback"
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "health-sync",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Node.js port of health-sync",
6
+ "files": [
7
+ "bin",
8
+ "src",
9
+ "README.md",
10
+ "health-sync.example.toml"
11
+ ],
12
+ "bin": {
13
+ "health-sync": "bin/health-sync.js"
14
+ },
15
+ "scripts": {
16
+ "start": "node ./bin/health-sync.js",
17
+ "check": "node --check ./bin/health-sync.js && node --check ./src/cli.js",
18
+ "test": "node --test ./tests/*.test.js"
19
+ },
20
+ "dependencies": {
21
+ "@iarna/toml": "^2.2.5",
22
+ "better-sqlite3": "^11.9.1"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ }
27
+ }
package/src/cli.js ADDED
@@ -0,0 +1,493 @@
1
+ import path from 'node:path';
2
+ import {
3
+ initConfigFile,
4
+ loadConfig,
5
+ scaffoldProviderConfig,
6
+ } from './config.js';
7
+ import { openDb } from './db.js';
8
+ import { PluginHelpers, providerEnabled } from './plugins/base.js';
9
+ import { loadProviders } from './plugins/loader.js';
10
+
11
+ const DEFAULT_CONFIG_PATH = 'health-sync.toml';
12
+
13
+ function parseGlobalOptions(argv) {
14
+ let configPath = DEFAULT_CONFIG_PATH;
15
+ let dbPath = null;
16
+ const remaining = [];
17
+
18
+ for (let i = 0; i < argv.length; i += 1) {
19
+ const arg = argv[i];
20
+ if (arg === '--config') {
21
+ i += 1;
22
+ if (i >= argv.length) {
23
+ throw new Error('--config requires a value');
24
+ }
25
+ configPath = argv[i];
26
+ continue;
27
+ }
28
+ if (arg.startsWith('--config=')) {
29
+ configPath = arg.slice('--config='.length);
30
+ continue;
31
+ }
32
+ if (arg === '--db') {
33
+ i += 1;
34
+ if (i >= argv.length) {
35
+ throw new Error('--db requires a value');
36
+ }
37
+ dbPath = argv[i];
38
+ continue;
39
+ }
40
+ if (arg.startsWith('--db=')) {
41
+ dbPath = arg.slice('--db='.length);
42
+ continue;
43
+ }
44
+ remaining.push(arg);
45
+ }
46
+
47
+ return { configPath, dbPath, remaining };
48
+ }
49
+
50
+ function parseAuthArgs(args) {
51
+ const out = {
52
+ provider: null,
53
+ listenHost: '127.0.0.1',
54
+ listenPort: 0,
55
+ };
56
+
57
+ for (let i = 0; i < args.length; i += 1) {
58
+ const arg = args[i];
59
+ if (arg === '--listen-host') {
60
+ i += 1;
61
+ if (i >= args.length) {
62
+ throw new Error('--listen-host requires a value');
63
+ }
64
+ out.listenHost = args[i];
65
+ continue;
66
+ }
67
+ if (arg.startsWith('--listen-host=')) {
68
+ out.listenHost = arg.slice('--listen-host='.length);
69
+ continue;
70
+ }
71
+ if (arg === '--listen-port') {
72
+ i += 1;
73
+ if (i >= args.length) {
74
+ throw new Error('--listen-port requires a value');
75
+ }
76
+ out.listenPort = Number.parseInt(args[i], 10) || 0;
77
+ continue;
78
+ }
79
+ if (arg.startsWith('--listen-port=')) {
80
+ out.listenPort = Number.parseInt(arg.slice('--listen-port='.length), 10) || 0;
81
+ continue;
82
+ }
83
+ if (arg.startsWith('--')) {
84
+ throw new Error(`Unknown auth option: ${arg}`);
85
+ }
86
+ if (!out.provider) {
87
+ out.provider = arg;
88
+ continue;
89
+ }
90
+ throw new Error(`Unexpected auth argument: ${arg}`);
91
+ }
92
+
93
+ if (!out.provider) {
94
+ throw new Error('auth requires PROVIDER argument');
95
+ }
96
+
97
+ return out;
98
+ }
99
+
100
+ function parseSyncArgs(args) {
101
+ const providers = [];
102
+
103
+ for (let i = 0; i < args.length; i += 1) {
104
+ const arg = args[i];
105
+ if (arg === '--providers') {
106
+ let consumed = false;
107
+ while (i + 1 < args.length && !args[i + 1].startsWith('-')) {
108
+ consumed = true;
109
+ const raw = args[i + 1];
110
+ for (const piece of String(raw).split(',').map((v) => v.trim()).filter(Boolean)) {
111
+ providers.push(piece);
112
+ }
113
+ i += 1;
114
+ }
115
+ if (!consumed) {
116
+ throw new Error('--providers requires one or more provider ids');
117
+ }
118
+ continue;
119
+ }
120
+ if (arg.startsWith('--providers=')) {
121
+ const raw = arg.slice('--providers='.length);
122
+ for (const piece of raw.split(',').map((v) => v.trim()).filter(Boolean)) {
123
+ providers.push(piece);
124
+ }
125
+ continue;
126
+ }
127
+ throw new Error(`Unknown sync option: ${arg}`);
128
+ }
129
+
130
+ return {
131
+ providers,
132
+ };
133
+ }
134
+
135
+ function parseProvidersArgs(args) {
136
+ const out = { verbose: false };
137
+ for (const arg of args) {
138
+ if (arg === '--verbose') {
139
+ out.verbose = true;
140
+ continue;
141
+ }
142
+ throw new Error(`Unknown providers option: ${arg}`);
143
+ }
144
+ return out;
145
+ }
146
+
147
+ function parseArgs(argv) {
148
+ const global = parseGlobalOptions(argv);
149
+ const [command, ...rest] = global.remaining;
150
+
151
+ if (!command) {
152
+ return {
153
+ command: null,
154
+ configPath: global.configPath,
155
+ dbPath: global.dbPath,
156
+ options: {},
157
+ };
158
+ }
159
+
160
+ const parsed = {
161
+ command,
162
+ configPath: global.configPath,
163
+ dbPath: global.dbPath,
164
+ options: {},
165
+ };
166
+
167
+ if (command === 'auth') {
168
+ parsed.options = parseAuthArgs(rest);
169
+ return parsed;
170
+ }
171
+ if (command === 'sync') {
172
+ parsed.options = parseSyncArgs(rest);
173
+ return parsed;
174
+ }
175
+ if (command === 'providers') {
176
+ parsed.options = parseProvidersArgs(rest);
177
+ return parsed;
178
+ }
179
+ if (command === 'init' || command === 'init-db' || command === 'status') {
180
+ if (rest.length) {
181
+ throw new Error(`Unexpected ${command} arguments: ${rest.join(' ')}`);
182
+ }
183
+ return parsed;
184
+ }
185
+
186
+ throw new Error(`Unknown command: ${command}`);
187
+ }
188
+
189
+ function resolveDbPath(overrideDbPath, loadedConfig) {
190
+ if (overrideDbPath) {
191
+ return overrideDbPath;
192
+ }
193
+ if (loadedConfig?.data?.app?.db) {
194
+ return loadedConfig.data.app.db;
195
+ }
196
+ return './health.sqlite';
197
+ }
198
+
199
+ function enableHint(providerId) {
200
+ const builtin = new Set(['oura', 'withings', 'hevy', 'strava', 'eightsleep']);
201
+ if (builtin.has(providerId)) {
202
+ return `[${providerId}].enabled = true`;
203
+ }
204
+ return `[plugins.${providerId}].enabled = true`;
205
+ }
206
+
207
+ function usage() {
208
+ return [
209
+ 'Usage: health-sync [--config path] [--db path] <command> [options]',
210
+ '',
211
+ 'Commands:',
212
+ ' init Initialize config file and database',
213
+ ' init-db Initialize database only',
214
+ ' auth <provider> Run provider authentication flow',
215
+ ' --listen-host <host> OAuth callback listen host (default 127.0.0.1)',
216
+ ' --listen-port <port> OAuth callback listen port (default 0 -> config redirect port)',
217
+ ' sync [--providers a,b,c] Sync enabled providers',
218
+ ' providers [--verbose] List discovered providers',
219
+ ' status Show sync state, counts, and recent runs',
220
+ ].join('\n');
221
+ }
222
+
223
+ async function loadContext(configPath) {
224
+ const loadedConfig = loadConfig(configPath);
225
+ const helpers = new PluginHelpers(loadedConfig.data);
226
+ const { providers, metadata } = await loadProviders(loadedConfig.data, {
227
+ cwd: path.dirname(path.resolve(configPath)),
228
+ });
229
+ return {
230
+ loadedConfig,
231
+ helpers,
232
+ providers,
233
+ metadata,
234
+ };
235
+ }
236
+
237
+ async function cmdInit(parsed) {
238
+ const configPath = path.resolve(parsed.configPath);
239
+ const loaded = loadConfig(configPath);
240
+ const dbPath = resolveDbPath(parsed.dbPath, loaded);
241
+
242
+ initConfigFile(configPath, dbPath);
243
+ const db = openDb(dbPath);
244
+ db.close();
245
+
246
+ console.log(`Initialized config: ${configPath}`);
247
+ console.log(`Initialized database: ${path.resolve(dbPath)}`);
248
+ return 0;
249
+ }
250
+
251
+ async function cmdInitDb(parsed) {
252
+ const configPath = path.resolve(parsed.configPath);
253
+ const loaded = loadConfig(configPath);
254
+ const dbPath = resolveDbPath(parsed.dbPath, loaded);
255
+
256
+ const db = openDb(dbPath);
257
+ db.close();
258
+ console.log(`Initialized database: ${path.resolve(dbPath)}`);
259
+ return 0;
260
+ }
261
+
262
+ async function cmdAuth(parsed) {
263
+ const configPath = path.resolve(parsed.configPath);
264
+ const loaded = loadConfig(configPath);
265
+ const dbPath = resolveDbPath(parsed.dbPath, loaded);
266
+ initConfigFile(configPath, dbPath);
267
+
268
+ scaffoldProviderConfig(configPath, parsed.options.provider);
269
+
270
+ const context = await loadContext(configPath);
271
+ const db = openDb(dbPath);
272
+
273
+ try {
274
+ const plugin = context.providers.get(parsed.options.provider);
275
+ if (!plugin) {
276
+ const known = context.providers.size
277
+ ? Array.from(context.providers.keys()).sort().join(', ')
278
+ : '(none)';
279
+ throw new Error(
280
+ `Unknown provider \`${parsed.options.provider}\`. `
281
+ + `Available providers: ${known}. `
282
+ + 'Use `health-sync providers` to inspect discovery/config status.',
283
+ );
284
+ }
285
+ if (!plugin.supportsAuth) {
286
+ throw new Error(`Provider \`${parsed.options.provider}\` does not support auth.`);
287
+ }
288
+
289
+ await plugin.auth(db, context.loadedConfig.data, context.helpers, {
290
+ listenHost: parsed.options.listenHost,
291
+ listenPort: parsed.options.listenPort,
292
+ configPath,
293
+ dbPath,
294
+ });
295
+
296
+ console.log(`Auth finished for provider ${parsed.options.provider}.`);
297
+ return 0;
298
+ } finally {
299
+ db.close();
300
+ }
301
+ }
302
+
303
+ async function cmdSync(parsed) {
304
+ const configPath = path.resolve(parsed.configPath);
305
+ const context = await loadContext(configPath);
306
+ const dbPath = resolveDbPath(parsed.dbPath, context.loadedConfig);
307
+ const db = openDb(dbPath);
308
+
309
+ try {
310
+ const discoveredIds = Array.from(context.providers.keys()).sort();
311
+ const requested = parsed.options.providers.length ? parsed.options.providers : discoveredIds;
312
+ if (parsed.options.providers.length) {
313
+ const unknown = requested.filter((id) => !context.providers.has(id));
314
+ if (unknown.length) {
315
+ const known = discoveredIds.length ? discoveredIds.join(', ') : '(none)';
316
+ throw new Error(
317
+ `Unknown provider(s): ${unknown.join(', ')}. `
318
+ + `Available providers: ${known}. `
319
+ + 'Use `health-sync providers` to inspect discovery/config status.',
320
+ );
321
+ }
322
+ }
323
+
324
+ const enabledConfiguredPluginIds = Object.entries(context.loadedConfig.data.plugins || {})
325
+ .filter(([, section]) => Boolean(section?.enabled))
326
+ .map(([id]) => id)
327
+ .filter((id) => !context.providers.has(id));
328
+ for (const missingId of enabledConfiguredPluginIds) {
329
+ console.warn(`WARNING: [plugins.${missingId}] is enabled but provider code was not discovered.`);
330
+ }
331
+
332
+ const toSync = requested.filter((id) => providerEnabled(context.loadedConfig.data, id));
333
+ const skipped = requested.filter((id) => !providerEnabled(context.loadedConfig.data, id));
334
+ for (const providerId of skipped) {
335
+ console.log(`Skipping ${providerId}: disabled in config (set ${enableHint(providerId)}).`);
336
+ }
337
+
338
+ if (!toSync.length) {
339
+ if (!parsed.options.providers.length) {
340
+ console.log(
341
+ 'No providers enabled; nothing to sync. '
342
+ + `Enable one or more providers in ${context.loadedConfig.path} `
343
+ + `(e.g. set ${enableHint('hevy')}).`,
344
+ );
345
+ } else if (requested.length) {
346
+ console.log('No enabled providers selected; nothing to sync.');
347
+ } else {
348
+ console.log('No providers specified; nothing to sync.');
349
+ }
350
+ return 0;
351
+ }
352
+
353
+ let successes = 0;
354
+ const failures = [];
355
+ for (const providerId of toSync) {
356
+ try {
357
+ await context.providers.get(providerId).sync(db, context.loadedConfig.data, context.helpers, {
358
+ configPath,
359
+ dbPath,
360
+ });
361
+ successes += 1;
362
+ } catch (err) {
363
+ failures.push(providerId);
364
+ console.warn(`WARNING: ${providerId} sync failed: ${err?.message || String(err)}`);
365
+ }
366
+ }
367
+
368
+ if (failures.length) {
369
+ console.warn(
370
+ `Sync completed with warnings (${failures.length}/${toSync.length} providers failed): `
371
+ + failures.join(', '),
372
+ );
373
+ if (successes === 0) {
374
+ console.warn('All selected providers failed.');
375
+ }
376
+ return 1;
377
+ }
378
+
379
+ return 0;
380
+ } finally {
381
+ db.close();
382
+ }
383
+ }
384
+
385
+ async function cmdProviders(parsed) {
386
+ const configPath = path.resolve(parsed.configPath);
387
+ const context = await loadContext(configPath);
388
+
389
+ const ids = Array.from(context.providers.keys()).sort();
390
+ for (const id of ids) {
391
+ const plugin = context.providers.get(id);
392
+ const meta = context.metadata.get(id) || {};
393
+ const enabled = providerEnabled(context.loadedConfig.data, id);
394
+ const auth = plugin.supportsAuth ? 'yes' : 'no';
395
+
396
+ console.log(`${id}\tenabled=${enabled ? 'yes' : 'no'}\tauth=${auth}\tsource=${plugin.source}`);
397
+ if (parsed.options.verbose && meta.moduleSpec) {
398
+ console.log(` module=${meta.moduleSpec}`);
399
+ }
400
+ if (parsed.options.verbose && plugin.description) {
401
+ console.log(` description=${plugin.description}`);
402
+ }
403
+ }
404
+
405
+ return 0;
406
+ }
407
+
408
+ async function cmdStatus(parsed) {
409
+ const configPath = path.resolve(parsed.configPath);
410
+ const loadedConfig = loadConfig(configPath);
411
+ const dbPath = resolveDbPath(parsed.dbPath, loadedConfig);
412
+ const db = openDb(dbPath);
413
+
414
+ try {
415
+ const syncState = db.listSyncState();
416
+ const recordCounts = db.listRecordCounts();
417
+ const runs = db.listRecentSyncRuns(20);
418
+
419
+ console.log('Sync State:');
420
+ if (!syncState.length) {
421
+ console.log(' (none)');
422
+ }
423
+ for (const row of syncState) {
424
+ console.log(` ${row.provider}/${row.resource} watermark=${row.watermark || '-'} updated_at=${row.updatedAt || '-'}`);
425
+ }
426
+
427
+ console.log('');
428
+ console.log('Record Counts:');
429
+ if (!recordCounts.length) {
430
+ console.log(' (none)');
431
+ }
432
+ for (const row of recordCounts) {
433
+ console.log(` ${row.provider}/${row.resource}: ${row.count}`);
434
+ }
435
+
436
+ console.log('');
437
+ console.log('Recent Sync Runs:');
438
+ if (!runs.length) {
439
+ console.log(' (none)');
440
+ }
441
+ for (const run of runs) {
442
+ const counts = `i=${run.insertedCount} u=${run.updatedCount} d=${run.deletedCount} n=${run.unchangedCount}`;
443
+ const wm = `wm=${run.watermarkBefore || '-'} -> ${run.watermarkAfter || '-'}`;
444
+ console.log(` #${run.id} ${run.startedAt} ${run.provider}/${run.resource} status=${run.status} ${counts} ${wm}`);
445
+ if (run.errorText) {
446
+ console.log(` error=${run.errorText.split('\n')[0]}`);
447
+ }
448
+ }
449
+
450
+ return 0;
451
+ } finally {
452
+ db.close();
453
+ }
454
+ }
455
+
456
+ export async function main(argv = process.argv.slice(2)) {
457
+ try {
458
+ const parsed = parseArgs(argv);
459
+ if (!parsed.command) {
460
+ console.log(usage());
461
+ return 1;
462
+ }
463
+
464
+ if (parsed.command === 'init') {
465
+ return await cmdInit(parsed);
466
+ }
467
+ if (parsed.command === 'init-db') {
468
+ return await cmdInitDb(parsed);
469
+ }
470
+ if (parsed.command === 'auth') {
471
+ return await cmdAuth(parsed);
472
+ }
473
+ if (parsed.command === 'sync') {
474
+ return await cmdSync(parsed);
475
+ }
476
+ if (parsed.command === 'providers') {
477
+ return await cmdProviders(parsed);
478
+ }
479
+ if (parsed.command === 'status') {
480
+ return await cmdStatus(parsed);
481
+ }
482
+
483
+ console.error(`Unknown command: ${parsed.command}`);
484
+ return 1;
485
+ } catch (err) {
486
+ console.error(err?.message || String(err));
487
+ return 1;
488
+ }
489
+ }
490
+
491
+ export {
492
+ parseArgs,
493
+ };