@myapihq/cli 1.3.13 → 2.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.
Files changed (57) hide show
  1. package/dist/commands/account.d.ts +7 -1
  2. package/dist/commands/account.js +356 -18
  3. package/dist/commands/authproduct.d.ts +7 -0
  4. package/dist/commands/authproduct.js +286 -0
  5. package/dist/commands/billing-auto-recharge.test.d.ts +1 -0
  6. package/dist/commands/billing-auto-recharge.test.js +103 -0
  7. package/dist/commands/billing.d.ts +3 -0
  8. package/dist/commands/billing.js +116 -1
  9. package/dist/commands/config.js +1 -1
  10. package/dist/commands/container.d.ts +1 -0
  11. package/dist/commands/container.js +115 -11
  12. package/dist/commands/doctor-setup.test.js +86 -1
  13. package/dist/commands/doctor.d.ts +8 -0
  14. package/dist/commands/doctor.js +70 -15
  15. package/dist/commands/domain.js +2 -2
  16. package/dist/commands/email/index.js +0 -13
  17. package/dist/commands/fn.d.ts +1 -0
  18. package/dist/commands/fn.js +64 -12
  19. package/dist/commands/keys.js +3 -3
  20. package/dist/commands/queue.d.ts +1 -0
  21. package/dist/commands/queue.js +10 -0
  22. package/dist/commands/setup.js +8 -8
  23. package/dist/commands/status.d.ts +1 -1
  24. package/dist/commands/status.js +23 -27
  25. package/dist/commands/storage.js +3 -1
  26. package/dist/commands/workflow.js +21 -6
  27. package/dist/completion.js +5 -4
  28. package/dist/config.js +1 -1
  29. package/dist/exposes.test.js +1 -2
  30. package/dist/index.js +57 -33
  31. package/dist/registrant.js +5 -5
  32. package/dist/sdk-queue.test.js +3 -2
  33. package/dist/skills/my-api-hq/README.md +1 -0
  34. package/dist/skills/my-api-hq/SKILL.md +27 -14
  35. package/dist/skills/my-auth-api/README.md +33 -0
  36. package/dist/skills/my-auth-api/SKILL.md +112 -0
  37. package/dist/skills/my-auth-api/claude/.claude-plugin/plugin.json +6 -0
  38. package/dist/skills/my-auth-api/openapi/.gitkeep +0 -0
  39. package/dist/skills/my-crm-api/SKILL.md +1 -1
  40. package/dist/skills/my-domain-api/SKILL.md +5 -5
  41. package/dist/skills/my-email-api/README.md +2 -3
  42. package/dist/skills/my-email-api/SKILL.md +7 -18
  43. package/dist/skills/my-email-api/claude/.claude-plugin/plugin.json +1 -1
  44. package/dist/skills/my-email-verify-api/SKILL.md +1 -1
  45. package/dist/skills/my-email-verify-api/claude/.claude-plugin/plugin.json +1 -1
  46. package/dist/skills/my-funnel-api/SKILL.md +1 -1
  47. package/dist/skills/my-git-api/README.md +43 -0
  48. package/dist/skills/my-git-api/SKILL.md +115 -0
  49. package/dist/skills/my-git-api/claude/.claude-plugin/plugin.json +6 -0
  50. package/dist/skills/my-git-api/openapi/.gitkeep +0 -0
  51. package/dist/skills/my-llm-api/README.md +1 -1
  52. package/dist/skills/my-llm-api/claude/.claude-plugin/plugin.json +1 -1
  53. package/package.json +2 -2
  54. package/dist/commands/auth.d.ts +0 -11
  55. package/dist/commands/auth.js +0 -345
  56. package/dist/commands/email/campaign.d.ts +0 -4
  57. package/dist/commands/email/campaign.js +0 -200
