cubelife 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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/SPRITE-LICENSE +14 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +39 -0
  6. package/dist/commands/agents.d.ts +2 -0
  7. package/dist/commands/agents.js +303 -0
  8. package/dist/commands/auth.d.ts +2 -0
  9. package/dist/commands/auth.js +233 -0
  10. package/dist/commands/billing.d.ts +2 -0
  11. package/dist/commands/billing.js +362 -0
  12. package/dist/commands/creature.d.ts +2 -0
  13. package/dist/commands/creature.js +166 -0
  14. package/dist/commands/default.d.ts +2 -0
  15. package/dist/commands/default.js +87 -0
  16. package/dist/commands/doctor.d.ts +2 -0
  17. package/dist/commands/doctor.js +48 -0
  18. package/dist/commands/init.d.ts +2 -0
  19. package/dist/commands/init.js +200 -0
  20. package/dist/commands/mcp.d.ts +2 -0
  21. package/dist/commands/mcp.js +9 -0
  22. package/dist/commands/projects.d.ts +2 -0
  23. package/dist/commands/projects.js +122 -0
  24. package/dist/commands/setup.d.ts +2 -0
  25. package/dist/commands/setup.js +453 -0
  26. package/dist/commands/status.d.ts +2 -0
  27. package/dist/commands/status.js +89 -0
  28. package/dist/commands/tutorial.d.ts +2 -0
  29. package/dist/commands/tutorial.js +9 -0
  30. package/dist/commands/view.d.ts +2 -0
  31. package/dist/commands/view.js +262 -0
  32. package/dist/data/sprite-data.d.ts +32 -0
  33. package/dist/data/sprite-data.js +865 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.js +6 -0
  36. package/dist/lib/api.d.ts +162 -0
  37. package/dist/lib/api.js +160 -0
  38. package/dist/lib/auth.d.ts +12 -0
  39. package/dist/lib/auth.js +113 -0
  40. package/dist/lib/browser.d.ts +1 -0
  41. package/dist/lib/browser.js +21 -0
  42. package/dist/lib/command-helpers.d.ts +26 -0
  43. package/dist/lib/command-helpers.js +60 -0
  44. package/dist/lib/compositor.d.ts +34 -0
  45. package/dist/lib/compositor.js +232 -0
  46. package/dist/lib/config.d.ts +39 -0
  47. package/dist/lib/config.js +89 -0
  48. package/dist/lib/constants.d.ts +12 -0
  49. package/dist/lib/constants.js +39 -0
  50. package/dist/lib/detect.d.ts +17 -0
  51. package/dist/lib/detect.js +99 -0
  52. package/dist/lib/doctor.d.ts +18 -0
  53. package/dist/lib/doctor.js +321 -0
  54. package/dist/lib/index.d.ts +11 -0
  55. package/dist/lib/index.js +6 -0
  56. package/dist/lib/integration.d.ts +66 -0
  57. package/dist/lib/integration.js +337 -0
  58. package/dist/lib/poll.d.ts +11 -0
  59. package/dist/lib/poll.js +31 -0
  60. package/dist/lib/resolve.d.ts +1 -0
  61. package/dist/lib/resolve.js +10 -0
  62. package/dist/lib/services/account-service.d.ts +17 -0
  63. package/dist/lib/services/account-service.js +30 -0
  64. package/dist/lib/services/agent-service.d.ts +17 -0
  65. package/dist/lib/services/agent-service.js +62 -0
  66. package/dist/lib/services/creature-service.d.ts +12 -0
  67. package/dist/lib/services/creature-service.js +35 -0
  68. package/dist/lib/services/project-service.d.ts +9 -0
  69. package/dist/lib/services/project-service.js +22 -0
  70. package/dist/lib/tutorial.d.ts +12 -0
  71. package/dist/lib/tutorial.js +358 -0
  72. package/dist/mcp/server.d.ts +8 -0
  73. package/dist/mcp/server.js +116 -0
  74. package/dist/ui/banner.d.ts +3 -0
  75. package/dist/ui/banner.js +27 -0
  76. package/dist/ui/half-block.d.ts +6 -0
  77. package/dist/ui/half-block.js +45 -0
  78. package/dist/ui/helpers.d.ts +3 -0
  79. package/dist/ui/helpers.js +11 -0
  80. package/dist/ui/index.d.ts +5 -0
  81. package/dist/ui/index.js +5 -0
  82. package/dist/ui/panel.d.ts +7 -0
  83. package/dist/ui/panel.js +21 -0
  84. package/dist/ui/preview.d.ts +7 -0
  85. package/dist/ui/preview.js +21 -0
  86. package/dist/ui/table.d.ts +8 -0
  87. package/dist/ui/table.js +20 -0
  88. package/dist/ui/theme.d.ts +24 -0
  89. package/dist/ui/theme.js +32 -0
  90. package/dist/version.d.ts +1 -0
  91. package/dist/version.js +1 -0
  92. package/package.json +63 -0
