@myapihq/cli 1.2.5 → 1.2.7

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 (42) hide show
  1. package/dist/commands/billing.d.ts +2 -0
  2. package/dist/commands/billing.js +94 -2
  3. package/dist/commands/container-validation.test.d.ts +1 -0
  4. package/dist/commands/container-validation.test.js +45 -0
  5. package/dist/commands/container.d.ts +15 -0
  6. package/dist/commands/container.js +242 -0
  7. package/dist/commands/domain.js +5 -0
  8. package/dist/commands/email/campaign.js +16 -1
  9. package/dist/commands/email/message.js +26 -3
  10. package/dist/commands/email/template.js +8 -1
  11. package/dist/commands/fn.d.ts +3 -0
  12. package/dist/commands/fn.js +115 -24
  13. package/dist/commands/funnel.d.ts +1 -0
  14. package/dist/commands/funnel.js +83 -0
  15. package/dist/commands/keys-validation.test.d.ts +1 -0
  16. package/dist/commands/keys-validation.test.js +87 -0
  17. package/dist/commands/keys.d.ts +6 -2
  18. package/dist/commands/keys.js +189 -55
  19. package/dist/commands/payments-validation.test.d.ts +1 -0
  20. package/dist/commands/payments-validation.test.js +31 -0
  21. package/dist/commands/payments.d.ts +13 -0
  22. package/dist/commands/payments.js +219 -0
  23. package/dist/commands/pixel.js +15 -2
  24. package/dist/commands/update.d.ts +1 -1
  25. package/dist/commands/update.js +68 -45
  26. package/dist/commands/webhook.js +10 -1
  27. package/dist/completion.d.ts +3 -1
  28. package/dist/completion.js +160 -55
  29. package/dist/exposes.test.js +2 -0
  30. package/dist/index.js +34 -13
  31. package/dist/sdk-billing-usage.test.d.ts +1 -0
  32. package/dist/sdk-billing-usage.test.js +74 -0
  33. package/dist/sdk-container.test.d.ts +1 -0
  34. package/dist/sdk-container.test.js +115 -0
  35. package/dist/sdk-function.test.js +73 -6
  36. package/dist/sdk-funnel-publish.test.d.ts +1 -0
  37. package/dist/sdk-funnel-publish.test.js +89 -0
  38. package/dist/sdk-iam.test.d.ts +1 -0
  39. package/dist/sdk-iam.test.js +190 -0
  40. package/dist/sdk-payments.test.d.ts +1 -0
  41. package/dist/sdk-payments.test.js +139 -0
  42. package/package.json +4 -4
@@ -6,5 +6,7 @@ export declare const EXPOSES: Exposes;
6
6
  export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
7
7
  export declare function balance(flags: Flags): Promise<void>;
8
8
  export declare function history(flags: Flags): Promise<void>;
9
+ export declare function usage(flags: Flags): Promise<void>;
9
10
  export declare function topup(amountStr: string, flags: Flags): Promise<void>;
10
11
  export declare function setup(_flags: Flags): Promise<void>;
12
+ export declare function spendCap(arg: string | undefined, flags: Flags): Promise<void>;
@@ -3,16 +3,26 @@ import { requireConfig } from '../config.js';
3
3
  import { success, error, printTable, info, printJson } from '../output.js';
4
4
  import { confirm, isNonInteractive } from '../prompt.js';
5
5
  import { formatDate } from '../utils.js';
6
- export const SCHEMA = {};
6
+ export const SCHEMA = {
7
+ period: 'string', // spend-cap window: month | day
8
+ };
7
9
  export const EXPOSES = [
8
10
  'GET /hq/billing/balance',
9
11
  'GET /hq/billing/history',
12
+ 'GET /hq/billing/usage',
10
13
  'POST /hq/billing/setup-payment',
11
14
  'POST /hq/billing/topup',
15
+ 'GET /hq/account/me',
16
+ 'PATCH /hq/account/spend-cap',
12
17
  ];
