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.
- package/README.md +138 -0
- package/bin/cli.js +2 -0
- package/dist/__tests__/auto-bidder.test.js +267 -0
- package/dist/__tests__/container-manager.test.js +189 -0
- package/dist/__tests__/deployment-executor.test.js +332 -0
- package/dist/__tests__/heartbeat.test.js +191 -0
- package/dist/__tests__/lease-handler.test.js +268 -0
- package/dist/__tests__/resource-limits.test.js +164 -0
- package/dist/api/server.js +607 -0
- package/dist/cli.js +47 -0
- package/dist/commands/deploy.js +568 -0
- package/dist/commands/earnings.js +70 -0
- package/dist/commands/start.js +358 -0
- package/dist/commands/status.js +50 -0
- package/dist/commands/stop.js +101 -0
- package/dist/lib/client.js +87 -0
- package/dist/lib/config.js +107 -0
- package/dist/lib/docker.js +415 -0
- package/dist/lib/logger.js +12 -0
- package/dist/lib/message-signer.js +93 -0
- package/dist/lib/monitor.js +105 -0
- package/dist/lib/p2p.js +186 -0
- package/dist/lib/resource-limits.js +84 -0
- package/dist/lib/state.js +113 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/usage-meter.js +63 -0
- package/dist/services/auto-bidder.js +332 -0
- package/dist/services/container-manager.js +282 -0
- package/dist/services/deployment-executor.js +1562 -0
- package/dist/services/heartbeat.js +110 -0
- package/dist/services/job-handler.js +241 -0
- package/dist/services/lease-handler.js +382 -0
- package/package.json +51 -0
|
@@ -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
|
+
}
|