@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.
- package/dist/commands/billing.d.ts +2 -0
- package/dist/commands/billing.js +94 -2
- package/dist/commands/container-validation.test.d.ts +1 -0
- package/dist/commands/container-validation.test.js +45 -0
- package/dist/commands/container.d.ts +15 -0
- package/dist/commands/container.js +242 -0
- package/dist/commands/domain.js +5 -0
- package/dist/commands/email/campaign.js +16 -1
- package/dist/commands/email/message.js +26 -3
- package/dist/commands/email/template.js +8 -1
- package/dist/commands/fn.d.ts +3 -0
- package/dist/commands/fn.js +115 -24
- package/dist/commands/funnel.d.ts +1 -0
- package/dist/commands/funnel.js +83 -0
- package/dist/commands/keys-validation.test.d.ts +1 -0
- package/dist/commands/keys-validation.test.js +87 -0
- package/dist/commands/keys.d.ts +6 -2
- package/dist/commands/keys.js +189 -55
- package/dist/commands/payments-validation.test.d.ts +1 -0
- package/dist/commands/payments-validation.test.js +31 -0
- package/dist/commands/payments.d.ts +13 -0
- package/dist/commands/payments.js +219 -0
- package/dist/commands/pixel.js +15 -2
- package/dist/commands/update.d.ts +1 -1
- package/dist/commands/update.js +68 -45
- package/dist/commands/webhook.js +10 -1
- package/dist/completion.d.ts +3 -1
- package/dist/completion.js +160 -55
- package/dist/exposes.test.js +2 -0
- package/dist/index.js +34 -13
- package/dist/sdk-billing-usage.test.d.ts +1 -0
- package/dist/sdk-billing-usage.test.js +74 -0
- package/dist/sdk-container.test.d.ts +1 -0
- package/dist/sdk-container.test.js +115 -0
- package/dist/sdk-function.test.js +73 -6
- package/dist/sdk-funnel-publish.test.d.ts +1 -0
- package/dist/sdk-funnel-publish.test.js +89 -0
- package/dist/sdk-iam.test.d.ts +1 -0
- package/dist/sdk-iam.test.js +190 -0
- package/dist/sdk-payments.test.d.ts +1 -0
- package/dist/sdk-payments.test.js +139 -0
- 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>;
|
package/dist/commands/billing.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/domain.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/commands/fn.d.ts
CHANGED
|
@@ -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>;
|