kova-node-cli 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.
@@ -0,0 +1,568 @@
1
+ // deployment management commands for customers
2
+ // create, list, get, update, close, pause, resume, deposit, logs, events, versions
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { getAuthToken, authFetch, formatDate, formatTable, handleApiError } from '../lib/client.js';
5
+ export function registerDeployCommands(program) {
6
+ const deploy = program
7
+ .command('deploy')
8
+ .description('manage deployments on the kova network');
9
+ // create deployment from sdl file
10
+ deploy
11
+ .command('create <sdl-file>')
12
+ .description('create a deployment from an SDL file')
13
+ .option('-d, --deposit <amount>', 'initial deposit amount (USD)', parseFloat)
14
+ .option('--backup', 'enable backup for this deployment')
15
+ .action(async (sdlFile, options) => {
16
+ const token = getAuthToken();
17
+ if (!token) {
18
+ console.error('\nnot logged in. run "kova auth login" first.');
19
+ process.exit(1);
20
+ }
21
+ if (!existsSync(sdlFile)) {
22
+ console.error(`\nfile not found: ${sdlFile}`);
23
+ process.exit(1);
24
+ }
25
+ let sdl;
26
+ try {
27
+ sdl = readFileSync(sdlFile, 'utf8');
28
+ }
29
+ catch (err) {
30
+ console.error(`\nfailed to read file: ${err.message}`);
31
+ process.exit(1);
32
+ }
33
+ if (!sdl.trim()) {
34
+ console.error('\nsdl file is empty');
35
+ process.exit(1);
36
+ }
37
+ console.log('creating deployment...');
38
+ try {
39
+ const body = { sdl };
40
+ if (options.deposit)
41
+ body.initialDeposit = options.deposit;
42
+ if (options.backup)
43
+ body.backupEnabled = true;
44
+ const res = await authFetch('/api/v1/deployments', {
45
+ method: 'POST',
46
+ body: JSON.stringify(body)
47
+ });
48
+ const data = await res.json();
49
+ if (!res.ok) {
50
+ console.error(`\nfailed to create deployment: ${data.error || data.message || 'unknown error'}`);
51
+ if (data.details)
52
+ console.error('details:', JSON.stringify(data.details, null, 2));
53
+ process.exit(1);
54
+ }
55
+ const d = data.deployment;
56
+ console.log('\ndeployment created successfully');
57
+ console.log('========================================');
58
+ console.log(`id: ${d.id}`);
59
+ console.log(`version: ${d.version}`);
60
+ console.log(`state: ${d.state}`);
61
+ console.log(`deposit: $${(d.initialDeposit || 0).toFixed(2)}`);
62
+ console.log(`created: ${formatDate(d.createdAt)}`);
63
+ console.log('========================================');
64
+ console.log('\nwaiting for provider bids...');
65
+ console.log(`run "kova deploy get ${d.id}" to check status`);
66
+ }
67
+ catch (err) {
68
+ handleApiError(err);
69
+ }
70
+ });
71
+ // list deployments
72
+ deploy
73
+ .command('list')
74
+ .alias('ls')
75
+ .description('list your deployments')
76
+ .option('-s, --state <state>', 'filter by state (active, paused, closed)')
77
+ .option('-l, --limit <n>', 'max results', '20')
78
+ .option('-o, --offset <n>', 'offset for pagination', '0')
79
+ .action(async (options) => {
80
+ const token = getAuthToken();
81
+ if (!token) {
82
+ console.error('\nnot logged in. run "kova auth login" first.');
83
+ process.exit(1);
84
+ }
85
+ try {
86
+ const params = new URLSearchParams();
87
+ if (options.state)
88
+ params.set('state', options.state);
89
+ params.set('limit', options.limit);
90
+ params.set('offset', options.offset);
91
+ const res = await authFetch(`/api/v1/deployments?${params.toString()}`);
92
+ const data = await res.json();
93
+ if (!res.ok) {
94
+ console.error(`\nfailed to list deployments: ${data.error || 'unknown error'}`);
95
+ process.exit(1);
96
+ }
97
+ if (data.deployments.length === 0) {
98
+ console.log('\nno deployments found');
99
+ if (options.state)
100
+ console.log(`(filtered by state: ${options.state})`);
101
+ return;
102
+ }
103
+ console.log('');
104
+ const rows = data.deployments.map((d) => ({
105
+ ID: d.id.substring(0, 12) + '...',
106
+ State: d.state,
107
+ Version: `v${d.version}`,
108
+ Created: formatDate(d.createdAt),
109
+ Deposit: `$${(d.initialDeposit || 0).toFixed(2)}`
110
+ }));
111
+ formatTable(rows);
112
+ const p = data.pagination;
113
+ if (p && p.total > rows.length) {
114
+ console.log(`\nshowing ${rows.length} of ${p.total} (page ${Math.floor(p.offset / p.limit) + 1}/${p.pages})`);
115
+ }
116
+ }
117
+ catch (err) {
118
+ handleApiError(err);
119
+ }
120
+ });
121
+ // get deployment details
122
+ deploy
123
+ .command('get <id>')
124
+ .description('get deployment details')
125
+ .action(async (id) => {
126
+ const token = getAuthToken();
127
+ if (!token) {
128
+ console.error('\nnot logged in. run "kova auth login" first.');
129
+ process.exit(1);
130
+ }
131
+ try {
132
+ const res = await authFetch(`/api/v1/deployments/${id}`);
133
+ const data = await res.json();
134
+ if (!res.ok) {
135
+ console.error(`\nfailed to get deployment: ${data.error || 'unknown error'}`);
136
+ process.exit(1);
137
+ }
138
+ const d = data.deployment;
139
+ console.log('\ndeployment details');
140
+ console.log('========================================');
141
+ console.log(`id: ${d.id}`);
142
+ console.log(`state: ${d.state}`);
143
+ console.log(`version: v${d.version}`);
144
+ console.log(`escrow balance: $${(d.escrowBalance || 0).toFixed(4)}`);
145
+ console.log(`initial deposit:$${(d.initialDeposit || 0).toFixed(2)}`);
146
+ console.log(`created: ${formatDate(d.createdAt)}`);
147
+ console.log(`updated: ${formatDate(d.updatedAt)}`);
148
+ if (d.closedAt)
149
+ console.log(`closed: ${formatDate(d.closedAt)}`);
150
+ // orders
151
+ if (data.orders && data.orders.length > 0) {
152
+ console.log('\norders:');
153
+ for (const o of data.orders) {
154
+ console.log(` ${o.id.substring(0, 12)}... | state: ${o.state} | max price: $${o.maxPricePerBlock}/block`);
155
+ }
156
+ }
157
+ // lease
158
+ if (data.lease) {
159
+ const l = data.lease;
160
+ console.log('\nlease:');
161
+ console.log(` id: ${l.id}`);
162
+ console.log(` provider: ${l.providerId}`);
163
+ console.log(` node: ${l.nodeId}`);
164
+ console.log(` price: $${l.pricePerBlock}/block`);
165
+ console.log(` total paid: $${(l.totalPaid || 0).toFixed(4)}`);
166
+ console.log(` state: ${l.state}`);
167
+ }
168
+ else {
169
+ console.log('\nlease: none (waiting for bids)');
170
+ }
171
+ console.log('========================================');
172
+ }
173
+ catch (err) {
174
+ handleApiError(err);
175
+ }
176
+ });
177
+ // update deployment sdl
178
+ deploy
179
+ .command('update <id> <sdl-file>')
180
+ .description('update deployment manifest from SDL file')
181
+ .action(async (id, sdlFile) => {
182
+ const token = getAuthToken();
183
+ if (!token) {
184
+ console.error('\nnot logged in. run "kova auth login" first.');
185
+ process.exit(1);
186
+ }
187
+ if (!existsSync(sdlFile)) {
188
+ console.error(`\nfile not found: ${sdlFile}`);
189
+ process.exit(1);
190
+ }
191
+ let sdl;
192
+ try {
193
+ sdl = readFileSync(sdlFile, 'utf8');
194
+ }
195
+ catch (err) {
196
+ console.error(`\nfailed to read file: ${err.message}`);
197
+ process.exit(1);
198
+ }
199
+ console.log('updating deployment...');
200
+ try {
201
+ const res = await authFetch(`/api/v1/deployments/${id}`, {
202
+ method: 'PUT',
203
+ body: JSON.stringify({ sdl })
204
+ });
205
+ const data = await res.json();
206
+ if (!res.ok) {
207
+ console.error(`\nupdate failed: ${data.error || data.message || 'unknown error'}`);
208
+ process.exit(1);
209
+ }
210
+ const d = data.deployment;
211
+ console.log('\ndeployment updated');
212
+ console.log(` id: ${d.id}`);
213
+ console.log(` version: v${d.version}`);
214
+ console.log(` state: ${d.state}`);
215
+ }
216
+ catch (err) {
217
+ handleApiError(err);
218
+ }
219
+ });
220
+ // close deployment
221
+ deploy
222
+ .command('close <id>')
223
+ .description('close a deployment')
224
+ .option('-y, --yes', 'skip confirmation')
225
+ .action(async (id, options) => {
226
+ const token = getAuthToken();
227
+ if (!token) {
228
+ console.error('\nnot logged in. run "kova auth login" first.');
229
+ process.exit(1);
230
+ }
231
+ if (!options.yes) {
232
+ // quick confirmation via stdin
233
+ const confirmed = await promptConfirm(`close deployment ${id.substring(0, 12)}...? (y/n) `);
234
+ if (!confirmed) {
235
+ console.log('cancelled');
236
+ return;
237
+ }
238
+ }
239
+ try {
240
+ const res = await authFetch(`/api/v1/deployments/${id}`, {
241
+ method: 'DELETE'
242
+ });
243
+ const data = await res.json();
244
+ if (!res.ok) {
245
+ console.error(`\nclose failed: ${data.error || data.message || 'unknown error'}`);
246
+ process.exit(1);
247
+ }
248
+ console.log('\ndeployment closed successfully');
249
+ console.log('any remaining escrow balance will be returned to your account');
250
+ }
251
+ catch (err) {
252
+ handleApiError(err);
253
+ }
254
+ });
255
+ // pause deployment
256
+ deploy
257
+ .command('pause <id>')
258
+ .description('pause a running deployment')
259
+ .action(async (id) => {
260
+ const token = getAuthToken();
261
+ if (!token) {
262
+ console.error('\nnot logged in. run "kova auth login" first.');
263
+ process.exit(1);
264
+ }
265
+ try {
266
+ const res = await authFetch(`/api/v1/deployments/${id}/pause`, {
267
+ method: 'POST'
268
+ });
269
+ const data = await res.json();
270
+ if (!res.ok) {
271
+ console.error(`\npause failed: ${data.error || 'unknown error'}`);
272
+ process.exit(1);
273
+ }
274
+ console.log('\ndeployment paused');
275
+ console.log(`use "kova deploy resume ${id}" to resume`);
276
+ }
277
+ catch (err) {
278
+ handleApiError(err);
279
+ }
280
+ });
281
+ // resume deployment
282
+ deploy
283
+ .command('resume <id>')
284
+ .description('resume a paused deployment')
285
+ .action(async (id) => {
286
+ const token = getAuthToken();
287
+ if (!token) {
288
+ console.error('\nnot logged in. run "kova auth login" first.');
289
+ process.exit(1);
290
+ }
291
+ try {
292
+ const res = await authFetch(`/api/v1/deployments/${id}/resume`, {
293
+ method: 'POST'
294
+ });
295
+ const data = await res.json();
296
+ if (!res.ok) {
297
+ console.error(`\nresume failed: ${data.error || 'unknown error'}`);
298
+ process.exit(1);
299
+ }
300
+ console.log('\ndeployment resumed');
301
+ }
302
+ catch (err) {
303
+ handleApiError(err);
304
+ }
305
+ });
306
+ // deposit funds
307
+ deploy
308
+ .command('deposit <id> <amount>')
309
+ .description('add funds to deployment escrow')
310
+ .action(async (id, amountStr) => {
311
+ const token = getAuthToken();
312
+ if (!token) {
313
+ console.error('\nnot logged in. run "kova auth login" first.');
314
+ process.exit(1);
315
+ }
316
+ const amount = parseFloat(amountStr);
317
+ if (isNaN(amount) || amount <= 0) {
318
+ console.error('\namount must be a positive number');
319
+ process.exit(1);
320
+ }
321
+ try {
322
+ const res = await authFetch(`/api/v1/deployments/${id}/deposit`, {
323
+ method: 'POST',
324
+ body: JSON.stringify({ amount })
325
+ });
326
+ const data = await res.json();
327
+ if (!res.ok) {
328
+ console.error(`\ndeposit failed: ${data.error || data.message || 'unknown error'}`);
329
+ process.exit(1);
330
+ }
331
+ console.log(`\n$${amount.toFixed(2)} deposited to deployment escrow`);
332
+ }
333
+ catch (err) {
334
+ handleApiError(err);
335
+ }
336
+ });
337
+ // get logs
338
+ deploy
339
+ .command('logs <id>')
340
+ .description('get deployment logs')
341
+ .option('-s, --service <name>', 'filter by service name')
342
+ .option('-l, --limit <n>', 'max log lines', '100')
343
+ .option('--search <query>', 'search log content')
344
+ .option('-f, --follow', 'stream logs in real-time (not yet supported)')
345
+ .action(async (id, options) => {
346
+ const token = getAuthToken();
347
+ if (!token) {
348
+ console.error('\nnot logged in. run "kova auth login" first.');
349
+ process.exit(1);
350
+ }
351
+ if (options.follow) {
352
+ console.log('note: real-time log streaming not yet supported in CLI, showing recent logs');
353
+ }
354
+ try {
355
+ const params = new URLSearchParams();
356
+ if (options.service)
357
+ params.set('service', options.service);
358
+ if (options.search)
359
+ params.set('search', options.search);
360
+ params.set('limit', options.limit);
361
+ const res = await authFetch(`/api/v1/deployments/${id}/logs?${params.toString()}`);
362
+ const data = await res.json();
363
+ if (!res.ok) {
364
+ console.error(`\nfailed to get logs: ${data.error || 'unknown error'}`);
365
+ process.exit(1);
366
+ }
367
+ if (!data.logs || data.logs.length === 0) {
368
+ console.log('\nno logs found');
369
+ return;
370
+ }
371
+ // print logs in a readable format
372
+ for (const log of data.logs) {
373
+ const ts = new Date(log.timestamp).toISOString().substring(11, 23);
374
+ const svc = log.serviceName ? `[${log.serviceName}]` : '';
375
+ const stream = log.stream === 'stderr' ? ' ERR' : '';
376
+ console.log(`${ts} ${svc}${stream} ${log.logLine}`);
377
+ }
378
+ console.log(`\n--- ${data.logs.length} log lines ---`);
379
+ }
380
+ catch (err) {
381
+ handleApiError(err);
382
+ }
383
+ });
384
+ // get events
385
+ deploy
386
+ .command('events <id>')
387
+ .description('show deployment events')
388
+ .option('-l, --limit <n>', 'max events', '50')
389
+ .action(async (id, options) => {
390
+ const token = getAuthToken();
391
+ if (!token) {
392
+ console.error('\nnot logged in. run "kova auth login" first.');
393
+ process.exit(1);
394
+ }
395
+ try {
396
+ const params = new URLSearchParams();
397
+ params.set('limit', options.limit);
398
+ const res = await authFetch(`/api/v1/deployments/${id}/events?${params.toString()}`);
399
+ const data = await res.json();
400
+ if (!res.ok) {
401
+ console.error(`\nfailed to get events: ${data.error || 'unknown error'}`);
402
+ process.exit(1);
403
+ }
404
+ if (!data.events || data.events.length === 0) {
405
+ console.log('\nno events found');
406
+ return;
407
+ }
408
+ console.log('\ndeployment events');
409
+ console.log('========================================');
410
+ for (const event of data.events) {
411
+ const ts = formatDate(event.timestamp);
412
+ const msg = event.message || event.type;
413
+ console.log(`${ts} ${event.type.padEnd(25)} ${msg}`);
414
+ }
415
+ console.log(`\n--- ${data.events.length} events ---`);
416
+ }
417
+ catch (err) {
418
+ handleApiError(err);
419
+ }
420
+ });
421
+ // get version history
422
+ deploy
423
+ .command('versions <id>')
424
+ .description('show deployment version history')
425
+ .action(async (id) => {
426
+ const token = getAuthToken();
427
+ if (!token) {
428
+ console.error('\nnot logged in. run "kova auth login" first.');
429
+ process.exit(1);
430
+ }
431
+ try {
432
+ const res = await authFetch(`/api/v1/deployments/${id}/versions`);
433
+ const data = await res.json();
434
+ if (!res.ok) {
435
+ console.error(`\nfailed to get versions: ${data.error || 'unknown error'}`);
436
+ process.exit(1);
437
+ }
438
+ if (!data.versions || data.versions.length === 0) {
439
+ console.log('\nno version history');
440
+ return;
441
+ }
442
+ console.log(`\ncurrent version: v${data.currentVersion}`);
443
+ console.log('========================================');
444
+ const rows = data.versions.map((v) => ({
445
+ Version: `v${v.version}`,
446
+ Description: v.description || '-',
447
+ Created: formatDate(v.createdAt)
448
+ }));
449
+ formatTable(rows);
450
+ }
451
+ catch (err) {
452
+ handleApiError(err);
453
+ }
454
+ });
455
+ // list bids for a deployment
456
+ deploy
457
+ .command('bids <id>')
458
+ .description('list bids for a deployment')
459
+ .action(async (id) => {
460
+ const token = getAuthToken();
461
+ if (!token) {
462
+ console.error('\nnot logged in. run "kova auth login" first.');
463
+ process.exit(1);
464
+ }
465
+ try {
466
+ const res = await authFetch(`/api/v1/deployments/${id}/bids`);
467
+ const data = await res.json();
468
+ if (!res.ok) {
469
+ console.error(`\nfailed to get bids: ${data.error || 'unknown error'}`);
470
+ process.exit(1);
471
+ }
472
+ if (!data.bids || data.bids.length === 0) {
473
+ console.log('\nno bids yet - providers are still evaluating your deployment');
474
+ return;
475
+ }
476
+ console.log(`\n${data.bids.length} bid(s) received`);
477
+ console.log('========================================');
478
+ const rows = data.bids.map((b) => ({
479
+ ID: b.id.substring(0, 12) + '...',
480
+ Provider: (b.providerName || b.providerId.substring(0, 12) + '...'),
481
+ Price: `$${b.pricePerBlock}/block`,
482
+ State: b.state,
483
+ Reputation: b.reputation?.score ? `${b.reputation.score}%` : '-'
484
+ }));
485
+ formatTable(rows);
486
+ console.log(`\naccept a bid: kova deploy accept-bid ${id} <bid-id>`);
487
+ }
488
+ catch (err) {
489
+ handleApiError(err);
490
+ }
491
+ });
492
+ // accept a bid
493
+ deploy
494
+ .command('accept-bid <deployment-id> <bid-id>')
495
+ .description('accept a bid and create a lease')
496
+ .action(async (deploymentId, bidId) => {
497
+ const token = getAuthToken();
498
+ if (!token) {
499
+ console.error('\nnot logged in. run "kova auth login" first.');
500
+ process.exit(1);
501
+ }
502
+ console.log('accepting bid...');
503
+ try {
504
+ const res = await authFetch(`/api/v1/deployments/${deploymentId}/lease`, {
505
+ method: 'POST',
506
+ body: JSON.stringify({ bidId })
507
+ });
508
+ const data = await res.json();
509
+ if (!res.ok) {
510
+ console.error(`\nfailed to accept bid: ${data.error || data.message || 'unknown error'}`);
511
+ process.exit(1);
512
+ }
513
+ const l = data.lease;
514
+ console.log('\nlease created');
515
+ console.log('========================================');
516
+ console.log(`lease id: ${l.id}`);
517
+ console.log(`provider: ${l.providerId}`);
518
+ console.log(`node: ${l.nodeId}`);
519
+ console.log(`price: $${l.pricePerBlock}/block`);
520
+ console.log(`state: ${l.state}`);
521
+ console.log('========================================');
522
+ console.log('\nmanifest is being sent to provider. your deployment will be live shortly.');
523
+ }
524
+ catch (err) {
525
+ handleApiError(err);
526
+ }
527
+ });
528
+ // restart deployment
529
+ deploy
530
+ .command('restart <id>')
531
+ .description('restart a deployment (apply file changes)')
532
+ .action(async (id) => {
533
+ const token = getAuthToken();
534
+ if (!token) {
535
+ console.error('\nnot logged in. run "kova auth login" first.');
536
+ process.exit(1);
537
+ }
538
+ try {
539
+ const res = await authFetch(`/api/v1/deployments/${id}/restart`, {
540
+ method: 'POST'
541
+ });
542
+ const data = await res.json();
543
+ if (!res.ok) {
544
+ console.error(`\nrestart failed: ${data.error || 'unknown error'}`);
545
+ process.exit(1);
546
+ }
547
+ console.log('\nrestart requested');
548
+ console.log(data.message || 'changes will apply within 10 seconds');
549
+ }
550
+ catch (err) {
551
+ handleApiError(err);
552
+ }
553
+ });
554
+ return deploy;
555
+ }
556
+ // simple stdin prompt for confirmation
557
+ function promptConfirm(message) {
558
+ return new Promise((resolve) => {
559
+ process.stdout.write(message);
560
+ process.stdin.setEncoding('utf8');
561
+ process.stdin.once('data', (data) => {
562
+ const answer = data.toString().trim().toLowerCase();
563
+ resolve(answer === 'y' || answer === 'yes');
564
+ });
565
+ // auto-resolve after 30s to avoid hanging
566
+ setTimeout(() => resolve(false), 30000);
567
+ });
568
+ }
@@ -0,0 +1,70 @@
1
+ import { stateManager } from '../lib/state.js';
2
+ import { NodeConfig } from '../lib/config.js';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+ // try to fetch earnings from orchestrator api
7
+ async function fetchBackendEarnings() {
8
+ try {
9
+ // get api key from credentials file
10
+ const credPath = join(homedir(), '.kova', 'credentials.json');
11
+ if (!existsSync(credPath)) {
12
+ return null;
13
+ }
14
+ const creds = JSON.parse(readFileSync(credPath, 'utf8'));
15
+ if (!creds.apiKey) {
16
+ return null;
17
+ }
18
+ // load config
19
+ await NodeConfig.load();
20
+ const orchestratorUrl = NodeConfig.get('orchestratorUrl') || 'http://localhost:3000';
21
+ // fetch earnings from orchestrator
22
+ const response = await fetch(`${orchestratorUrl}/api/v1/provider/earnings`, {
23
+ headers: {
24
+ 'Authorization': `Bearer ${creds.apiKey}`,
25
+ 'Content-Type': 'application/json'
26
+ }
27
+ });
28
+ if (!response.ok) {
29
+ return null;
30
+ }
31
+ return await response.json();
32
+ }
33
+ catch (err) {
34
+ // silently fail if can't reach backend
35
+ return null;
36
+ }
37
+ }
38
+ export async function earningsCommand() {
39
+ const state = stateManager.getState();
40
+ console.log('\n kova earnings\n');
41
+ // try to fetch backend earnings
42
+ const backendEarnings = await fetchBackendEarnings();
43
+ if (backendEarnings) {
44
+ console.log('--- backend (authoritative) ---');
45
+ console.log(`total earned: $${backendEarnings.totalEarned.toFixed(4)}`);
46
+ console.log(`total streamed: $${backendEarnings.totalStreamed.toFixed(4)}`);
47
+ console.log(`withdrawable balance: $${backendEarnings.withdrawableBalance.toFixed(4)}`);
48
+ console.log(`active leases: ${backendEarnings.activeLeases}`);
49
+ console.log(`total leases: ${backendEarnings.totalLeases}`);
50
+ if (backendEarnings.walletAddress) {
51
+ console.log(`wallet: ${backendEarnings.walletAddress}`);
52
+ }
53
+ console.log('');
54
+ }
55
+ console.log('--- local stats ---');
56
+ console.log(`jobs completed: ${state.jobsCompleted}`);
57
+ console.log(`jobs failed: ${state.jobsFailed}`);
58
+ console.log(`local earnings cache: $${state.totalEarnings.toFixed(4)}`);
59
+ if (state.jobsCompleted > 0) {
60
+ const avgPerJob = state.totalEarnings / state.jobsCompleted;
61
+ console.log(`average per job: $${avgPerJob.toFixed(4)}`);
62
+ }
63
+ if (!backendEarnings) {
64
+ console.log('\nnote: could not reach orchestrator. showing local stats only.');
65
+ console.log('connect with an api key to see full earnings from backend.\n');
66
+ }
67
+ else {
68
+ console.log('\nwithdraw earnings via dashboard or "kova withdraw"\n');
69
+ }
70
+ }