@@ -0,0 +1,233 @@
1
+ import * as p from '@clack/prompts';
2
+ import { register, login, logout, currentUser, requireAuth, } from '../lib/auth.js';
3
+ import { AdminClient } from '../lib/api.js';
4
+ import { brand, label } from '../ui/theme.js';
5
+ import { panel } from '../ui/panel.js';
6
+ import { isCancel } from '../ui/helpers.js';
7
+ import { rootOpts } from '../lib/command-helpers.js';
8
+ import { fetchAccountSummary, fetchLinkedInfo } from '../lib/services/account-service.js';
9
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
10
+ export function registerAuthCommands(program) {
11
+ program
12
+ .command('register')
13
+ .description('Create a new CubeLife account')
14
+ .option('--email <email>', 'Email address (non-interactive)')
15
+ .option('--password <password>', 'Password (non-interactive)')
16
+ .action(async function (opts) {
17
+ const { yes } = rootOpts(this);
18
+ const nonInteractive = !!(opts.email && opts.password);
19
+ let email;
20
+ let pw;
21
+ if (nonInteractive) {
22
+ email = opts.email;
23
+ pw = opts.password;
24
+ }
25
+ else {
26
+ p.intro(brand.primary('Create your CubeLife account'));
27
+ const emailResult = await p.text({
28
+ message: 'Email address',
29
+ placeholder: 'you@example.com',
30
+ validate: (v) => {
31
+ if (!EMAIL_RE.test(v))
32
+ return 'Please enter a valid email address.';
33
+ },
34
+ });
35
+ if (isCancel(emailResult)) {
36
+ p.cancel('Registration cancelled.');
37
+ process.exit(0);
38
+ }
39
+ email = emailResult;
40
+ const pwResult = await p.password({
41
+ message: 'Password (min 6 characters)',
42
+ validate: (v) => {
43
+ if (v.length < 6)
44
+ return 'Password must be at least 6 characters.';
45
+ },
46
+ });
47
+ if (isCancel(pwResult)) {
48
+ p.cancel('Registration cancelled.');
49
+ process.exit(0);
50
+ }
51
+ pw = pwResult;
52
+ if (!yes) {
53
+ const confirmResult = await p.password({
54
+ message: 'Confirm password',
55
+ validate: (v) => {
56
+ if (v !== pw)
57
+ return 'Passwords do not match.';
58
+ },
59
+ });
60
+ if (isCancel(confirmResult)) {
61
+ p.cancel('Registration cancelled.');
62
+ process.exit(0);
63
+ }
64
+ }
65
+ }
66
+ const spin = p.spinner();
67
+ spin.start('Creating account');
68
+ try {
69
+ const auth = await register(email, pw);
70
+ spin.stop('Account created');
71
+ p.log.success(`Registered as ${brand.primary(auth.email)}`);
72
+ p.outro(`Run ${brand.accent('cubelife tutorial')} to get started.`);
73
+ }
74
+ catch (err) {
75
+ spin.stop('Registration failed');
76
+ p.log.error(err.message);
77
+ process.exit(1);
78
+ }
79
+ });
80
+ program
81
+ .command('login')
82
+ .description('Log in to your CubeLife account')
83
+ .option('--email <email>', 'Email address (non-interactive)')
84
+ .option('--password <password>', 'Password (non-interactive)')
85
+ .action(async function (opts) {
86
+ const nonInteractive = !!(opts.email && opts.password);
87
+ let email;
88
+ let pw;
89
+ if (nonInteractive) {
90
+ email = opts.email;
91
+ pw = opts.password;
92
+ }
93
+ else {
94
+ p.intro(brand.primary('Log in to CubeLife'));
95
+ const emailResult = await p.text({
96
+ message: 'Email address',
97
+ placeholder: 'you@example.com',
98
+ validate: (v) => {
99
+ if (!EMAIL_RE.test(v))
100
+ return 'Please enter a valid email address.';
101
+ },
102
+ });
103
+ if (isCancel(emailResult)) {
104
+ p.cancel('Login cancelled.');
105
+ process.exit(0);
106
+ }
107
+ email = emailResult;
108
+ const pwResult = await p.password({
109
+ message: 'Password',
110
+ });
111
+ if (isCancel(pwResult)) {
112
+ p.cancel('Login cancelled.');
113
+ process.exit(0);
114
+ }
115
+ pw = pwResult;
116
+ }
117
+ const spin = p.spinner();
118
+ spin.start('Logging in');
119
+ try {
120
+ const auth = await login(email, pw);
121
+ spin.stop('Logged in');
122
+ p.log.success(`Logged in as ${brand.primary(auth.email)}`);
123
+ }
124
+ catch (err) {
125
+ spin.stop('Login failed');
126
+ p.log.error(err.message);
127
+ if (!nonInteractive) {
128
+ const retry = await p.confirm({ message: 'Try again?' });
129
+ if (!isCancel(retry) && retry) {
130
+ await program.commands
131
+ .find((c) => c.name() === 'login')
132
+ ?.parseAsync([], { from: 'user' });
133
+ return;
134
+ }
135
+ }
136
+ process.exit(1);
137
+ }
138
+ });
139
+ program
140
+ .command('logout')
141
+ .description('Log out and clear stored credentials')
142
+ .action(async function () {
143
+ const { yes } = rootOpts(this);
144
+ const user = await currentUser();
145
+ if (!user) {
146
+ p.log.warn('Not logged in.');
147
+ return;
148
+ }
149
+ if (!yes) {
150
+ const confirmed = await p.confirm({
151
+ message: `Log out of ${brand.primary(user.email)}?`,
152
+ });
153
+ if (isCancel(confirmed) || !confirmed) {
154
+ p.cancel('Cancelled.');
155
+ return;
156
+ }
157
+ }
158
+ await logout();
159
+ p.log.success('Logged out.');
160
+ });
161
+ program
162
+ .command('whoami')
163
+ .description('Show current account, tier, and linked agent')
164
+ .action(async function () {
165
+ const { json } = rootOpts(this);
166
+ const user = await currentUser();
167
+ if (!user) {
168
+ if (json) {
169
+ console.log(JSON.stringify({ error: 'not_logged_in' }));
170
+ }
171
+ else {
172
+ p.log.warn(`Not logged in. Run ${brand.accent('cubelife login')}`);
173
+ }
174
+ return;
175
+ }
176
+ let tierLabel = 'unavailable';
177
+ let usageText = 'unavailable';
178
+ let sparksText = 'unavailable';
179
+ let usageToday = 0;
180
+ let usageLimit = 0;
181
+ let sparks = 0;
182
+ let tier = 'free';
183
+ try {
184
+ const session = await requireAuth();
185
+ const client = new AdminClient(session.token);
186
+ const summary = await fetchAccountSummary(client);
187
+ tier = summary.tier;
188
+ usageToday = summary.usageToday;
189
+ usageLimit = summary.usageLimit;
190
+ sparks = summary.sparks;
191
+ const ratio = usageLimit > 0 ? usageToday / usageLimit : 0;
192
+ const countStr = usageToday.toLocaleString();
193
+ const limitStr = usageLimit.toLocaleString();
194
+ const usageRaw = `${countStr} / ${limitStr} today`;
195
+ usageText = ratio > 0.8 ? brand.warning(usageRaw) : usageRaw;
196
+ sparksText = sparks.toLocaleString();
197
+ tierLabel = `${summary.tierLabel} (${summary.agentLimit} agents, ${limitStr} calls/day)`;
198
+ }
199
+ catch {
200
+ tierLabel = 'unavailable';
201
+ usageText = 'unavailable';
202
+ sparksText = 'unavailable';
203
+ }
204
+ const linked = await fetchLinkedInfo();
205
+ const projectText = linked.projectId ?? 'none';
206
+ const agentText = linked.agentId ?? 'none';
207
+ const integrationText = linked.tools.length > 0
208
+ ? linked.tools.map((t) => t.name).join(', ')
209
+ : 'none detected';
210
+ if (json) {
211
+ console.log(JSON.stringify({
212
+ email: user.email,
213
+ tier,
214
+ usage: { today: usageToday, limit: usageLimit },
215
+ sparks,
216
+ project: linked.projectId,
217
+ agent: linked.agentId,
218
+ integrations: linked.tools.map((t) => t.id),
219
+ }));
220
+ return;
221
+ }
222
+ const lines = [
223
+ `${label('Email')}${brand.primary(user.email)}`,
224
+ `${label('Tier')}${tierLabel}`,
225
+ `${label('Usage')}${usageText}`,
226
+ `${label('Sparks')}${sparksText}`,
227
+ `${label('Project')}${projectText}`,
228
+ `${label('Agent')}${agentText}`,
229
+ `${label('Integrations')}${integrationText}`,
230
+ ];
231
+ console.log(panel(lines, { title: 'Account', width: 56 }));
232
+ });
233
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerBillingCommands(program: Command): void;
@@ -0,0 +1,362 @@
1
+ import * as p from '@clack/prompts';
2
+ import { AdminClient, TIER_LIMITS, resolveTier, } from '../lib/api.js';
3
+ import { requireAuth } from '../lib/auth.js';
4
+ import { brand, label } from '../ui/theme.js';
5
+ import { panel } from '../ui/panel.js';
6
+ import { table } from '../ui/table.js';
7
+ import { isCancel } from '../ui/helpers.js';
8
+ import { rootOpts, handleCommandError } from '../lib/command-helpers.js';
9
+ import { openBrowser } from '../lib/browser.js';
10
+ import { pollVerification } from '../lib/poll.js';
11
+ const LIFE_CALLBACK_URL = 'https://life.cubeworld.co.za/';
12
+ const TIER_ORDER = ['free', 'standard', 'pro'];
13
+ function formatCents(cents) {
14
+ return `R${(cents / 100).toFixed(2)}`;
15
+ }
16
+ function formatPrice(tier) {
17
+ const limits = TIER_LIMITS[tier];
18
+ return limits.priceZarCents === 0 ? 'Free' : `${formatCents(limits.priceZarCents)}/mo`;
19
+ }
20
+ function usageWarning(today, limit) {
21
+ const countStr = today.toLocaleString();
22
+ const limitStr = limit.toLocaleString();
23
+ const raw = `${countStr} / ${limitStr} today`;
24
+ const ratio = limit > 0 ? today / limit : 0;
25
+ return ratio > 0.8 ? brand.warning(raw) : raw;
26
+ }
27
+ export function registerBillingCommands(program) {
28
+ const billing = program
29
+ .command('billing')
30
+ .description('View and manage your billing, usage, and plan');
31
+ billing
32
+ .command('overview', { isDefault: true })
33
+ .description('Show billing overview')
34
+ .action(async function () {
35
+ const { json } = rootOpts(this);
36
+ try {
37
+ const session = await requireAuth();
38
+ const client = new AdminClient(session.token);
39
+ const spin = p.spinner();
40
+ if (!json)
41
+ spin.start('Loading billing info');
42
+ const [billingUser, usage] = await Promise.all([
43
+ client.getBillingUser(),
44
+ client.getBillingUsage(1),
45
+ ]);
46
+ if (!json)
47
+ spin.stop('Billing loaded');
48
+ const tier = resolveTier(billingUser.tier);
49
+ const limits = TIER_LIMITS[tier];
50
+ const todayUsage = usage.length > 0 ? usage[0].apiCalls : 0;
51
+ if (json) {
52
+ console.log(JSON.stringify({
53
+ tier,
54
+ price: formatPrice(tier),
55
+ apiCalls: { today: todayUsage, limit: limits.calls },
56
+ sparks: billingUser.sparks,
57
+ dormancyState: billingUser.dormancyState,
58
+ }));
59
+ return;
60
+ }
61
+ const lines = [
62
+ `${label('Plan')}${limits.label} (${formatPrice(tier)})`,
63
+ `${label('API Calls')}${usageWarning(todayUsage, limits.calls)}`,
64
+ `${label('Sparks')}${billingUser.sparks.toLocaleString()}`,
65
+ `${label('Status')}${billingUser.dormancyState ?? 'active'}`,
66
+ ];
67
+ console.log(panel(lines, { title: 'Billing Overview', width: 56 }));
68
+ p.log.message(brand.muted(`Run ${brand.accent('cubelife billing usage')} for 7-day history`));
69
+ p.log.message(brand.muted(`Run ${brand.accent('cubelife billing plans')} to compare plans`));
70
+ }
71
+ catch (err) {
72
+ handleCommandError({ error: err, json });
73
+ }
74
+ });
75
+ billing
76
+ .command('usage')
77
+ .description('Show usage over time')
78
+ .option('--days <n>', 'Number of days (1-30)', '7')
79
+ .action(async function (opts) {
80
+ const { json } = rootOpts(this);
81
+ const days = parseInt(opts.days, 10);
82
+ if (isNaN(days) || days < 1 || days > 30) {
83
+ if (json)
84
+ console.log(JSON.stringify({ error: 'days_must_be_1_to_30' }));
85
+ else
86
+ p.log.error('Days must be between 1 and 30.');
87
+ process.exit(1);
88
+ }
89
+ try {
90
+ const session = await requireAuth();
91
+ const client = new AdminClient(session.token);
92
+ const spin = p.spinner();
93
+ if (!json)
94
+ spin.start('Fetching usage');
95
+ const [billingUser, usage] = await Promise.all([
96
+ client.getBillingUser(),
97
+ client.getBillingUsage(days),
98
+ ]);
99
+ if (!json)
100
+ spin.stop('Usage loaded');
101
+ if (json) {
102
+ console.log(JSON.stringify({ usage }));
103
+ return;
104
+ }
105
+ if (usage.length === 0) {
106
+ p.log.info('No usage data available.');
107
+ return;
108
+ }
109
+ const rows = usage.map((day) => ({
110
+ date: day.date,
111
+ apiCalls: day.apiCalls.toLocaleString(),
112
+ events: day.eventsProcessed.toLocaleString(),
113
+ messages: day.messagesSent.toLocaleString(),
114
+ sparks: day.sparksUsed.toLocaleString(),
115
+ }));
116
+ console.log(table([
117
+ { label: 'Date', key: 'date', width: 12 },
118
+ { label: 'API Calls', key: 'apiCalls', width: 11 },
119
+ { label: 'Events', key: 'events', width: 8 },
120
+ { label: 'Messages', key: 'messages', width: 10 },
121
+ { label: 'Sparks', key: 'sparks', width: 8 },
122
+ ], rows));
123
+ const tier = resolveTier(billingUser.tier);
124
+ const limits = TIER_LIMITS[tier];
125
+ p.log.message(brand.muted(`Limit: ${limits.calls.toLocaleString()} API calls/day (${limits.label})`));
126
+ }
127
+ catch (err) {
128
+ handleCommandError({ error: err, json });
129
+ }
130
+ });
131
+ billing
132
+ .command('history')
133
+ .description('Show billing transaction history')
134
+ .option('--limit <n>', 'Number of transactions', '10')
135
+ .action(async function (opts) {
136
+ const { json } = rootOpts(this);
137
+ const limit = parseInt(opts.limit, 10);
138
+ if (isNaN(limit) || limit < 1 || limit > 100) {
139
+ if (json)
140
+ console.log(JSON.stringify({ error: 'limit_must_be_1_to_100' }));
141
+ else
142
+ p.log.error('Limit must be between 1 and 100.');
143
+ process.exit(1);
144
+ }
145
+ try {
146
+ const session = await requireAuth();
147
+ const client = new AdminClient(session.token);
148
+ const spin = p.spinner();
149
+ if (!json)
150
+ spin.start('Fetching history');
151
+ const history = await client.getBillingHistory(limit);
152
+ if (!json)
153
+ spin.stop('History loaded');
154
+ if (json) {
155
+ console.log(JSON.stringify({ history }));
156
+ return;
157
+ }
158
+ if (history.length === 0) {
159
+ p.log.info('No billing history.');
160
+ return;
161
+ }
162
+ const rows = history.map((item) => ({
163
+ date: new Date(item.createdAt).toLocaleDateString(),
164
+ type: item.type.replace(/_/g, ' '),
165
+ amount: formatCents(item.amount),
166
+ status: item.status,
167
+ }));
168
+ console.log(table([
169
+ { label: 'Date', key: 'date', width: 12 },
170
+ { label: 'Type', key: 'type', width: 14 },
171
+ { label: 'Amount', key: 'amount', width: 10 },
172
+ { label: 'Status', key: 'status', width: 8 },
173
+ ], rows));
174
+ }
175
+ catch (err) {
176
+ handleCommandError({ error: err, json });
177
+ }
178
+ });
179
+ billing
180
+ .command('plans')
181
+ .description('Compare available plans')
182
+ .action(async function () {
183
+ const { json } = rootOpts(this);
184
+ let currentTier = 'free';
185
+ try {
186
+ const session = await requireAuth();
187
+ const client = new AdminClient(session.token);
188
+ const billingUser = await client.getBillingUser();
189
+ currentTier = resolveTier(billingUser.tier);
190
+ }
191
+ catch {
192
+ // show plans even if not logged in
193
+ }
194
+ if (json) {
195
+ const plans = TIER_ORDER.map((t) => ({
196
+ tier: t,
197
+ current: t === currentTier,
198
+ ...TIER_LIMITS[t],
199
+ }));
200
+ console.log(JSON.stringify({ plans, currentTier }));
201
+ return;
202
+ }
203
+ const header = ' ' + TIER_ORDER.map((t) => {
204
+ const lbl = TIER_LIMITS[t].label.padEnd(14);
205
+ return t === currentTier ? brand.primary(lbl) : lbl;
206
+ }).join('');
207
+ const rows = [
208
+ featureRow('Price', (t) => formatPrice(t), currentTier),
209
+ featureRow('Agents', (t) => `${TIER_LIMITS[t].agents}`, currentTier),
210
+ featureRow('API Calls', (t) => `${TIER_LIMITS[t].calls.toLocaleString()}/day`, currentTier),
211
+ featureRow('Embeds', (t) => TIER_LIMITS[t].embedInstances === -1 ? 'Unlimited' : `${TIER_LIMITS[t].embedInstances}`, currentTier),
212
+ featureRow('Analytics', (t) => TIER_LIMITS[t].analyticsHistory === 0 ? '--' : `${TIER_LIMITS[t].analyticsHistory} days`, currentTier),
213
+ featureRow('Branding', (t) => TIER_LIMITS[t].customBranding ? 'Custom' : '--', currentTier),
214
+ featureRow('Webhooks', (t) => TIER_LIMITS[t].webhooks ? 'Yes' : '--', currentTier),
215
+ ];
216
+ console.log('\n' + header);
217
+ console.log(brand.muted(' ' + '─'.repeat(54)));
218
+ rows.forEach((r) => console.log(r));
219
+ console.log('');
220
+ p.log.message(`Current plan: ${brand.primary(TIER_LIMITS[currentTier].label)}`);
221
+ const tierIdx = TIER_ORDER.indexOf(currentTier);
222
+ if (tierIdx < TIER_ORDER.length - 1) {
223
+ const next = TIER_ORDER[tierIdx + 1];
224
+ p.log.message(brand.muted(`Run ${brand.accent(`cubelife billing upgrade ${next}`)} to upgrade`));
225
+ }
226
+ });
227
+ billing
228
+ .command('upgrade <plan>')
229
+ .description('Upgrade your plan')
230
+ .action(async function (plan) {
231
+ const { json, yes } = rootOpts(this);
232
+ if (!TIER_ORDER.includes(plan)) {
233
+ if (json)
234
+ console.log(JSON.stringify({ error: 'invalid_plan', valid: ['standard', 'pro'] }));
235
+ else
236
+ p.log.error(`Invalid plan. Valid options: ${brand.accent('standard')}, ${brand.accent('pro')}`);
237
+ process.exit(1);
238
+ }
239
+ if (plan === 'free') {
240
+ if (json)
241
+ console.log(JSON.stringify({ error: 'cannot_upgrade_to_free' }));
242
+ else
243
+ p.log.error('Cannot upgrade to the free plan. To downgrade, visit the dashboard.');
244
+ process.exit(1);
245
+ }
246
+ const targetTier = plan;
247
+ try {
248
+ const session = await requireAuth();
249
+ const client = new AdminClient(session.token);
250
+ const billingUser = await client.getBillingUser();
251
+ const currentTier = resolveTier(billingUser.tier);
252
+ if (currentTier === targetTier) {
253
+ if (json)
254
+ console.log(JSON.stringify({ error: 'already_on_plan' }));
255
+ else
256
+ p.log.warn(`Already on the ${TIER_LIMITS[currentTier].label} plan.`);
257
+ process.exit(1);
258
+ }
259
+ const currentIdx = TIER_ORDER.indexOf(currentTier);
260
+ const targetIdx = TIER_ORDER.indexOf(targetTier);
261
+ if (targetIdx < currentIdx) {
262
+ if (json)
263
+ console.log(JSON.stringify({ error: 'downgrade_not_supported' }));
264
+ else
265
+ p.log.error('Downgrades are not available through the CLI. Visit the dashboard to change your plan.');
266
+ process.exit(1);
267
+ }
268
+ if (!json && !yes) {
269
+ const lines = [
270
+ `${label('Current plan')}${TIER_LIMITS[currentTier].label}`,
271
+ `${label('New plan')}${TIER_LIMITS[targetTier].label} (${formatPrice(targetTier)})`,
272
+ ];
273
+ console.log(panel(lines, { title: `Upgrade to ${TIER_LIMITS[targetTier].label}`, width: 44 }));
274
+ const confirmed = await p.confirm({
275
+ message: `Confirm upgrade to ${TIER_LIMITS[targetTier].label}?`,
276
+ });
277
+ if (isCancel(confirmed) || !confirmed) {
278
+ p.cancel('Cancelled.');
279
+ return;
280
+ }
281
+ }
282
+ const spin = p.spinner();
283
+ if (!json)
284
+ spin.start('Initiating checkout');
285
+ const result = await client.subscribe({
286
+ plan: targetTier,
287
+ email: session.email,
288
+ callbackUrl: LIFE_CALLBACK_URL,
289
+ });
290
+ if (!json)
291
+ spin.stop('Checkout ready');
292
+ if (json) {
293
+ console.log(JSON.stringify({
294
+ authorisationUrl: result.authorisationUrl,
295
+ reference: result.reference,
296
+ }));
297
+ }
298
+ if (!json) {
299
+ const opened = await openBrowser(result.authorisationUrl);
300
+ if (opened) {
301
+ p.log.info('Opening PayStack checkout in your browser...');
302
+ }
303
+ else {
304
+ p.log.info('Open this URL in your browser to complete checkout:');
305
+ }
306
+ p.log.message(brand.accent(result.authorisationUrl));
307
+ }
308
+ const pollSpin = p.spinner();
309
+ if (!json)
310
+ pollSpin.start('Waiting for payment...');
311
+ let pollResult;
312
+ try {
313
+ pollResult = await pollVerification(client, result.reference);
314
+ }
315
+ catch (pollErr) {
316
+ if (!json)
317
+ pollSpin.stop('Failed');
318
+ throw pollErr;
319
+ }
320
+ if (pollResult.status === 'success') {
321
+ if (!json) {
322
+ pollSpin.stop('Payment confirmed');
323
+ p.log.success(`Plan upgraded to ${brand.primary(TIER_LIMITS[targetTier].label)}`);
324
+ }
325
+ else {
326
+ console.log(JSON.stringify({ verified: true, plan: targetTier }));
327
+ }
328
+ }
329
+ else if (pollResult.status === 'timeout') {
330
+ if (!json) {
331
+ pollSpin.stop('Timed out');
332
+ p.log.warn('Payment verification timed out. The payment may still complete.');
333
+ p.log.message(brand.muted(`Reference: ${result.reference}`));
334
+ p.log.message(brand.muted(`Run ${brand.accent('cubelife billing')} to check your current plan.`));
335
+ }
336
+ else {
337
+ console.log(JSON.stringify({ error: 'timeout', reference: result.reference }));
338
+ }
339
+ }
340
+ else {
341
+ if (!json) {
342
+ pollSpin.stop('Payment not completed');
343
+ p.log.error(`Payment ${pollResult.status}. Run ${brand.accent(`cubelife billing upgrade ${targetTier}`)} to try again.`);
344
+ }
345
+ else {
346
+ console.log(JSON.stringify({ error: pollResult.status }));
347
+ }
348
+ }
349
+ }
350
+ catch (err) {
351
+ handleCommandError({ error: err, json });
352
+ }
353
+ });
354
+ }
355
+ function featureRow(featureLabel, getValue, currentTier) {
356
+ const lbl = brand.label((' ' + featureLabel).padEnd(14));
357
+ const values = TIER_ORDER.map((t) => {
358
+ const val = getValue(t).padEnd(14);
359
+ return t === currentTier ? brand.primary(val) : val;
360
+ }).join('');
361
+ return lbl + values;
362
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function registerCreatureCommands(program: Command): void;