13
18
  const SUBCOMMAND_USAGE = {
14
19
  'balance': 'myapi billing balance [--json]',
15
20
  'history': 'myapi billing history [--json]',
21
+ 'usage': `myapi billing usage [--period month|30d] [--json]
22
+
23
+ Spend rolled up by service — the accurate "where is my money going" view.
24
+ Aggregates every billing event, unlike the flat history log. Defaults to
25
+ the current calendar month; --period 30d gives the trailing 30 days.`,
16
26
  'topup': `myapi billing topup <amount> [--yes]
17
27
 
18
28
  Amount is in whole dollars (e.g. "10" charges $10).
@@ -20,6 +30,18 @@ Confirmation is required for amounts of $50 or more — pass --yes to skip it.
20
30
 
21
31
  Example: myapi billing topup 10`,
22
32
  'setup': 'myapi billing setup',
33
+ 'spend-cap': `myapi billing spend-cap [<amount> | clear] [--period month|day]
34
+
35
+ The account-level spend ceiling (IAM "Layer 2") — a self-imposed limit
36
+ *below* your balance that bounds total spend across every key and function.
37
+
38
+ myapi billing spend-cap Show the current cap + period spend
39
+ myapi billing spend-cap 50 Cap total spend at $50/month
40
+ myapi billing spend-cap 5 --period day Cap at $5/day
41
+ myapi billing spend-cap clear Remove the cap
42
+
43
+ This is distinct from a per-key cap (myapi keys create --spend-cap). The
44
+ account cap is the aggregate backstop; per-key caps bound each credential.`,
23
45
  };
24
46
  export async function run(subcommand, args, flags) {
25
47
  if (!subcommand || (flags.help && !subcommand)) {
@@ -28,8 +50,10 @@ export async function run(subcommand, args, flags) {
28
50
  Subcommands:
29
51
  balance Check balance, credits, and payment method status
30
52
  history View recent transactions and top-ups
53
+ usage Spend rolled up by service (this month, or --period 30d)
31
54
  topup Top up your balance (whole dollars)
32
- setup Open a checkout link to add or update payment method`);
55
+ setup Open a checkout link to add or update payment method
56
+ spend-cap Set/show/clear the account-level spend ceiling (IAM Layer 2)`);
33
57
  return;
34
58
  }
35
59
  if (flags.help) {
@@ -43,8 +67,10 @@ Subcommands:
43
67
  switch (subcommand) {
44
68
  case 'balance': return balance(flags);
45
69
  case 'history': return history(flags);
70
+ case 'usage': return usage(flags);
46
71
  case 'topup': return topup(args[0], flags);
47
72
  case 'setup': return setup(flags);
73
+ case 'spend-cap': return spendCap(args[0], flags);
48
74
  default: error(`Unknown subcommand: ${subcommand}. Run "myapi billing --help" for available subcommands.`);
49
75
  }
50
76
  }
@@ -87,6 +113,31 @@ export async function history(flags) {
87
113
  empty: 'No transactions yet.',
88
114
  });
89
115
  }
