cubelife 0.2.0 → 0.3.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/dist/commands/agents.js +2 -2
- package/dist/commands/billing.js +373 -6
- package/dist/commands/projects.js +2 -2
- package/dist/lib/api.d.ts +38 -2
- package/dist/lib/api.js +27 -0
- package/dist/lib/command-helpers.d.ts +1 -0
- package/dist/lib/command-helpers.js +11 -0
- package/dist/lib/poll.d.ts +3 -0
- package/dist/lib/poll.js +2 -1
- package/package.json +1 -1
package/dist/commands/agents.js
CHANGED
|
@@ -6,7 +6,7 @@ import { table } from '../ui/table.js';
|
|
|
6
6
|
import { panel } from '../ui/panel.js';
|
|
7
7
|
import { CREATURE_TYPES, ID_DISPLAY_LENGTH } from '../lib/constants.js';
|
|
8
8
|
import { isCancel } from '../ui/helpers.js';
|
|
9
|
-
import { rootOpts, requireProject, handleCommandError, } from '../lib/command-helpers.js';
|
|
9
|
+
import { rootOpts, requireProject, handleCommandError, formatTimestamp, } from '../lib/command-helpers.js';
|
|
10
10
|
import * as agentService from '../lib/services/agent-service.js';
|
|
11
11
|
function maskKey(key) {
|
|
12
12
|
if (key.length <= 12)
|
|
@@ -51,7 +51,7 @@ export function registerAgentCommands(program) {
|
|
|
51
51
|
id: a.id.slice(0, ID_DISPLAY_LENGTH),
|
|
52
52
|
form: formatForm(a),
|
|
53
53
|
visibility: a.isPublic ? 'public' : 'private',
|
|
54
|
-
created:
|
|
54
|
+
created: formatTimestamp(a.createdAt),
|
|
55
55
|
}));
|
|
56
56
|
console.log(table([
|
|
57
57
|
{ label: '', key: 'linked', width: 2 },
|
package/dist/commands/billing.js
CHANGED
|
@@ -5,7 +5,7 @@ import { brand, label } from '../ui/theme.js';
|
|
|
5
5
|
import { panel } from '../ui/panel.js';
|
|
6
6
|
import { table } from '../ui/table.js';
|
|
7
7
|
import { isCancel } from '../ui/helpers.js';
|
|
8
|
-
import { rootOpts, handleCommandError } from '../lib/command-helpers.js';
|
|
8
|
+
import { rootOpts, handleCommandError, formatTimestamp } from '../lib/command-helpers.js';
|
|
9
9
|
import { openBrowser } from '../lib/browser.js';
|
|
10
10
|
import { pollVerification } from '../lib/poll.js';
|
|
11
11
|
const LIFE_CALLBACK_URL = 'https://life.cubeworld.co.za/';
|
|
@@ -53,16 +53,19 @@ export function registerBillingCommands(program) {
|
|
|
53
53
|
tier,
|
|
54
54
|
price: formatPrice(tier),
|
|
55
55
|
apiCalls: { today: todayUsage, limit: limits.calls },
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
burstSparks: billingUser.burstSparks ?? 0,
|
|
57
|
+
gems: billingUser.gems ?? 0,
|
|
58
|
+
sleeping: billingUser.sleeping ?? false,
|
|
58
59
|
}));
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
62
|
+
const status = billingUser.sleeping ? brand.warning('sleeping') : 'active';
|
|
61
63
|
const lines = [
|
|
62
64
|
`${label('Plan')}${limits.label} (${formatPrice(tier)})`,
|
|
63
65
|
`${label('API Calls')}${usageWarning(todayUsage, limits.calls)}`,
|
|
64
|
-
`${label('Sparks')}${billingUser.
|
|
65
|
-
`${label('
|
|
66
|
+
`${label('Burst Sparks')}${(billingUser.burstSparks ?? 0).toLocaleString()}`,
|
|
67
|
+
`${label('Gems')}${(billingUser.gems ?? 0).toLocaleString()}`,
|
|
68
|
+
`${label('Status')}${status}`,
|
|
66
69
|
];
|
|
67
70
|
console.log(panel(lines, { title: 'Billing Overview', width: 56 }));
|
|
68
71
|
p.log.message(brand.muted(`Run ${brand.accent('cubelife billing usage')} for 7-day history`));
|
|
@@ -160,7 +163,7 @@ export function registerBillingCommands(program) {
|
|
|
160
163
|
return;
|
|
161
164
|
}
|
|
162
165
|
const rows = history.map((item) => ({
|
|
163
|
-
date:
|
|
166
|
+
date: formatTimestamp(item.createdAt),
|
|
164
167
|
type: item.type.replace(/_/g, ' '),
|
|
165
168
|
amount: formatCents(item.amount),
|
|
166
169
|
status: item.status,
|
|
@@ -351,7 +354,371 @@ export function registerBillingCommands(program) {
|
|
|
351
354
|
handleCommandError({ error: err, json });
|
|
352
355
|
}
|
|
353
356
|
});
|
|
357
|
+
// --- Spark Packs ---
|
|
358
|
+
const sparks = billing.command('sparks').description('Buy Spark packs');
|
|
359
|
+
sparks
|
|
360
|
+
.command('list', { isDefault: true })
|
|
361
|
+
.description('List available Spark packs')
|
|
362
|
+
.action(async function () {
|
|
363
|
+
const { json } = rootOpts(this);
|
|
364
|
+
if (json) {
|
|
365
|
+
console.log(JSON.stringify({ packs: SPARK_PACKS }));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
console.log(table([
|
|
369
|
+
{ label: 'Pack', key: 'label', width: 10 },
|
|
370
|
+
{ label: 'Sparks', key: 'sparks', width: 10 },
|
|
371
|
+
{ label: 'Price', key: 'price', width: 10 },
|
|
372
|
+
], SPARK_PACKS.map((pk) => ({ label: pk.label, sparks: pk.sparks.toLocaleString(), price: formatCents(pk.priceZarCents) }))));
|
|
373
|
+
});
|
|
374
|
+
sparks
|
|
375
|
+
.command('buy <pack>')
|
|
376
|
+
.description('Buy a Spark pack (ember|flame|blaze|inferno)')
|
|
377
|
+
.action(async function (packId) {
|
|
378
|
+
const { json, yes } = rootOpts(this);
|
|
379
|
+
const pack = SPARK_PACKS.find((pk) => pk.id === packId);
|
|
380
|
+
if (!pack) {
|
|
381
|
+
if (json)
|
|
382
|
+
console.log(JSON.stringify({ error: 'invalid_pack', valid: SPARK_PACKS.map((pk) => pk.id) }));
|
|
383
|
+
else
|
|
384
|
+
p.log.error(`Invalid pack. Valid options: ${SPARK_PACKS.map((pk) => brand.accent(pk.id)).join(', ')}`);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
await doPurchase({
|
|
389
|
+
json, yes,
|
|
390
|
+
title: `Buy ${pack.label} Spark Pack`,
|
|
391
|
+
description: `${pack.sparks} Sparks for ${formatCents(pack.priceZarCents)}`,
|
|
392
|
+
initiate: (client, email) => client.initiateTopUp({ packId: pack.id, email, callbackUrl: `${LIFE_CALLBACK_URL}?cb=billing&type=spark_topup` }),
|
|
393
|
+
verify: (client, ref) => client.verifyTopUp(ref),
|
|
394
|
+
successMsg: `${pack.sparks} Sparks added to your account`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
handleCommandError({ error: err, json });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
// --- Gem Packs ---
|
|
402
|
+
const gems = billing.command('gems').description('Buy Gem packs');
|
|
403
|
+
gems
|
|
404
|
+
.command('list', { isDefault: true })
|
|
405
|
+
.description('List available Gem packs')
|
|
406
|
+
.action(async function () {
|
|
407
|
+
const { json } = rootOpts(this);
|
|
408
|
+
if (json) {
|
|
409
|
+
console.log(JSON.stringify({ packs: GEM_PACKS }));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
console.log(table([
|
|
413
|
+
{ label: 'Pack', key: 'label', width: 10 },
|
|
414
|
+
{ label: 'Gems', key: 'gems', width: 10 },
|
|
415
|
+
{ label: 'Price', key: 'price', width: 10 },
|
|
416
|
+
], GEM_PACKS.map((pk) => ({ label: pk.label, gems: pk.gems.toLocaleString(), price: formatCents(pk.priceZarCents) }))));
|
|
417
|
+
});
|
|
418
|
+
gems
|
|
419
|
+
.command('buy <pack>')
|
|
420
|
+
.description('Buy a Gem pack (shard|crystal|cluster|vein)')
|
|
421
|
+
.action(async function (packId) {
|
|
422
|
+
const { json, yes } = rootOpts(this);
|
|
423
|
+
const pack = GEM_PACKS.find((pk) => pk.id === packId);
|
|
424
|
+
if (!pack) {
|
|
425
|
+
if (json)
|
|
426
|
+
console.log(JSON.stringify({ error: 'invalid_pack', valid: GEM_PACKS.map((pk) => pk.id) }));
|
|
427
|
+
else
|
|
428
|
+
p.log.error(`Invalid pack. Valid options: ${GEM_PACKS.map((pk) => brand.accent(pk.id)).join(', ')}`);
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
await doPurchase({
|
|
433
|
+
json, yes,
|
|
434
|
+
title: `Buy ${pack.label} Gem Pack`,
|
|
435
|
+
description: `${pack.gems} Gems for ${formatCents(pack.priceZarCents)}`,
|
|
436
|
+
initiate: (client, email) => client.initiateGemTopUp({ packId: pack.id, email, callbackUrl: `${LIFE_CALLBACK_URL}?cb=billing&type=gem_topup` }),
|
|
437
|
+
verify: (client, ref) => client.verifyGemTopUp(ref),
|
|
438
|
+
successMsg: `${pack.gems} Gems added to your account`,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
catch (err) {
|
|
442
|
+
handleCommandError({ error: err, json });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
// --- Vault ---
|
|
446
|
+
billing
|
|
447
|
+
.command('vault')
|
|
448
|
+
.description('Convert Burst Sparks to Gems (3:1 ratio)')
|
|
449
|
+
.option('--amount <n>', 'Number of sparks to convert')
|
|
450
|
+
.action(async function (opts) {
|
|
451
|
+
const { json, yes } = rootOpts(this);
|
|
452
|
+
try {
|
|
453
|
+
const session = await requireAuth();
|
|
454
|
+
const client = new AdminClient(session.token);
|
|
455
|
+
const billingUser = await client.getBillingUser();
|
|
456
|
+
const tier = resolveTier(billingUser.tier);
|
|
457
|
+
if (tier === 'free') {
|
|
458
|
+
if (json)
|
|
459
|
+
console.log(JSON.stringify({ error: 'free_tier' }));
|
|
460
|
+
else
|
|
461
|
+
p.log.error('Vaulting is available on Standard and Pro plans.');
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
const burstSparks = billingUser.burstSparks ?? 0;
|
|
465
|
+
if (burstSparks < 3) {
|
|
466
|
+
if (json)
|
|
467
|
+
console.log(JSON.stringify({ error: 'insufficient_sparks', burstSparks }));
|
|
468
|
+
else
|
|
469
|
+
p.log.error(`Not enough Burst Sparks to vault (need at least 3, have ${burstSparks}).`);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
const amount = opts.amount ? parseInt(opts.amount, 10) : undefined;
|
|
473
|
+
if (amount !== undefined && (isNaN(amount) || amount < 3 || amount > burstSparks)) {
|
|
474
|
+
if (json)
|
|
475
|
+
console.log(JSON.stringify({ error: 'invalid_amount' }));
|
|
476
|
+
else
|
|
477
|
+
p.log.error(`Amount must be between 3 and ${burstSparks}.`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
const raw = amount ?? burstSparks;
|
|
481
|
+
const sparksToConvert = Math.floor(raw / 3) * 3;
|
|
482
|
+
const gemsToGain = sparksToConvert / 3;
|
|
483
|
+
if (sparksToConvert === 0) {
|
|
484
|
+
if (json)
|
|
485
|
+
console.log(JSON.stringify({ error: 'insufficient_sparks', burstSparks }));
|
|
486
|
+
else
|
|
487
|
+
p.log.error(`Not enough Burst Sparks to vault (need at least 3, have ${burstSparks}).`);
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
if (!json && !yes) {
|
|
491
|
+
const lines = [
|
|
492
|
+
`${label('Convert')}${sparksToConvert} Burst Sparks`,
|
|
493
|
+
`${label('Receive')}${gemsToGain} Gems`,
|
|
494
|
+
`${label('Remaining')}${burstSparks - sparksToConvert} Burst Sparks`,
|
|
495
|
+
];
|
|
496
|
+
console.log(panel(lines, { title: 'Vault Conversion', width: 44 }));
|
|
497
|
+
const confirmed = await p.confirm({ message: 'Confirm vault conversion?' });
|
|
498
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
499
|
+
p.cancel('Cancelled.');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const spin = p.spinner();
|
|
504
|
+
if (!json)
|
|
505
|
+
spin.start('Converting sparks to gems');
|
|
506
|
+
const result = await client.vaultSparks(sparksToConvert);
|
|
507
|
+
if (!json) {
|
|
508
|
+
spin.stop('Conversion complete');
|
|
509
|
+
p.log.success(`${result.gemsAdded} Gems added. Balance: ${result.gemsBalance} Gems, ${result.burstSparksRemaining} Burst Sparks remaining.`);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
console.log(JSON.stringify(result));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
handleCommandError({ error: err, json });
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
// --- Cancel ---
|
|
520
|
+
billing
|
|
521
|
+
.command('cancel')
|
|
522
|
+
.description('Cancel your subscription')
|
|
523
|
+
.action(async function () {
|
|
524
|
+
const { json, yes } = rootOpts(this);
|
|
525
|
+
try {
|
|
526
|
+
const session = await requireAuth();
|
|
527
|
+
const client = new AdminClient(session.token);
|
|
528
|
+
const billingUser = await client.getBillingUser();
|
|
529
|
+
const tier = resolveTier(billingUser.tier);
|
|
530
|
+
if (tier === 'free') {
|
|
531
|
+
if (json)
|
|
532
|
+
console.log(JSON.stringify({ error: 'already_free' }));
|
|
533
|
+
else
|
|
534
|
+
p.log.warn('Already on the Free plan.');
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
if (!json && !yes) {
|
|
538
|
+
const lines = [
|
|
539
|
+
`${label('Current plan')}${TIER_LIMITS[tier].label} (${formatPrice(tier)})`,
|
|
540
|
+
`${label('After cancel')}Free`,
|
|
541
|
+
];
|
|
542
|
+
console.log(panel(lines, { title: 'Cancel Subscription', width: 44 }));
|
|
543
|
+
const confirmed = await p.confirm({ message: 'Are you sure you want to cancel?' });
|
|
544
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
545
|
+
p.cancel('Kept current plan.');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
const spin = p.spinner();
|
|
550
|
+
if (!json)
|
|
551
|
+
spin.start('Cancelling subscription');
|
|
552
|
+
await client.cancelSubscription();
|
|
553
|
+
if (!json) {
|
|
554
|
+
spin.stop('Subscription cancelled');
|
|
555
|
+
p.log.success('Your plan has been changed to Free.');
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
console.log(JSON.stringify({ cancelled: true }));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
handleCommandError({ error: err, json });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
// --- Downgrade Preview ---
|
|
566
|
+
billing
|
|
567
|
+
.command('downgrade-preview')
|
|
568
|
+
.description('Preview the impact of downgrading your plan')
|
|
569
|
+
.option('--target <tier>', 'Target tier (free or standard)', 'free')
|
|
570
|
+
.action(async function (opts) {
|
|
571
|
+
const { json } = rootOpts(this);
|
|
572
|
+
try {
|
|
573
|
+
const session = await requireAuth();
|
|
574
|
+
const client = new AdminClient(session.token);
|
|
575
|
+
const spin = p.spinner();
|
|
576
|
+
if (!json)
|
|
577
|
+
spin.start('Loading preview');
|
|
578
|
+
const preview = await client.getDowngradePreview(opts.target);
|
|
579
|
+
if (!json)
|
|
580
|
+
spin.stop('Preview loaded');
|
|
581
|
+
if (json) {
|
|
582
|
+
console.log(JSON.stringify(preview));
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const lines = [
|
|
586
|
+
`${label('Current')}${preview.currentTier}`,
|
|
587
|
+
`${label('Target')}${preview.targetTier}`,
|
|
588
|
+
];
|
|
589
|
+
if (preview.impacts?.length) {
|
|
590
|
+
lines.push('');
|
|
591
|
+
for (const impact of preview.impacts) {
|
|
592
|
+
const warn = impact.current > impact.limit;
|
|
593
|
+
const line = `${impact.label}: ${impact.current} → limit ${impact.limit}`;
|
|
594
|
+
lines.push(` ${warn ? brand.warning(line) : line}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (preview.lostFeatures?.length) {
|
|
598
|
+
lines.push('');
|
|
599
|
+
lines.push(brand.warning(`Features lost: ${preview.lostFeatures.join(', ')}`));
|
|
600
|
+
}
|
|
601
|
+
console.log(panel(lines, { title: 'Downgrade Preview', width: 52 }));
|
|
602
|
+
p.log.message(brand.muted(`Run ${brand.accent('cubelife billing cancel')} to proceed.`));
|
|
603
|
+
}
|
|
604
|
+
catch (err) {
|
|
605
|
+
handleCommandError({ error: err, json });
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
// --- Starter Pack ---
|
|
609
|
+
billing
|
|
610
|
+
.command('starter-pack')
|
|
611
|
+
.description('Buy the Starter Pack (one-time, R40)')
|
|
612
|
+
.action(async function () {
|
|
613
|
+
const { json, yes } = rootOpts(this);
|
|
614
|
+
try {
|
|
615
|
+
const session = await requireAuth();
|
|
616
|
+
const client = new AdminClient(session.token);
|
|
617
|
+
const billingUser = await client.getBillingUser();
|
|
618
|
+
if (billingUser.starterPackClaimed) {
|
|
619
|
+
if (json)
|
|
620
|
+
console.log(JSON.stringify({ error: 'already_claimed' }));
|
|
621
|
+
else
|
|
622
|
+
p.log.warn('Starter Pack already claimed.');
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
await doPurchase({
|
|
626
|
+
json, yes, session,
|
|
627
|
+
title: 'Buy Starter Pack',
|
|
628
|
+
description: '30 Gems + exclusive item for R40.00',
|
|
629
|
+
initiate: (client2, email) => client2.initiateStarterPack({ email, callbackUrl: `${LIFE_CALLBACK_URL}?cb=billing&type=starter_pack` }),
|
|
630
|
+
verify: (client2, ref) => client2.verifyStarterPack(ref),
|
|
631
|
+
successMsg: 'Starter Pack claimed! 30 Gems and exclusive item added.',
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
handleCommandError({ error: err, json });
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
async function doPurchase(opts) {
|
|
640
|
+
const { json, yes, title, description, initiate, verify, successMsg } = opts;
|
|
641
|
+
const session = opts.session ?? await requireAuth();
|
|
642
|
+
const client = new AdminClient(session.token);
|
|
643
|
+
if (!json && !yes) {
|
|
644
|
+
console.log(panel([description], { title, width: 44 }));
|
|
645
|
+
const confirmed = await p.confirm({ message: 'Proceed to checkout?' });
|
|
646
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
647
|
+
p.cancel('Cancelled.');
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const spin = p.spinner();
|
|
652
|
+
if (!json)
|
|
653
|
+
spin.start('Initiating checkout');
|
|
654
|
+
const result = await initiate(client, session.email);
|
|
655
|
+
if (!json)
|
|
656
|
+
spin.stop('Checkout ready');
|
|
657
|
+
if (json) {
|
|
658
|
+
console.log(JSON.stringify({ authorisationUrl: result.authorisationUrl, reference: result.reference }));
|
|
659
|
+
}
|
|
660
|
+
if (!json) {
|
|
661
|
+
const opened = await openBrowser(result.authorisationUrl);
|
|
662
|
+
if (opened)
|
|
663
|
+
p.log.info('Opening PayStack checkout in your browser...');
|
|
664
|
+
else
|
|
665
|
+
p.log.info('Open this URL to complete checkout:');
|
|
666
|
+
p.log.message(brand.accent(result.authorisationUrl));
|
|
667
|
+
}
|
|
668
|
+
const pollSpin = p.spinner();
|
|
669
|
+
if (!json)
|
|
670
|
+
pollSpin.start('Waiting for payment...');
|
|
671
|
+
let pollResult;
|
|
672
|
+
try {
|
|
673
|
+
pollResult = await pollVerification(client, result.reference, {
|
|
674
|
+
verify: (c, ref) => verify(c, ref),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (pollErr) {
|
|
678
|
+
if (!json)
|
|
679
|
+
pollSpin.stop('Failed');
|
|
680
|
+
throw pollErr;
|
|
681
|
+
}
|
|
682
|
+
if (pollResult.status === 'success') {
|
|
683
|
+
if (!json) {
|
|
684
|
+
pollSpin.stop('Payment confirmed');
|
|
685
|
+
p.log.success(successMsg);
|
|
686
|
+
}
|
|
687
|
+
else
|
|
688
|
+
console.log(JSON.stringify({ verified: true }));
|
|
689
|
+
}
|
|
690
|
+
else if (pollResult.status === 'timeout') {
|
|
691
|
+
if (!json) {
|
|
692
|
+
pollSpin.stop('Timed out');
|
|
693
|
+
p.log.warn('Payment verification timed out. The payment may still complete.');
|
|
694
|
+
p.log.message(brand.muted(`Reference: ${result.reference}`));
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
console.log(JSON.stringify({ error: 'timeout', reference: result.reference }));
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
if (!json) {
|
|
702
|
+
pollSpin.stop('Payment not completed');
|
|
703
|
+
p.log.error(`Payment ${pollResult.status}.`);
|
|
704
|
+
}
|
|
705
|
+
else
|
|
706
|
+
console.log(JSON.stringify({ error: pollResult.status }));
|
|
707
|
+
}
|
|
354
708
|
}
|
|
709
|
+
// --- Pack data ---
|
|
710
|
+
const SPARK_PACKS = [
|
|
711
|
+
{ id: 'ember', sparks: 50, priceZarCents: 4_000, label: 'Ember' },
|
|
712
|
+
{ id: 'flame', sparks: 150, priceZarCents: 10_000, label: 'Flame' },
|
|
713
|
+
{ id: 'blaze', sparks: 500, priceZarCents: 30_000, label: 'Blaze' },
|
|
714
|
+
{ id: 'inferno', sparks: 1_500, priceZarCents: 80_000, label: 'Inferno' },
|
|
715
|
+
];
|
|
716
|
+
const GEM_PACKS = [
|
|
717
|
+
{ id: 'shard', gems: 50, priceZarCents: 3_000, label: 'Shard' },
|
|
718
|
+
{ id: 'crystal', gems: 200, priceZarCents: 10_000, label: 'Crystal' },
|
|
719
|
+
{ id: 'cluster', gems: 600, priceZarCents: 25_000, label: 'Cluster' },
|
|
720
|
+
{ id: 'vein', gems: 1_500, priceZarCents: 50_000, label: 'Vein' },
|
|
721
|
+
];
|
|
355
722
|
function featureRow(featureLabel, getValue, currentTier) {
|
|
356
723
|
const lbl = brand.label((' ' + featureLabel).padEnd(14));
|
|
357
724
|
const values = TIER_ORDER.map((t) => {
|
|
@@ -4,7 +4,7 @@ import { ID_DISPLAY_LENGTH } from '../lib/constants.js';
|
|
|
4
4
|
import { brand, dot } from '../ui/theme.js';
|
|
5
5
|
import { table } from '../ui/table.js';
|
|
6
6
|
import { isCancel } from '../ui/helpers.js';
|
|
7
|
-
import { rootOpts, handleCommandError, } from '../lib/command-helpers.js';
|
|
7
|
+
import { rootOpts, handleCommandError, formatTimestamp, } from '../lib/command-helpers.js';
|
|
8
8
|
import * as projectService from '../lib/services/project-service.js';
|
|
9
9
|
export function registerProjectCommands(program) {
|
|
10
10
|
const projects = program
|
|
@@ -35,7 +35,7 @@ export function registerProjectCommands(program) {
|
|
|
35
35
|
id: proj.id.slice(0, ID_DISPLAY_LENGTH),
|
|
36
36
|
name: proj.name,
|
|
37
37
|
product: proj.product,
|
|
38
|
-
created:
|
|
38
|
+
created: formatTimestamp(proj.createdAt),
|
|
39
39
|
}));
|
|
40
40
|
console.log(table([
|
|
41
41
|
{ label: '', key: 'linked', width: 2 },
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ export type Tier = 'free' | 'standard' | 'pro';
|
|
|
6
6
|
export interface BillingUser {
|
|
7
7
|
tier: Tier;
|
|
8
8
|
sparks: number;
|
|
9
|
+
burstSparks?: number;
|
|
10
|
+
gems?: number;
|
|
11
|
+
sleeping?: boolean;
|
|
9
12
|
dormancyState: string;
|
|
10
13
|
starterPackClaimed?: boolean;
|
|
11
14
|
locale?: string;
|
|
@@ -19,14 +22,36 @@ export interface UsageDay {
|
|
|
19
22
|
}
|
|
20
23
|
export interface BillingHistoryItem {
|
|
21
24
|
id: string;
|
|
22
|
-
type: 'subscription' | 'spark_topup' | 'starter_pack' | 'cosmetic';
|
|
25
|
+
type: 'subscription' | 'subscription_ended' | 'spark_topup' | 'gem_topup' | 'starter_pack' | 'cosmetic';
|
|
23
26
|
product: 'world' | 'life';
|
|
24
27
|
amount: number;
|
|
25
28
|
currency: string;
|
|
26
29
|
sparks: number | null;
|
|
30
|
+
gems: number | null;
|
|
27
31
|
status: 'success' | 'failed';
|
|
28
32
|
paystackReference: string;
|
|
29
|
-
createdAt:
|
|
33
|
+
createdAt: unknown;
|
|
34
|
+
}
|
|
35
|
+
export interface VaultResponse {
|
|
36
|
+
gemsAdded: number;
|
|
37
|
+
gemsBalance: number;
|
|
38
|
+
burstSparksRemaining: number;
|
|
39
|
+
}
|
|
40
|
+
export interface DowngradePreview {
|
|
41
|
+
currentTier: string;
|
|
42
|
+
targetTier: string;
|
|
43
|
+
impacts: Array<{
|
|
44
|
+
key: string;
|
|
45
|
+
label: string;
|
|
46
|
+
current: number;
|
|
47
|
+
limit: number;
|
|
48
|
+
}>;
|
|
49
|
+
lostFeatures: string[];
|
|
50
|
+
}
|
|
51
|
+
export interface TopUpRequest {
|
|
52
|
+
packId: string;
|
|
53
|
+
email: string;
|
|
54
|
+
callbackUrl: string;
|
|
30
55
|
}
|
|
31
56
|
export interface SubscribeRequest {
|
|
32
57
|
plan: string;
|
|
@@ -138,6 +163,17 @@ export declare class AdminClient {
|
|
|
138
163
|
getBillingHistory(limit?: number): Promise<BillingHistoryItem[]>;
|
|
139
164
|
subscribe(opts: SubscribeRequest): Promise<SubscribeResponse>;
|
|
140
165
|
verifyPayment(reference: string): Promise<VerifyResponse>;
|
|
166
|
+
initiateTopUp(opts: TopUpRequest): Promise<SubscribeResponse>;
|
|
167
|
+
verifyTopUp(reference: string): Promise<VerifyResponse>;
|
|
168
|
+
initiateGemTopUp(opts: TopUpRequest): Promise<SubscribeResponse>;
|
|
169
|
+
verifyGemTopUp(reference: string): Promise<VerifyResponse>;
|
|
170
|
+
initiateStarterPack(opts: Omit<TopUpRequest, 'packId'>): Promise<SubscribeResponse>;
|
|
171
|
+
verifyStarterPack(reference: string): Promise<VerifyResponse>;
|
|
172
|
+
vaultSparks(amount?: number): Promise<VaultResponse>;
|
|
173
|
+
cancelSubscription(): Promise<{
|
|
174
|
+
cancelled: boolean;
|
|
175
|
+
}>;
|
|
176
|
+
getDowngradePreview(targetTier: string): Promise<DowngradePreview>;
|
|
141
177
|
listProjects(): Promise<ProjectListResponse>;
|
|
142
178
|
createProject(name: string): Promise<ProjectResponse>;
|
|
143
179
|
deleteProject(id: string): Promise<void>;
|
package/dist/lib/api.js
CHANGED
|
@@ -75,6 +75,33 @@ export class AdminClient {
|
|
|
75
75
|
async verifyPayment(reference) {
|
|
76
76
|
return this.request('POST', '/v1/billing/verify', { reference });
|
|
77
77
|
}
|
|
78
|
+
async initiateTopUp(opts) {
|
|
79
|
+
return this.request('POST', '/v1/billing/topup', opts);
|
|
80
|
+
}
|
|
81
|
+
async verifyTopUp(reference) {
|
|
82
|
+
return this.request('POST', '/v1/billing/topup/verify', { reference });
|
|
83
|
+
}
|
|
84
|
+
async initiateGemTopUp(opts) {
|
|
85
|
+
return this.request('POST', '/v1/billing/gem-topup', opts);
|
|
86
|
+
}
|
|
87
|
+
async verifyGemTopUp(reference) {
|
|
88
|
+
return this.request('POST', '/v1/billing/gem-topup/verify', { reference });
|
|
89
|
+
}
|
|
90
|
+
async initiateStarterPack(opts) {
|
|
91
|
+
return this.request('POST', '/v1/billing/starter-pack', opts);
|
|
92
|
+
}
|
|
93
|
+
async verifyStarterPack(reference) {
|
|
94
|
+
return this.request('POST', '/v1/billing/starter-pack/verify', { reference });
|
|
95
|
+
}
|
|
96
|
+
async vaultSparks(amount) {
|
|
97
|
+
return this.request('POST', '/v1/billing/vault', amount ? { amount } : {});
|
|
98
|
+
}
|
|
99
|
+
async cancelSubscription() {
|
|
100
|
+
return this.request('POST', '/v1/billing/cancel');
|
|
101
|
+
}
|
|
102
|
+
async getDowngradePreview(targetTier) {
|
|
103
|
+
return this.request('GET', `/v1/billing/downgrade-preview?targetTier=${encodeURIComponent(targetTier)}`);
|
|
104
|
+
}
|
|
78
105
|
async listProjects() {
|
|
79
106
|
return this.request('GET', '/v1/projects');
|
|
80
107
|
}
|
|
@@ -15,6 +15,7 @@ export interface CommandErrorOptions {
|
|
|
15
15
|
exitCode?: number;
|
|
16
16
|
}
|
|
17
17
|
export declare function handleCommandError(opts: CommandErrorOptions): never;
|
|
18
|
+
export declare function formatTimestamp(ts: unknown): string;
|
|
18
19
|
export interface ResolvedProject {
|
|
19
20
|
projectId: string;
|
|
20
21
|
}
|
|
@@ -24,6 +24,17 @@ export function handleCommandError(opts) {
|
|
|
24
24
|
}
|
|
25
25
|
process.exit(opts.exitCode ?? 1);
|
|
26
26
|
}
|
|
27
|
+
export function formatTimestamp(ts) {
|
|
28
|
+
if (!ts)
|
|
29
|
+
return '--';
|
|
30
|
+
if (typeof ts === 'number')
|
|
31
|
+
return new Date(ts).toLocaleDateString();
|
|
32
|
+
if (typeof ts === 'object' && ts !== null && '_seconds' in ts) {
|
|
33
|
+
return new Date(ts._seconds * 1000).toLocaleDateString();
|
|
34
|
+
}
|
|
35
|
+
const d = new Date(ts);
|
|
36
|
+
return isNaN(d.getTime()) ? '--' : d.toLocaleDateString();
|
|
37
|
+
}
|
|
27
38
|
export async function requireProject(cmd, json) {
|
|
28
39
|
const projectFlag = cmd.parent?.opts().project;
|
|
29
40
|
let projectId;
|
package/dist/lib/poll.d.ts
CHANGED
|
@@ -7,5 +7,8 @@ export interface PollResult {
|
|
|
7
7
|
export interface PollOptions {
|
|
8
8
|
interval?: number;
|
|
9
9
|
timeout?: number;
|
|
10
|
+
verify?: (client: AdminClient, reference: string) => Promise<{
|
|
11
|
+
verified: boolean;
|
|
12
|
+
}>;
|
|
10
13
|
}
|
|
11
14
|
export declare function pollVerification(client: AdminClient, reference: string, opts?: PollOptions): Promise<PollResult>;
|
package/dist/lib/poll.js
CHANGED
|
@@ -5,9 +5,10 @@ export async function pollVerification(client, reference, opts = {}) {
|
|
|
5
5
|
const interval = opts.interval ?? 5_000;
|
|
6
6
|
const timeout = opts.timeout ?? 300_000;
|
|
7
7
|
const deadline = Date.now() + timeout;
|
|
8
|
+
const verifyFn = opts.verify ?? ((c, ref) => c.verifyPayment(ref));
|
|
8
9
|
while (Date.now() < deadline) {
|
|
9
10
|
try {
|
|
10
|
-
const data = await client
|
|
11
|
+
const data = await verifyFn(client, reference);
|
|
11
12
|
return { status: 'success', data };
|
|
12
13
|
}
|
|
13
14
|
catch (err) {
|