@@ -0,0 +1,286 @@
1
+ import { auth as sdkAuth } from '@myapihq/sdk';
2
+ import { requireConfig } from '../config.js';
3
+ import { success, error, info, printTable, printJson } from '../output.js';
4
+ import { requireOrg } from '../helpers.js';
5
+ // `myapi auth` — the END-USER auth product (my-auth-api): a managed OIDC IdP
6
+ // for the users of apps built on MyAPI. Operator/account auth lives under
7
+ // `myapi account` (see commands/auth.ts + index.ts dispatch). The machine
8
+ // OIDC endpoints (discovery/jwks/authorize/token/userinfo) are consumed by
9
+ // apps + the JS SDK, not the CLI, so only the management surface is here.
10
+ export const SCHEMA = {
11
+ name: 'string',
12
+ type: 'string', // spa | web
13
+ redirect: 'string', // comma-separated redirect URIs
14
+ theme: 'string', // JSON for the hosted login page
15
+ connections: 'string', // comma-separated sign-in methods: google,password,magic
16
+ domain: 'string', // custom auth domain, e.g. auth.acme.com
17
+ };
18
+ export const EXPOSES = [
19
+ 'POST /auth/orgs/{org_id}/tenant',
20
+ 'GET /auth/orgs/{org_id}/tenant',
21
+ 'POST /auth/orgs/{org_id}/clients',
22
+ 'GET /auth/orgs/{org_id}/clients',
23
+ 'GET /auth/orgs/{org_id}/usage',
24
+ 'POST /auth/orgs/{org_id}/domain',
25
+ 'GET /auth/orgs/{org_id}/domain',
26
+ 'DELETE /auth/orgs/{org_id}/domain',
27
+ ];
28
+ const SUBCOMMAND_USAGE = {
29
+ 'tenant': `myapi auth tenant [show|create] [--connections <list>] [--theme <json>] [--org <id>] [--json]
30
+
31
+ Your org's OIDC auth tenant — the identity provider your app's end users sign
32
+ in against. One per org.
33
+
34
+ myapi auth tenant Show the tenant (issuer + hosted login URL)
35
+ myapi auth tenant create Create/enable it (idempotent)
36
+
37
+ --connections <list> Sign-in methods on the hosted login page, comma-separated:
38
+ google,password,magic (default: google).
39
+ --theme <json> Branding JSON for the hosted login page.`,
40
+ 'usage': `myapi auth usage [--org <id>] [--json]
41
+
42
+ Monthly active users (MAU) for the current period — auth is billed per MAU.`,
43
+ 'domain': `myapi auth domain [show|set|delete] [--domain <host>] [--org <id>] [--json]
44
+
45
+ Serve auth on your own domain (e.g. auth.acme.com) instead of the default issuer.
46
+
47
+ myapi auth domain Show the current custom domain + status
48
+ myapi auth domain set --domain auth.acme.com Register it (prints the DNS record to create)
49
+ myapi auth domain delete Remove the custom domain
50
+
51
+ After 'set', create the printed DNS A record; TLS provisions automatically
52
+ (~30 min) and the domain becomes your issuer once active.`,
53
+ 'client': `myapi auth client <list|create> [--org <id>] [--json]
54
+
55
+ OIDC clients are the apps that authenticate against your tenant.
56
+
57
+ myapi auth client list
58
+ myapi auth client create --name "My App" --type spa --redirect https://app.example.com/callback
59
+
60
+ --name <n> Human label for the app (required)
61
+ --type spa|web spa = public (no secret); web = confidential (secret once)
62
+ --redirect <urls> Allowed redirect URIs, comma-separated (required).
63
+ Absolute https (or http://localhost for dev).`,
64
+ };
65
+ export const HELP = `Usage: myapi auth <subcommand>
66
+
67
+ Authentication for your app's end users — a managed OIDC identity provider
68
+ (à la Kinde/Auth0). One auth tenant per org; register OIDC clients (apps)
69
+ against it; sign users in via the hosted login page or the JS SDK; verify
70
+ tokens locally against the tenant JWKS.
71
+
72
+ End users sign in with managed Google, email/password, or magic links — pick
73
+ which via \`tenant create --connections\`. (Operator/account commands moved to
74
+ \`myapi account\`.)
75
+
76
+ Subcommands:
77
+ client Register and list OIDC clients (your apps)
78
+ domain Serve auth on your own domain (auth.acme.com)
79
+ tenant Show or create your org's OIDC auth tenant (+ sign-in methods)
80
+ usage Monthly active users (MAU) for the current period`;
81
+ export async function run(subcommand, args, flags) {
82
+ if (!subcommand || (flags.help && !subcommand)) {
83
+ info(HELP);
84
+ return;
85
+ }
86
+ if (flags.help) {
87
+ const usage = SUBCOMMAND_USAGE[subcommand];
88
+ info(usage ? `Usage: ${usage}` : `Unknown subcommand "${subcommand}". Run "myapi auth --help".`);
89
+ return;
90
+ }
91
+ switch (subcommand) {
92
+ case 'tenant': return tenant(args, flags);
93
+ case 'client': return client(args, flags);
94
+ case 'usage': return usage(flags);
95
+ case 'domain': return domain(args, flags);
96
+ default: error(`Unknown subcommand "${subcommand}". Run "myapi auth --help" for tenant/client/usage/domain.`);
97
+ }
98
+ }
99
+ async function tenant(args, flags) {
100
+ const action = args[0] || 'show';
101
+ const config = requireConfig();
102
+ const orgId = requireOrg(flags, config, 'myapi auth tenant [show|create] [--org <id>]');
103
+ if (action === 'show') {
104
+ try {
105
+ const t = await sdkAuth.getTenant(config.api_key, orgId);
106
+ if (flags.json) {
107
+ printJson(t);
108
+ return;
109
+ }
110
+ info(`Auth tenant: ${t.tenant_id}`);
111
+ info(`Issuer: ${t.issuer}`);
112
+ info(`Login URL: ${t.login_url}`);
113
+ if (t.connections?.length)
114
+ info(`Sign-in: ${t.connections.join(', ')}`);
115
+ }
116
+ catch (e) {
117
+ if (e?.code === 'TENANT_NOT_FOUND' || e?.status === 404) {
118
+ error('No auth tenant yet for this org.\nCreate it with: myapi auth tenant create');
119
+ }
120
+ throw e;
121
+ }
122
+ return;
123
+ }
124
+ if (action === 'create' || action === 'init') {
125
+ let theme;
126
+ if (flags.theme) {
127
+ try {
128
+ theme = JSON.parse(flags.theme);
129
+ }
130
+ catch {
131
+ error('--theme must be valid JSON');
132
+ }
133
+ }
134
+ let connections;
135
+ if (flags.connections) {
136
+ const allowed = ['google', 'password', 'magic'];
137
+ connections = flags.connections.split(',').map(s => s.trim()).filter(Boolean);
138
+ const bad = connections.filter(c => !allowed.includes(c));
139
+ if (bad.length)
140
+ error(`--connections may only contain google, password, magic (got: ${bad.join(', ')})`);
141
+ }
142
+ const t = await sdkAuth.createTenant(config.api_key, orgId, { theme, connections });
143
+ if (flags.json) {
144
+ printJson(t);
145
+ return;
146
+ }
147
+ success(`Auth tenant ready: ${t.tenant_id}`);
148
+ info(`Issuer: ${t.issuer}`);
149
+ info(`Login URL: ${t.login_url}`);
150
+ if (t.connections?.length)
151
+ info(`Sign-in: ${t.connections.join(', ')}`);
152
+ info('Next: register an app → myapi auth client create --name <n> --type spa --redirect <url>');
153
+ return;
154
+ }
155
+ error(`Unknown action "${action}". Use: myapi auth tenant [show|create]`);
156
+ }
157
+ async function usage(flags) {
158
+ const config = requireConfig();
159
+ const orgId = requireOrg(flags, config, 'myapi auth usage [--org <id>]');
160
+ const u = await sdkAuth.getUsage(config.api_key, orgId);
161
+ if (flags.json) {
162
+ printJson(u);
163
+ return;
164
+ }
165
+ info(`Period: ${u.period}`);
166
+ info(`Active users: ${u.active_users} MAU`);
167
+ info(`Price: $${(u.price_cents_each / 100).toFixed(2)} per MAU`);
168
+ }
169
+ async function domain(args, flags) {
170
+ const action = args[0] || 'show';
171
+ const config = requireConfig();
172
+ const orgId = requireOrg(flags, config, 'myapi auth domain [show|set|delete] [--org <id>]');
173
+ const showDomain = (d) => {
174
+ info(`Domain: ${d.domain}`);
175
+ info(`Status: ${d.status}`);
176
+ if (d.status !== 'active') {
177
+ info('');
178
+ info(`DNS — create this record:`);
179
+ info(` ${d.dns.type} ${d.dns.name} → ${d.dns.value}`);
180
+ if (d.next)
181
+ info(`\n${d.next}`);
182
+ }
183
+ else {
184
+ info(`Issuer: ${d.issuer}`);
185
+ info(`Login URL: ${d.login_url}`);
186
+ }
187
+ };
188
+ if (action === 'show') {
189
+ try {
190
+ const d = await sdkAuth.getDomain(config.api_key, orgId);
191
+ if (flags.json) {
192
+ printJson(d);
193
+ return;
194
+ }
195
+ showDomain(d);
196
+ }
197
+ catch (e) {
198
+ if (e?.code === 'NO_DOMAIN' || e?.status === 404) {
199
+ error('No custom auth domain set.\nSet one with: myapi auth domain set --domain auth.example.com');
200
+ }
201
+ throw e;
202
+ }
203
+ return;
204
+ }
205
+ if (action === 'set' || action === 'register') {
206
+ const host = flags.domain || args[1] || '';
207
+ if (!host)
208
+ error('--domain is required (e.g. --domain auth.example.com)');
209
+ const d = await sdkAuth.registerDomain(config.api_key, orgId, host);
210
+ if (flags.json) {
211
+ printJson(d);
212
+ return;
213
+ }
214
+ success(`Custom auth domain registered: ${d.domain} (${d.status})`);
215
+ showDomain(d);
216
+ return;
217
+ }
218
+ if (action === 'delete' || action === 'remove') {
219
+ const host = flags.domain || args[1];
220
+ let target = host;
221
+ if (!target) {
222
+ // No host given — resolve the current one so the user doesn't have to retype it.
223
+ try {
224
+ target = (await sdkAuth.getDomain(config.api_key, orgId)).domain;
225
+ }
226
+ catch {
227
+ error('No custom auth domain to delete.');
228
+ }
229
+ }
230
+ await sdkAuth.deleteDomain(config.api_key, orgId, target);
231
+ success(`Custom auth domain removed: ${target}`);
232
+ return;
233
+ }
234
+ error(`Unknown action "${action}". Use: myapi auth domain [show|set|delete]`);
235
+ }
236
+ async function client(args, flags) {
237
+ const action = args[0] || 'list';
238
+ const config = requireConfig();
239
+ const orgId = requireOrg(flags, config, 'myapi auth client <list|create> [--org <id>]');
240
+ if (action === 'list') {
241
+ const res = await sdkAuth.listClients(config.api_key, orgId);
242
+ if (flags.json) {
243
+ printJson(res);
244
+ return;
245
+ }
246
+ printTable((res.clients || []).map(c => ({
247
+ ClientID: c.client_id,
248
+ Name: c.name || '',
249
+ Type: c.type,
250
+ Redirects: (c.redirect_uris || []).join(', '),
251
+ })), {
252
+ flags,
253
+ empty: 'No OIDC clients yet.\nCreate one: myapi auth client create --name <n> --type spa --redirect <url>',
254
+ });
255
+ return;
256
+ }
257
+ if (action === 'create') {
258
+ const name = flags.name || '';
259
+ const type = flags.type;
260
+ const redirect = flags.redirect || '';
261
+ if (!name)
262
+ error('--name is required (e.g. --name "My App")');
263
+ if (type !== 'spa' && type !== 'web')
264
+ error("--type must be 'spa' (public) or 'web' (confidential)");
265
+ if (!redirect)
266
+ error('--redirect is required (comma-separate multiple URIs)');
267
+ const redirect_uris = redirect.split(',').map(s => s.trim()).filter(Boolean);
268
+ const c = await sdkAuth.createClient(config.api_key, orgId, { name, type: type, redirect_uris });
269
+ if (flags.json) {
270
+ printJson(c);
271
+ return;
272
+ }
273
+ success(`Client created: ${c.client_id}`);
274
+ info(`Type: ${c.type}`);
275
+ info(`Redirects: ${(c.redirect_uris || []).join(', ')}`);
276
+ if (c.issuer)
277
+ info(`Issuer: ${c.issuer}`);
278
+ if (c.client_secret) {
279
+ info('');
280
+ info('Client secret (shown ONCE — store it now, it cannot be retrieved again):');
281
+ info(` ${c.client_secret}`);
282
+ }
283
+ return;
284
+ }
285
+ error(`Unknown action "${action}". Use: myapi auth client <list|create>`);
286
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,103 @@
1
+ // Tests for the auto-recharge client surface: the SDK `withFundsRetry`
2
+ // poll-and-retry helper + the CLI's `autoRechargeSummary` render helper.
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import { MyApiError, withFundsRetry, isInsufficientFunds, isSpendCapExceeded, autoRechargeState } from '@myapihq/sdk';
5
+ import { autoRechargeSummary } from './billing.js';
6
+ // Build a 402 the way client.ts does — body is the unwrapped `error` object.
7
+ function funds(state, retryAfter) {
8
+ const body = { code: 'INSUFFICIENT_FUNDS', auto_recharge: state };
9
+ if (retryAfter != null)
10
+ body.retry_after_seconds = retryAfter;
11
+ return new MyApiError('INSUFFICIENT_FUNDS', 402, 'insufficient balance', body);
12
+ }
13
+ const noopSleep = () => Promise.resolve();
14
+ describe('withFundsRetry', () => {
15
+ it('returns the value when the call succeeds first try (no retry)', async () => {
16
+ const call = vi.fn().mockResolvedValue('ok');
17
+ await expect(withFundsRetry(call, { sleep: noopSleep })).resolves.toBe('ok');
18
+ expect(call).toHaveBeenCalledTimes(1);
19
+ });
20
+ it('retries on an in-flight refill, then succeeds', async () => {
21
+ const call = vi.fn()
22
+ .mockRejectedValueOnce(funds('in_flight', 0))
23
+ .mockResolvedValueOnce('ok');
24
+ const sleep = vi.fn(noopSleep);
25
+ await expect(withFundsRetry(call, { sleep })).resolves.toBe('ok');
26
+ expect(call).toHaveBeenCalledTimes(2);
27
+ expect(sleep).toHaveBeenCalledTimes(1);
28
+ });
29
+ it('honors the server retry_after_seconds for the sleep duration', async () => {
30
+ const call = vi.fn()
31
+ .mockRejectedValueOnce(funds('in_flight', 7))
32
+ .mockResolvedValueOnce('ok');
33
+ const sleep = vi.fn(noopSleep);
34
+ await withFundsRetry(call, { sleep });
35
+ expect(sleep).toHaveBeenCalledWith(7000);
36
+ });
37
+ it('gives up after maxRetries and rethrows the last error', async () => {
38
+ const call = vi.fn().mockRejectedValue(funds('in_flight', 0));
39
+ await expect(withFundsRetry(call, { maxRetries: 2, sleep: noopSleep }))
40
+ .rejects.toMatchObject({ code: 'INSUFFICIENT_FUNDS' });
41
+ expect(call).toHaveBeenCalledTimes(3); // initial + 2 retries
42
+ });
43
+ it('does NOT retry a capped refill — rethrows immediately (needs a human)', async () => {
44
+ const call = vi.fn().mockRejectedValue(funds('capped'));
45
+ await expect(withFundsRetry(call, { sleep: noopSleep })).rejects.toMatchObject({ code: 'INSUFFICIENT_FUNDS' });
46
+ expect(call).toHaveBeenCalledTimes(1);
47
+ });
48
+ it('does NOT retry no_pm / failed / disabled states', async () => {
49
+ for (const state of ['no_pm', 'failed', 'disabled']) {
50
+ const call = vi.fn().mockRejectedValue(funds(state));
51
+ await expect(withFundsRetry(call, { sleep: noopSleep })).rejects.toBeInstanceOf(MyApiError);
52
+ expect(call).toHaveBeenCalledTimes(1);
53
+ }
54
+ });
55
+ it('does NOT retry a SPEND_CAP_EXCEEDED hard ceiling', async () => {
56
+ const err = new MyApiError('SPEND_CAP_EXCEEDED', 402, 'cap', { code: 'SPEND_CAP_EXCEEDED', cap_cents: 5000 });
57
+ const call = vi.fn().mockRejectedValue(err);
58
+ await expect(withFundsRetry(call, { sleep: noopSleep })).rejects.toBe(err);
59
+ expect(call).toHaveBeenCalledTimes(1);
60
+ });
61
+ it('rethrows non-funds errors untouched', async () => {
62
+ const err = new Error('network down');
63
+ const call = vi.fn().mockRejectedValue(err);
64
+ await expect(withFundsRetry(call, { sleep: noopSleep })).rejects.toBe(err);
65
+ expect(call).toHaveBeenCalledTimes(1);
66
+ });
67
+ });
68
+ describe('funds predicates', () => {
69
+ it('isInsufficientFunds matches both new and legacy codes', () => {
70
+ expect(isInsufficientFunds(funds('disabled'))).toBe(true);
71
+ expect(isInsufficientFunds(new MyApiError('INSUFFICIENT_BALANCE', 402, 'x', {}))).toBe(true);
72
+ expect(isInsufficientFunds(new MyApiError('SPEND_CAP_EXCEEDED', 402, 'x', {}))).toBe(false);
73
+ expect(isInsufficientFunds(new Error('x'))).toBe(false);
74
+ });
75
+ it('isSpendCapExceeded + autoRechargeState read the right fields', () => {
76
+ expect(isSpendCapExceeded(new MyApiError('SPEND_CAP_EXCEEDED', 402, 'x', {}))).toBe(true);
77
+ expect(autoRechargeState(funds('in_flight'))).toBe('in_flight');
78
+ expect(autoRechargeState(new Error('x'))).toBeUndefined();
79
+ });
80
+ });
81
+ describe('autoRechargeSummary', () => {
82
+ const base = {
83
+ enabled: true, threshold_cents: 500, amount_cents: 2000, monthly_cap_cents: 10000,
84
+ month_to_date_recharged_cents: 4000, has_payment_method: true,
85
+ last_recharge_status: 'succeeded', last_recharge_attempt_at: null,
86
+ };
87
+ it('returns null when disabled', () => {
88
+ expect(autoRechargeSummary({ ...base, enabled: false })).toBeNull();
89
+ });
90
+ it('summarizes an enabled config with dollar amounts', () => {
91
+ const s = autoRechargeSummary(base);
92
+ expect(s).toContain('$5.00');
93
+ expect(s).toContain('$20.00');
94
+ expect(s).toContain('$40.00'); // month-to-date
95
+ expect(s).toContain('$100.00'); // cap
96
+ });
97
+ it('flags a failed/capped status but not a healthy/pending one', () => {
98
+ expect(autoRechargeSummary({ ...base, last_recharge_status: 'failed' })).toContain('⚠ failed');
99
+ expect(autoRechargeSummary({ ...base, last_recharge_status: 'capped' })).toContain('⚠ capped');
100
+ expect(autoRechargeSummary({ ...base, last_recharge_status: 'pending' })).not.toContain('⚠');
101
+ expect(autoRechargeSummary(base)).not.toContain('⚠');
102
+ });
103
+ });
@@ -1,3 +1,4 @@
1
+ import { hq } from '@myapihq/sdk';
1
2
  import type { FlagSchema } from '../flags.js';
2
3
  import type { Flags } from '../helpers.js';
3
4
  import type { Exposes } from '../exposes.js';
@@ -9,4 +10,6 @@ export declare function history(flags: Flags): Promise<void>;
9
10
  export declare function usage(flags: Flags): Promise<void>;
10
11
  export declare function topup(amountStr: string, flags: Flags): Promise<void>;
11
12
  export declare function setup(_flags: Flags): Promise<void>;
13
+ export declare function autoRechargeSummary(cfg: hq.AutoRechargeConfig): string | null;
14
+ export declare function autoRecharge(action: string | undefined, flags: Flags): Promise<void>;
12
15
  export declare function spendCap(arg: string | undefined, flags: Flags): Promise<void>;
@@ -5,6 +5,9 @@ import { confirm, isNonInteractive } from '../prompt.js';
5
5
  import { formatDate } from '../utils.js';
6
6
  export const SCHEMA = {
7
7
  period: 'string', // spend-cap window: month | day
8
+ threshold: 'string', // auto-recharge: refill when balance drops below ($)
9
+ amount: 'string', // auto-recharge: how much to refill each time ($)
10
+ 'monthly-cap': 'string', // auto-recharge: max auto-recharged per month ($)
8
11
  };
9
12
  export const EXPOSES = [
10
13
  'GET /hq/billing/balance',
@@ -14,8 +17,26 @@ export const EXPOSES = [
14
17
  'POST /hq/billing/topup',
15
18
  'GET /hq/account/me',
16
19
  'PATCH /hq/account/spend-cap',
20
+ 'GET /hq/billing/auto-recharge',
21
+ 'PUT /hq/billing/auto-recharge',
22
+ 'DELETE /hq/billing/auto-recharge',
17
23
  ];
18
24
  const SUBCOMMAND_USAGE = {
25
+ 'auto-recharge': `myapi billing auto-recharge [show | set | disable]
26
+
27
+ Keep the prepaid wallet funded without a human in the loop: when the balance
28
+ drops below the threshold, MyAPI charges your saved card to refill it, bounded
29
+ by a monthly cap. Off by default; enabling needs a payment method on file.
30
+
31
+ myapi billing auto-recharge Show current config + status
32
+ myapi billing auto-recharge set \\
33
+ --threshold 5 --amount 20 --monthly-cap 100
34
+ Refill to keep ≥$5, $20 at a time,
35
+ up to $100/month
36
+ myapi billing auto-recharge disable Turn off (settings are kept)
37
+
38
+ Amounts are whole dollars. The refill amount must be ≥ $5 and ≥ the threshold;
39
+ the monthly cap must be ≥ the refill amount.`,
19
40
  'balance': 'myapi billing balance [--json]',
20
41
  'history': 'myapi billing history [--json]',
21
42
  'usage': `myapi billing usage [--period month|30d] [--json]
@@ -48,6 +69,7 @@ export async function run(subcommand, args, flags) {
48
69
  info(`Usage: myapi billing <subcommand>
49
70
 
50
71
  Subcommands:
72
+ auto-recharge Keep the wallet funded automatically (show/set/disable)
51
73
  balance Check balance, credits, and payment method status
52
74
  history View recent transactions and top-ups
53
75
  setup Open a checkout link to add or update payment method
@@ -71,6 +93,7 @@ Subcommands:
71
93
  case 'topup': return topup(args[0], flags);
72
94
  case 'setup': return setup(flags);
73
95
  case 'spend-cap': return spendCap(args[0], flags);
96
+ case 'auto-recharge': return autoRecharge(args[0], flags);
74
97
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi billing --help" for available subcommands.`);
75
98
  }
76
99
  }
@@ -92,7 +115,7 @@ export async function balance(flags) {
92
115
  // Anonymous accounts can't transact — paid surface is gated on a
93
116
  // verified email and credits only fund on upgrade. Surface the unblock.
94
117
  if (config.is_anonymous) {
95
- info('→ Link an email to unlock $5 free credit + paid actions: myapi auth link <email>');
118
+ info('→ Link an email to unlock $5 free credit + paid actions: myapi account link <email>');
96
119
  }
97
120
  }
98
121
  export async function history(flags) {
@@ -165,6 +188,98 @@ export async function setup(_flags) {
165
188
  const result = await hq.setupPayment(config.api_key);
166
189
  success(`Open this URL in your browser to set up payment:\n${result.url}`);
167
190
  }
191
+ function fmtCents(c) {
192
+ return c == null ? '—' : `$${(c / 100).toFixed(2)}`;
193
+ }
194
+ // One-line summary of auto-recharge state, reused by `billing auto-recharge`
195
+ // and `status`. Returns null when off (callers decide whether to show "off").
196
+ export function autoRechargeSummary(cfg) {
197
+ if (!cfg.enabled)
198
+ return null;
199
+ let s = `on — refill to ≥ ${fmtCents(cfg.threshold_cents)}, ${fmtCents(cfg.amount_cents)} each (${fmtCents(cfg.month_to_date_recharged_cents)}/${fmtCents(cfg.monthly_cap_cents)} this month)`;
200
+ if (cfg.last_recharge_status && cfg.last_recharge_status !== 'succeeded' && cfg.last_recharge_status !== 'pending') {
201
+ s += ` ⚠ ${cfg.last_recharge_status}`;
202
+ }
203
+ return s;
204
+ }
205
+ const RECHARGE_STATUS_HINT = {
206
+ failed: 'last refill was declined — check your card: myapi billing setup',
207
+ capped: 'monthly cap reached — refills paused until next month, or raise --monthly-cap',
208
+ no_pm: 'no payment method — add one: myapi billing setup',
209
+ pending: 'a refill is in progress',
210
+ };
211
+ function renderAutoRecharge(cfg) {
212
+ if (!cfg.enabled) {
213
+ info('Auto-recharge: off');
214
+ info(` Payment method on file: ${cfg.has_payment_method ? 'yes' : 'no'}`);
215
+ info(' Enable with: myapi billing auto-recharge set --threshold <$> --amount <$> --monthly-cap <$>');
216
+ if (!cfg.has_payment_method)
217
+ info(' (add a payment method first: myapi billing setup)');
218
+ return;
219
+ }
220
+ info('Auto-recharge: on');
221
+ info(` Refill to keep balance ≥ ${fmtCents(cfg.threshold_cents)}, adding ${fmtCents(cfg.amount_cents)} each time`);
222
+ info(` Monthly cap: ${fmtCents(cfg.month_to_date_recharged_cents)} of ${fmtCents(cfg.monthly_cap_cents)} used this month`);
223
+ info(` Payment method on file: ${cfg.has_payment_method ? 'yes' : 'no'}`);
224
+ if (cfg.last_recharge_status && cfg.last_recharge_status !== 'succeeded') {
225
+ const hint = RECHARGE_STATUS_HINT[cfg.last_recharge_status];
226
+ info(` ⚠ Status: ${cfg.last_recharge_status}${hint ? ` — ${hint}` : ''}`);
227
+ }
228
+ }
229
+ // Keep the wallet funded without a human. No arg / "show" → display config;
230
+ // "set" → enable/update from --threshold/--amount/--monthly-cap (whole
231
+ // dollars); "disable" → turn off (settings preserved for easy re-enable).
232
+ export async function autoRecharge(action, flags) {
233
+ const config = requireConfig();
234
+ const act = action ?? 'show';
235
+ if (act === 'show') {
236
+ const cfg = await hq.getAutoRecharge(config.api_key);
237
+ if (flags.json) {
238
+ printJson(cfg);
239
+ return;
240
+ }
241
+ renderAutoRecharge(cfg);
242
+ return;
243
+ }
244
+ if (act === 'disable') {
245
+ await hq.disableAutoRecharge(config.api_key);
246
+ success('Auto-recharge disabled. Your threshold/amount/cap are kept — re-enable with: myapi billing auto-recharge set');
247
+ return;
248
+ }
249
+ if (act === 'set') {
250
+ const dollarsToCents = (v, name) => {
251
+ if (v == null)
252
+ return undefined;
253
+ const n = Number(v);
254
+ if (!Number.isFinite(n) || n < 0) {
255
+ error(`Invalid --${name} "${v}". Use a whole dollar amount (e.g. --${name} 20).`);
256
+ }
257
+ return Math.round(n * 100);
258
+ };
259
+ const input = { enabled: true };
260
+ const threshold = dollarsToCents(flags.threshold, 'threshold');
261
+ const amount = dollarsToCents(flags.amount, 'amount');
262
+ const cap = dollarsToCents(flags['monthly-cap'], 'monthly-cap');
263
+ if (threshold != null)
264
+ input.threshold_cents = threshold;
265
+ if (amount != null)
266
+ input.amount_cents = amount;
267
+ if (cap != null)
268
+ input.monthly_cap_cents = cap;
269
+ // The backend enforces the invariants (≥$5 floor, amount ≥ threshold,
270
+ // cap ≥ amount, payment method present) and returns a 400 the top-level
271
+ // handler renders — no need to duplicate that validation here.
272
+ const cfg = await hq.setAutoRecharge(config.api_key, input);
273
+ if (flags.json) {
274
+ printJson(cfg);
275
+ return;
276
+ }
277
+ success('Auto-recharge enabled.');
278
+ renderAutoRecharge(cfg);
279
+ return;
280
+ }
281
+ error(`Unknown action "${act}". Use: myapi billing auto-recharge [show | set | disable]`);
282
+ }
168
283
  // The account-level spend ceiling. No arg → show; "clear" → remove; a
169
284
  // dollar amount → set. Distinct from per-key caps (myapi keys create
170
285
  // --spend-cap): this is the aggregate backstop across the whole account.
@@ -49,7 +49,7 @@ export async function setFunnel(id, _flags, via = 'auth config') {
49
49
  const config = requireConfig();
50
50
  const orgId = config.default_org;
51
51
  if (!orgId)
52
- error('No default organization set. Run: myapi auth config set-org <id>');
52
+ error('No default organization set. Run: myapi account config set-org <id>');
53
53
  info('› Validating…');
54
54
  const { funnel } = await sdkFunnel.getFunnel(config.api_key, orgId, id);
55
55
  const funnelId = funnel.id;
@@ -11,6 +11,7 @@ export declare function create(flags: Flags): Promise<void>;
11
11
  export declare function list(flags: Flags): Promise<void>;
12
12
  export declare function get(id: string, flags: Flags): Promise<void>;
13
13
  export declare function del(id: string, flags: Flags): Promise<void>;
14
+ export declare function _isTarball(p: string): boolean;
14
15
  export declare function deploy(id: string, image: string, flags: Flags): Promise<void>;
15
16
  export declare function logs(id: string, flags: Flags): Promise<void>;
16
17
  export declare function domain(id: string, domainArg: string | undefined, flags: Flags): Promise<void>;