116
+ // Spend rolled up by service. Aggregates every billing event over the
117
+ // window (current calendar month, or trailing 30 days with --period 30d) —
118
+ // the accurate "where is my money going" view, distinct from `history`.
119
+ export async function usage(flags) {
120
+ const config = requireConfig();
121
+ const period = flags.period || 'month';
122
+ if (period !== 'month' && period !== '30d') {
123
+ error(`Invalid --period "${period}". Use month or 30d.`);
124
+ }
125
+ const res = await hq.getBillingUsage(config.api_key, period);
126
+ if (flags.json) {
127
+ printJson(res);
128
+ return;
129
+ }
130
+ info(`Spend by service — ${res.period === '30d' ? 'last 30 days' : 'this month'} (since ${formatDate(res.since)})`);
131
+ printTable(res.services.map(s => ({
132
+ Service: s.service,
133
+ Requests: s.requests,
134
+ Cost: s.cost_display,
135
+ })), {
136
+ flags,
137
+ empty: 'No usage recorded in this window.',
138
+ });
139
+ info(`Total: ${res.total_display}`);
140
+ }
90
141
  export async function topup(amountStr, flags) {
91
142
  const amount = Math.round(parseFloat(amountStr));
92
143
  if (!amountStr || isNaN(amount) || amount <= 0) {
@@ -114,3 +165,44 @@ export async function setup(_flags) {
114
165
  const result = await hq.setupPayment(config.api_key);
115
166
  success(`Open this URL in your browser to set up payment:\n${result.url}`);
116
167
  }
168
+ // The account-level spend ceiling. No arg → show; "clear" → remove; a
169
+ // dollar amount → set. Distinct from per-key caps (myapi keys create
170
+ // --spend-cap): this is the aggregate backstop across the whole account.
171
+ export async function spendCap(arg, flags) {
172
+ const config = requireConfig();
173
+ if (!arg) {
174
+ const acct = await hq.getAccount(config.api_key);
175
+ if (flags.json) {
176
+ printJson({
177
+ spend_cap_cents: acct.spend_cap_cents ?? null,
178
+ current_period_spend_cents: acct.current_period_spend_cents ?? null,
179
+ });
180
+ return;
181
+ }
182
+ if (acct.spend_cap_cents == null) {
183
+ info('No account spend cap set.');
184
+ info('Set one with: myapi billing spend-cap <dollars> [--period month|day]');
185
+ }
186
+ else {
187
+ const spent = acct.current_period_spend_cents ?? 0;
188
+ info(`Account spend cap: $${(spent / 100).toFixed(2)} spent / $${(acct.spend_cap_cents / 100).toFixed(2)} this period`);
189
+ }
190
+ return;
191
+ }
192
+ if (arg === 'clear' || arg === 'none') {
193
+ await hq.setAccountSpendCap(config.api_key, null);
194
+ success('Account spend cap cleared.');
195
+ return;
196
+ }
197
+ const dollars = Number(arg);
198
+ if (!Number.isFinite(dollars) || dollars < 0) {
199
+ error(`Invalid amount "${arg}". Use a non-negative dollar amount, or "clear" to remove the cap.\nExample: myapi billing spend-cap 50`);
200
+ }
201
+ const period = flags.period || 'month';
202
+ if (period !== 'month' && period !== 'day') {
203
+ error(`Invalid --period "${period}". Use month or day.`);
204
+ }
205
+ const res = await hq.setAccountSpendCap(config.api_key, Math.round(dollars * 100), period);
206
+ const cents = res.spend_cap_cents ?? Math.round(dollars * 100);
207
+ success(`Account spend cap set: $${(cents / 100).toFixed(2)} per ${res.spend_cap_period}.`);
208
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,45 @@
1
+ // Unit tests for container.ts CLI's pure helpers — name validation
2
+ // (mirrors the backend regex/reserved set) and the --env K=V parser.
3
+ import { describe, it, expect } from 'vitest';
4
+ import { _validateName, _parseEnv, NAME_RE, RESERVED_NAMES } from './container.js';
5
+ describe('_validateName — pure helper', () => {
6
+ it.each(['a', 'api-server', 'worker-2', '0', 'a'.repeat(50)])('accepts %s', (name) => {
7
+ expect(_validateName(name)).toBeNull();
8
+ });
9
+ it.each([
10
+ ['empty', ''],
11
+ ['UPPERCASE', 'BAD'],
12
+ ['underscore', 'my_app'],
13
+ ['leading hyphen', '-app'],
14
+ ['too long', 'a'.repeat(51)],
15
+ ['space', 'my app'],
16
+ ])('rejects %s', (_label, name) => {
17
+ expect(_validateName(name)).toMatch(/Invalid --name/);
18
+ });
19
+ it.each([...RESERVED_NAMES])('rejects reserved name %s', (name) => {
20
+ expect(_validateName(name)).toMatch(/is reserved/);
21
+ });
22
+ it('matches the backend regex exactly', () => {
23
+ expect(NAME_RE.source).toBe('^[a-z0-9][a-z0-9-]{0,49}$');
24
+ });
25
+ });
26
+ describe('_parseEnv — pure helper', () => {
27
+ it('parses a single KEY=VALUE pair', () => {
28
+ expect(_parseEnv('FOO=bar')).toEqual({ FOO: 'bar' });
29
+ });
30
+ it('parses comma-separated pairs', () => {
31
+ expect(_parseEnv('FOO=bar,BAZ=qux')).toEqual({ FOO: 'bar', BAZ: 'qux' });
32
+ });
33
+ it('keeps = inside the value', () => {
34
+ expect(_parseEnv('URL=https://x?a=1')).toEqual({ URL: 'https://x?a=1' });
35
+ });
36
+ it('tolerates surrounding whitespace and empty segments', () => {
37
+ expect(_parseEnv(' FOO=bar , ')).toEqual({ FOO: 'bar' });
38
+ });
39
+ it.each([
40
+ ['no equals', 'FOObar'],
41
+ ['leading equals (empty key)', '=bar'],
42
+ ])('rejects %s', (_label, raw) => {
43
+ expect(_parseEnv(raw)).toMatch(/Invalid --env entry/);
44
+ });
45
+ });
@@ -0,0 +1,15 @@
1
+ import type { FlagSchema } from '../flags.js';
2
+ import { type Flags } from '../helpers.js';
3
+ import type { Exposes } from '../exposes.js';
4
+ export declare const EXPOSES: Exposes;
5
+ export declare const SCHEMA: FlagSchema;
6
+ export declare const NAME_RE: RegExp;
7
+ export declare const RESERVED_NAMES: Set<string>;
8
+ export declare function _validateName(name: string): string | null;
9
+ export declare function _parseEnv(raw: string): Record<string, string> | string;
10
+ export declare function create(flags: Flags): Promise<void>;
11
+ export declare function list(flags: Flags): Promise<void>;
12
+ export declare function get(id: string, flags: Flags): Promise<void>;
13
+ export declare function del(id: string, flags: Flags): Promise<void>;
14
+ export declare function deploy(id: string, image: string, flags: Flags): Promise<void>;
15
+ export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -0,0 +1,242 @@
1
+ import { container as sdkContainer } from '@myapihq/sdk';
2
+ import { requireConfig } from '../config.js';
3
+ import { success, error, printTable, info, printJson, banner } from '../output.js';
4
+ import { formatDate } from '../utils.js';
5
+ import { requireOrg } from '../helpers.js';
6
+ export const EXPOSES = [
7
+ 'POST /container/orgs/{org_id}/containers',
8
+ 'GET /container/orgs/{org_id}/containers',
9
+ 'GET /container/orgs/{org_id}/containers/{id}',
10
+ 'DELETE /container/orgs/{org_id}/containers/{id}',
11
+ 'POST /container/orgs/{org_id}/containers/{id}/deploy',
12
+ ];
13
+ export const SCHEMA = {
14
+ name: 'string',
15
+ type: 'string',
16
+ cron: 'string',
17
+ cpu: 'string',
18
+ memory: 'string',
19
+ 'min-instances': 'number',
20
+ 'max-instances': 'number',
21
+ port: 'number',
22
+ env: 'string',
23
+ };
24
+ const CONTAINER_TYPES = ['service', 'worker', 'job'];
25
+ // Mirrors validateName in myapi-hq/internal/routes/container/crud.go —
26
+ // identical to functions. Client-side rejection so typos fail before the
27
+ // network call; the backend runs the same regex as defence in depth.
28
+ export const NAME_RE = /^[a-z0-9][a-z0-9-]{0,49}$/;
29
+ export const RESERVED_NAMES = new Set(['www', 'api', 'admin', 'system', 'default']);
30
+ // Returns an error message on failure, or null on success. Pure — no I/O,
31
+ // no process.exit. Tests use this form; the caller wraps it in error().
32
+ export function _validateName(name) {
33
+ if (!NAME_RE.test(name)) {
34
+ return `Invalid --name "${name}". Lowercase letters, digits, hyphens; 1-50 chars; starts with a letter or digit.`;
35
+ }
36
+ if (RESERVED_NAMES.has(name)) {
37
+ return `Name "${name}" is reserved. Pick a different one.`;
38
+ }
39
+ return null;
40
+ }
41
+ // Parses `--env K=V,K2=V2` into an object. Returns the map or an error
42
+ // message string (pure form, for tests).
43
+ export function _parseEnv(raw) {
44
+ const env = {};
45
+ for (const pair of raw.split(',').map(s => s.trim()).filter(Boolean)) {
46
+ const eq = pair.indexOf('=');
47
+ if (eq < 1) {
48
+ return `Invalid --env entry "${pair}". Use KEY=VALUE, comma-separated.`;
49
+ }
50
+ env[pair.slice(0, eq)] = pair.slice(eq + 1);
51
+ }
52
+ return env;
53
+ }
54
+ function summarizeContainer(c) {
55
+ return {
56
+ id: c.id,
57
+ name: c.name,
58
+ type: c.type,
59
+ status: c.status,
60
+ url: c.url || '(not deployed)',
61
+ updated_at: c.updated_at ? formatDate(c.updated_at) : '',
62
+ };
63
+ }
64
+ export async function create(flags) {
65
+ const config = requireConfig();
66
+ const orgId = requireOrg(flags, config, 'myapi container create --name <name> [--type service|worker|job] [--org <id>]');
67
+ const name = flags.name;
68
+ if (!name) {
69
+ error('Missing --name.\nUsage: myapi container create --name <name> [--type service|worker|job] [--org <id>]\n\n→ Name is a kebab-case slug, 1-50 chars (e.g. "my-worker").');
70
+ }
71
+ const nameErr = _validateName(name);
72
+ if (nameErr)
73
+ error(nameErr);
74
+ const type = flags.type ?? 'service';
75
+ if (!CONTAINER_TYPES.includes(type)) {
76
+ error(`Invalid --type "${type}". Use one of: ${CONTAINER_TYPES.join(', ')}.`);
77
+ }
78
+ const cron = flags.cron;
79
+ if (cron && type !== 'job') {
80
+ error('--cron is only valid for --type job (services and workers are always-on).');
81
+ }
82
+ const payload = { name, type: type };
83
+ if (cron)
84
+ payload.cron_schedule = cron;
85
+ if (typeof flags.cpu === 'string')
86
+ payload.cpu = flags.cpu;
87
+ if (typeof flags.memory === 'string')
88
+ payload.memory = flags.memory;
89
+ if (typeof flags['min-instances'] === 'number')
90
+ payload.min_instances = flags['min-instances'];
91
+ if (typeof flags['max-instances'] === 'number')
92
+ payload.max_instances = flags['max-instances'];
93
+ if (typeof flags.port === 'number')
94
+ payload.port = flags.port;
95
+ if (typeof flags.env === 'string') {
96
+ const env = _parseEnv(flags.env);
97
+ if (typeof env === 'string')
98
+ error(env);
99
+ payload.env = env;
100
+ }
101
+ const result = await sdkContainer.createContainer(config.api_key, orgId, payload);
102
+ success(`Container created: ${result.container.id}`);
103
+ info(`Name: ${result.container.name}`);
104
+ info(`Type: ${result.container.type}${result.container.cron_schedule ? ` (${result.container.cron_schedule})` : ''}`);
105
+ info(`Resources: ${result.container.cpu} CPU, ${result.container.memory}, instances ${result.container.min_instances}-${result.container.max_instances}`);
106
+ // The scoped key is returned ONCE — it's delivered to the running
107
+ // container as the MYAPI_KEY env var. Deploy rotates it.
108
+ info('');
109
+ info(`Scoped API key (returned once — save it if you need it):`);
110
+ info(` ${result.scoped_api_key}`);
111
+ if (!result.container.url) {
112
+ info('');
113
+ banner(`Next: deploy an image with myapi container deploy ${result.container.id} <image-ref>`);
114
+ }
115
+ }
116
+ export async function list(flags) {
117
+ const config = requireConfig();
118
+ const orgId = requireOrg(flags, config, 'myapi container list [--org <id>]');
119
+ const containers = await sdkContainer.listContainers(config.api_key, orgId);
120
+ if (flags.json) {
121
+ printJson(containers);
122
+ return;
123
+ }
124
+ printTable(containers.map(summarizeContainer), {
125
+ flags,
126
+ empty: 'No containers yet. Create one with: myapi container create --name <name>',
127
+ });
128
+ }
129
+ export async function get(id, flags) {
130
+ const config = requireConfig();
131
+ const orgId = requireOrg(flags, config, 'myapi container get <id> [--org <id>]');
132
+ if (!id)
133
+ error('Missing id.\nUsage: myapi container get <id>');
134
+ const c = await sdkContainer.getContainer(config.api_key, orgId, id);
135
+ if (flags.json) {
136
+ printJson(c);
137
+ return;
138
+ }
139
+ info(`ID: ${c.id}`);
140
+ info(`Name: ${c.name}`);
141
+ info(`Type: ${c.type}${c.cron_schedule ? ` (${c.cron_schedule})` : ''}`);
142
+ info(`Status: ${c.status}`);
143
+ info(`Resources: ${c.cpu} CPU, ${c.memory}, instances ${c.min_instances}-${c.max_instances}`);
144
+ if (c.port)
145
+ info(`Port: ${c.port}`);
146
+ info(`URL: ${c.url || '(not deployed)'}`);
147
+ info(`Created: ${c.created_at}`);
148
+ info(`Updated: ${c.updated_at}`);
149
+ }
150
+ export async function del(id, flags) {
151
+ const config = requireConfig();
152
+ const orgId = requireOrg(flags, config, 'myapi container delete <id> [--org <id>]');
153
+ if (!id)
154
+ error('Missing id.\nUsage: myapi container delete <id>');
155
+ await sdkContainer.deleteContainer(config.api_key, orgId, id);
156
+ success(`Deleted container ${id}`);
157
+ }
158
+ // deploy ships a pre-built image to Cloud Run. The scoped API key is
159
+ // rotated on every deploy — the fresh value is shown once here.
160
+ export async function deploy(id, image, flags) {
161
+ const config = requireConfig();
162
+ const orgId = requireOrg(flags, config, 'myapi container deploy <id> <image-ref> [--org <id>]');
163
+ if (!id)
164
+ error('Missing id.\nUsage: myapi container deploy <id> <image-ref>');
165
+ if (!image)
166
+ error('Missing image ref.\nUsage: myapi container deploy <id> <image-ref>\n\n→ <image-ref> is a pre-built container image (e.g. a registry path).');
167
+ const result = await sdkContainer.deployContainer(config.api_key, orgId, id, image);
168
+ if (flags.json) {
169
+ printJson(result);
170
+ return;
171
+ }
172
+ success(`Deployed container ${id} (revision ${result.revision_id})`);
173
+ info(`Status: ${result.status}`);
174
+ info(`URL: ${result.url}`);
175
+ info('');
176
+ info(`Scoped API key was rotated. New value (returned once — save it if you need it):`);
177
+ info(` ${result.scoped_api_key}`);
178
+ }
179
+ // ── Dispatcher ───────────────────────────────────────────────────────────────
180
+ const SUBCOMMAND_USAGE = {
181
+ 'create': `myapi container create --name <name> [--type service|worker|job] [--cron <expr>]
182
+ [--cpu <n>] [--memory <size>] [--min-instances <n>]
183
+ [--max-instances <n>] [--port <n>] [--env K=V,...] [--org <id>]
184
+
185
+ Persists a container record + issues a scoped API key. Deploy an image
186
+ separately with "myapi container deploy".
187
+
188
+ Types:
189
+ service HTTP server (default) — scales to zero
190
+ worker always-on background process (min 1 instance)
191
+ job runs to completion — the only type that accepts --cron
192
+
193
+ Examples:
194
+ myapi container create --name api --port 8080
195
+ myapi container create --name nightly --type job --cron "0 3 * * *"
196
+ myapi container create --name queue-worker --type worker --memory 1Gi`,
197
+ 'deploy': `myapi container deploy <id> <image-ref> [--org <id>] [--json]
198
+
199
+ Ships a pre-built container image to the runtime. The scoped API key is
200
+ rotated on every deploy — the fresh value is printed once.
201
+
202
+ Example:
203
+ myapi container deploy <id> registry.example.com/my-app:v2`,
204
+ 'list': 'myapi container list [--org <id>] [--json]',
205
+ 'get': 'myapi container get <id> [--org <id>] [--json]',
206
+ 'delete': 'myapi container delete <id> [--org <id>]',
207
+ };
208
+ export async function run(subcommand, args, flags) {
209
+ if (!subcommand || (flags.help && !subcommand)) {
210
+ info(`Usage: myapi container <subcommand>
211
+
212
+ Run containers — long-running services, background workers, and scheduled
213
+ jobs. The heavier-duty sibling of edge functions (myapi fn), for native
214
+ dependencies and long execution.
215
+
216
+ Subcommands:
217
+ create Register a container and get its scoped API key (returned once)
218
+ deploy <id> <image> Ship a pre-built image and go live
219
+ list List containers in your org
220
+ get <id> Inspect a container
221
+ delete <id> Soft-delete and revoke its scoped API key
222
+
223
+ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
224
+ return;
225
+ }
226
+ if (flags.help) {
227
+ const usage = SUBCOMMAND_USAGE[subcommand];
228
+ if (usage)
229
+ info(`Usage: ${usage}`);
230
+ else
231
+ info(`Unknown subcommand: ${subcommand}. Run "myapi container --help" for the list.`);
232
+ return;
233
+ }
234
+ switch (subcommand) {
235
+ case 'create': return create(flags);
236
+ case 'deploy': return deploy(args[0], args[1], flags);
237
+ case 'list': return list(flags);
238
+ case 'get': return get(args[0], flags);
239
+ case 'delete': return del(args[0], flags);
240
+ default: error(`Unknown subcommand: ${subcommand}. Run "myapi container --help" for a list of valid subcommands.`);
241
+ }
242
+ }
@@ -16,6 +16,11 @@ export const EXPOSES = [
16
16
  'GET /domain/orgs/{org_id}/{domain}/status',
17
17
  'POST /domain/orgs/{org_id}/{domain}/email-infra',
18
18
  'POST /domain/orgs/{org_id}/{domain}/retry-provisioning',
19
+ 'GET /domain/orgs/{org_id}/{domain}/records',
20
+ 'POST /domain/orgs/{org_id}/{domain}/records',
21
+ 'GET /domain/orgs/{org_id}/{domain}/records/{record_id}',
22
+ 'PATCH /domain/orgs/{org_id}/{domain}/records/{record_id}',
23
+ 'DELETE /domain/orgs/{org_id}/{domain}/records/{record_id}',
19
24
  ];
20
25
  export const SCHEMA = {
21
26
  org: 'string',
@@ -35,7 +35,22 @@ async function list(flags) {
35
35
  const config = requireConfig();
36
36
  const orgId = requireOrg(flags, config, 'myapi email campaign list [--org <id>]');
37
37
  const campaigns = await sdkEmail.listCampaigns(config.api_key, orgId);
38
- printTable(campaigns, {
38
+ if (flags.json) {
39
+ printJson(campaigns);
40
+ return;
41
+ }
42
+ // Drops warmup_config — a nested object that renders as "[object Object]"
43
+ // in a table. The scalar fields below are what a listing needs; the full
44
+ // config is in `myapi email campaign get <id>` and `--json`.
45
+ printTable(campaigns.map(c => ({
46
+ id: c.id,
47
+ name: c.name,
48
+ status: c.status,
49
+ template_id: c.template_id,
50
+ from_address: c.from_address,
51
+ per_day_limit: c.per_day_limit,
52
+ created_at: c.created_at,
53
+ })), {
39
54
  flags,
40
55
  empty: 'No campaigns yet. Create one with: myapi email campaign create',
41
56
  });
@@ -9,6 +9,17 @@ export const EXPOSES = [
9
9
  'GET /email/outbox/{address}',
10
10
  'GET /email/message/{message_id}',
11
11
  ];
12
+ // Table columns for a message listing. Drops `body` — the full message
13
+ // content is large and belongs in `myapi email message get <id>`, not in a
14
+ // list row. `--json` still returns the complete record.
15
+ function summarizeMessage(m) {
16
+ return {
17
+ message_id: m.message_id,
18
+ from: m.from,
19
+ subject: m.subject,
20
+ received_at: m.received_at,
21
+ };
22
+ }
12
23
  async function send(flags) {
13
24
  const config = requireConfig();
14
25
  if (!flags.from || !flags.to || !flags.subject) {
@@ -50,7 +61,11 @@ async function sent(flags) {
50
61
  const limit = flags.limit || 50;
51
62
  const offset = flags.offset || 0;
52
63
  const emails = await sdkEmail.getSentEmails(config.api_key, limit, offset);
53
- printTable(emails, {
64
+ if (flags.json) {
65
+ printJson(emails);
66
+ return;
67
+ }
68
+ printTable(emails.map(summarizeMessage), {
54
69
  flags,
55
70
  empty: 'No sent emails yet.',
56
71
  });
@@ -60,7 +75,11 @@ async function inbox(address, flags) {
60
75
  if (!address)
61
76
  error('Missing required arguments.\nUsage: myapi email message inbox <address>');
62
77
  const messages = await sdkEmail.getInbox(config.api_key, address);
63
- printTable(messages, {
78
+ if (flags.json) {
79
+ printJson(messages);
80
+ return;
81
+ }
82
+ printTable(messages.map(summarizeMessage), {
64
83
  flags,
65
84
  empty: `No messages in ${address}.`,
66
85
  });
@@ -70,7 +89,11 @@ async function outbox(address, flags) {
70
89
  if (!address)
71
90
  error('Missing required arguments.\nUsage: myapi email message outbox <address>');
72
91
  const messages = await sdkEmail.getOutbox(config.api_key, address);
73
- printTable(messages, {
92
+ if (flags.json) {
93
+ printJson(messages);
94
+ return;
95
+ }
96
+ printTable(messages.map(summarizeMessage), {
74
97
  flags,
75
98
  empty: `No outbound messages from ${address}.`,
76
99
  });
@@ -44,7 +44,14 @@ async function list(flags) {
44
44
  printJson(templates);
45
45
  return;
46
46
  }
47
- printTable(templates, {
47
+ // Drops preview_url (a long URL, available via `template get`) and
48
+ // created_at (updated_at is the field a listing cares about).
49
+ printTable(templates.map(t => ({
50
+ id: t.id,
51
+ name: t.name,
52
+ subject: t.subject,
53
+ updated_at: t.updated_at,
54
+ })), {
48
55
  flags,
49
56
  empty: 'No templates yet. Generate one with: myapi email template generate --prompt <p> --name <n>',
50
57
  });
@@ -10,4 +10,7 @@ export declare function create(flags: Flags): Promise<void>;
10
10
  export declare function list(flags: Flags): Promise<void>;
11
11
  export declare function get(id: string, flags: Flags): Promise<void>;
12
12
  export declare function del(id: string, flags: Flags): Promise<void>;
13
+ export declare function deploy(id: string, bundlePath: string, flags: Flags): Promise<void>;
14
+ export declare function setEnv(id: string, name: string, value: string, flags: Flags): Promise<void>;
15
+ export declare function runs(id: string, flags: Flags): Promise<void>;
13
16
  export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;