@myapihq/cli 1.2.6 → 1.2.8

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.
@@ -6,6 +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>;
11
12
  export declare function spendCap(arg: string | undefined, flags: Flags): Promise<void>;
@@ -9,6 +9,7 @@ export const SCHEMA = {
9
9
  export const EXPOSES = [
10
10
  'GET /hq/billing/balance',
11
11
  'GET /hq/billing/history',
12
+ 'GET /hq/billing/usage',
12
13
  'POST /hq/billing/setup-payment',
13
14
  'POST /hq/billing/topup',
14
15
  'GET /hq/account/me',
@@ -17,6 +18,11 @@ export const EXPOSES = [
17
18
  const SUBCOMMAND_USAGE = {
18
19
  'balance': 'myapi billing balance [--json]',
19
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.`,
20
26
  'topup': `myapi billing topup <amount> [--yes]
21
27
 
22
28
  Amount is in whole dollars (e.g. "10" charges $10).
@@ -44,6 +50,7 @@ export async function run(subcommand, args, flags) {
44
50
  Subcommands:
45
51
  balance Check balance, credits, and payment method status
46
52
  history View recent transactions and top-ups
53
+ usage Spend rolled up by service (this month, or --period 30d)
47
54
  topup Top up your balance (whole dollars)
48
55
  setup Open a checkout link to add or update payment method
49
56
  spend-cap Set/show/clear the account-level spend ceiling (IAM Layer 2)`);
@@ -60,6 +67,7 @@ Subcommands:
60
67
  switch (subcommand) {
61
68
  case 'balance': return balance(flags);
62
69
  case 'history': return history(flags);
70
+ case 'usage': return usage(flags);
63
71
  case 'topup': return topup(args[0], flags);
64
72
  case 'setup': return setup(flags);
65
73
  case 'spend-cap': return spendCap(args[0], flags);
@@ -105,6 +113,31 @@ export async function history(flags) {
105
113
  empty: 'No transactions yet.',
106
114
  });
107
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
+ }
108
141
  export async function topup(amountStr, flags) {
109
142
  const amount = Math.round(parseFloat(amountStr));
110
143
  if (!amountStr || isNaN(amount) || amount <= 0) {
@@ -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,16 @@
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 logs(id: string, flags: Flags): Promise<void>;
16
+ export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
@@ -0,0 +1,270 @@
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
+ 'GET /container/orgs/{org_id}/containers/{id}/logs',
13
+ ];
14
+ export const SCHEMA = {
15
+ name: 'string',
16
+ type: 'string',
17
+ cron: 'string',
18
+ cpu: 'string',
19
+ memory: 'string',
20
+ 'min-instances': 'number',
21
+ 'max-instances': 'number',
22
+ port: 'number',
23
+ env: 'string',
24
+ tail: 'number',
25
+ };
26
+ const CONTAINER_TYPES = ['service', 'worker', 'job'];
27
+ // Mirrors validateName in myapi-hq/internal/routes/container/crud.go —
28
+ // identical to functions. Client-side rejection so typos fail before the
29
+ // network call; the backend runs the same regex as defence in depth.
30
+ export const NAME_RE = /^[a-z0-9][a-z0-9-]{0,49}$/;
31
+ export const RESERVED_NAMES = new Set(['www', 'api', 'admin', 'system', 'default']);
32
+ // Returns an error message on failure, or null on success. Pure — no I/O,
33
+ // no process.exit. Tests use this form; the caller wraps it in error().
34
+ export function _validateName(name) {
35
+ if (!NAME_RE.test(name)) {
36
+ return `Invalid --name "${name}". Lowercase letters, digits, hyphens; 1-50 chars; starts with a letter or digit.`;
37
+ }
38
+ if (RESERVED_NAMES.has(name)) {
39
+ return `Name "${name}" is reserved. Pick a different one.`;
40
+ }
41
+ return null;
42
+ }
43
+ // Parses `--env K=V,K2=V2` into an object. Returns the map or an error
44
+ // message string (pure form, for tests).
45
+ export function _parseEnv(raw) {
46
+ const env = {};
47
+ for (const pair of raw.split(',').map(s => s.trim()).filter(Boolean)) {
48
+ const eq = pair.indexOf('=');
49
+ if (eq < 1) {
50
+ return `Invalid --env entry "${pair}". Use KEY=VALUE, comma-separated.`;
51
+ }
52
+ env[pair.slice(0, eq)] = pair.slice(eq + 1);
53
+ }
54
+ return env;
55
+ }
56
+ function summarizeContainer(c) {
57
+ return {
58
+ id: c.id,
59
+ name: c.name,
60
+ type: c.type,
61
+ status: c.status,
62
+ url: c.url || '(not deployed)',
63
+ updated_at: c.updated_at ? formatDate(c.updated_at) : '',
64
+ };
65
+ }
66
+ export async function create(flags) {
67
+ const config = requireConfig();
68
+ const orgId = requireOrg(flags, config, 'myapi container create --name <name> [--type service|worker|job] [--org <id>]');
69
+ const name = flags.name;
70
+ if (!name) {
71
+ 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").');
72
+ }
73
+ const nameErr = _validateName(name);
74
+ if (nameErr)
75
+ error(nameErr);
76
+ const type = flags.type ?? 'service';
77
+ if (!CONTAINER_TYPES.includes(type)) {
78
+ error(`Invalid --type "${type}". Use one of: ${CONTAINER_TYPES.join(', ')}.`);
79
+ }
80
+ const cron = flags.cron;
81
+ if (cron && type !== 'job') {
82
+ error('--cron is only valid for --type job (services and workers are always-on).');
83
+ }
84
+ const payload = { name, type: type };
85
+ if (cron)
86
+ payload.cron_schedule = cron;
87
+ if (typeof flags.cpu === 'string')
88
+ payload.cpu = flags.cpu;
89
+ if (typeof flags.memory === 'string')
90
+ payload.memory = flags.memory;
91
+ if (typeof flags['min-instances'] === 'number')
92
+ payload.min_instances = flags['min-instances'];
93
+ if (typeof flags['max-instances'] === 'number')
94
+ payload.max_instances = flags['max-instances'];
95
+ if (typeof flags.port === 'number')
96
+ payload.port = flags.port;
97
+ if (typeof flags.env === 'string') {
98
+ const env = _parseEnv(flags.env);
99
+ if (typeof env === 'string')
100
+ error(env);
101
+ payload.env = env;
102
+ }
103
+ const result = await sdkContainer.createContainer(config.api_key, orgId, payload);
104
+ success(`Container created: ${result.container.id}`);
105
+ info(`Name: ${result.container.name}`);
106
+ info(`Type: ${result.container.type}${result.container.cron_schedule ? ` (${result.container.cron_schedule})` : ''}`);
107
+ info(`Resources: ${result.container.cpu} CPU, ${result.container.memory}, instances ${result.container.min_instances}-${result.container.max_instances}`);
108
+ // The scoped key is returned ONCE — it's delivered to the running
109
+ // container as the MYAPI_KEY env var. Deploy rotates it.
110
+ info('');
111
+ info(`Scoped API key (returned once — save it if you need it):`);
112
+ info(` ${result.scoped_api_key}`);
113
+ if (!result.container.url) {
114
+ info('');
115
+ banner(`Next: deploy an image with myapi container deploy ${result.container.id} <image-ref>`);
116
+ }
117
+ }
118
+ export async function list(flags) {
119
+ const config = requireConfig();
120
+ const orgId = requireOrg(flags, config, 'myapi container list [--org <id>]');
121
+ const containers = await sdkContainer.listContainers(config.api_key, orgId);
122
+ if (flags.json) {
123
+ printJson(containers);
124
+ return;
125
+ }
126
+ printTable(containers.map(summarizeContainer), {
127
+ flags,
128
+ empty: 'No containers yet. Create one with: myapi container create --name <name>',
129
+ });
130
+ }
131
+ export async function get(id, flags) {
132
+ const config = requireConfig();
133
+ const orgId = requireOrg(flags, config, 'myapi container get <id> [--org <id>]');
134
+ if (!id)
135
+ error('Missing id.\nUsage: myapi container get <id>');
136
+ const c = await sdkContainer.getContainer(config.api_key, orgId, id);
137
+ if (flags.json) {
138
+ printJson(c);
139
+ return;
140
+ }
141
+ info(`ID: ${c.id}`);
142
+ info(`Name: ${c.name}`);
143
+ info(`Type: ${c.type}${c.cron_schedule ? ` (${c.cron_schedule})` : ''}`);
144
+ info(`Status: ${c.status}`);
145
+ info(`Resources: ${c.cpu} CPU, ${c.memory}, instances ${c.min_instances}-${c.max_instances}`);
146
+ if (c.port)
147
+ info(`Port: ${c.port}`);
148
+ info(`URL: ${c.url || '(not deployed)'}`);
149
+ info(`Created: ${c.created_at}`);
150
+ info(`Updated: ${c.updated_at}`);
151
+ }
152
+ export async function del(id, flags) {
153
+ const config = requireConfig();
154
+ const orgId = requireOrg(flags, config, 'myapi container delete <id> [--org <id>]');
155
+ if (!id)
156
+ error('Missing id.\nUsage: myapi container delete <id>');
157
+ await sdkContainer.deleteContainer(config.api_key, orgId, id);
158
+ success(`Deleted container ${id}`);
159
+ }
160
+ // deploy ships a pre-built image to Cloud Run. The scoped API key is
161
+ // rotated on every deploy — the fresh value is shown once here.
162
+ export async function deploy(id, image, flags) {
163
+ const config = requireConfig();
164
+ const orgId = requireOrg(flags, config, 'myapi container deploy <id> <image-ref> [--org <id>]');
165
+ if (!id)
166
+ error('Missing id.\nUsage: myapi container deploy <id> <image-ref>');
167
+ if (!image)
168
+ 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).');
169
+ const result = await sdkContainer.deployContainer(config.api_key, orgId, id, image);
170
+ if (flags.json) {
171
+ printJson(result);
172
+ return;
173
+ }
174
+ success(`Deployed container ${id} (revision ${result.revision_id})`);
175
+ info(`Status: ${result.status}`);
176
+ info(`URL: ${result.url}`);
177
+ info('');
178
+ info(`Scoped API key was rotated. New value (returned once — save it if you need it):`);
179
+ info(` ${result.scoped_api_key}`);
180
+ }
181
+ // logs prints the container's recent Cloud Run runtime logs, newest first.
182
+ export async function logs(id, flags) {
183
+ const config = requireConfig();
184
+ const orgId = requireOrg(flags, config, 'myapi container logs <id> [--tail <n>] [--org <id>]');
185
+ if (!id)
186
+ error('Missing id.\nUsage: myapi container logs <id> [--tail <n>]');
187
+ const tail = typeof flags.tail === 'number' ? flags.tail : undefined;
188
+ const entries = await sdkContainer.getContainerLogs(config.api_key, orgId, id, tail);
189
+ if (flags.json) {
190
+ printJson(entries);
191
+ return;
192
+ }
193
+ if (entries.length === 0) {
194
+ info('No logs — the container is not deployed yet, or has produced no output.');
195
+ return;
196
+ }
197
+ for (const e of entries) {
198
+ info(`${e.timestamp} ${(e.severity || '').padEnd(8)} ${e.text}`);
199
+ }
200
+ }
201
+ // ── Dispatcher ───────────────────────────────────────────────────────────────
202
+ const SUBCOMMAND_USAGE = {
203
+ 'create': `myapi container create --name <name> [--type service|worker|job] [--cron <expr>]
204
+ [--cpu <n>] [--memory <size>] [--min-instances <n>]
205
+ [--max-instances <n>] [--port <n>] [--env K=V,...] [--org <id>]
206
+
207
+ Persists a container record + issues a scoped API key. Deploy an image
208
+ separately with "myapi container deploy".
209
+
210
+ Types:
211
+ service HTTP server (default) — scales to zero
212
+ worker always-on background process (min 1 instance)
213
+ job runs to completion — the only type that accepts --cron
214
+
215
+ Examples:
216
+ myapi container create --name api --port 8080
217
+ myapi container create --name nightly --type job --cron "0 3 * * *"
218
+ myapi container create --name queue-worker --type worker --memory 1Gi`,
219
+ 'deploy': `myapi container deploy <id> <image-ref> [--org <id>] [--json]
220
+
221
+ Ships a pre-built container image to the runtime. The scoped API key is
222
+ rotated on every deploy — the fresh value is printed once.
223
+
224
+ Example:
225
+ myapi container deploy <id> registry.example.com/my-app:v2`,
226
+ 'list': 'myapi container list [--org <id>] [--json]',
227
+ 'get': 'myapi container get <id> [--org <id>] [--json]',
228
+ 'logs': `myapi container logs <id> [--tail <n>] [--org <id>] [--json]
229
+
230
+ Recent Cloud Run runtime logs, newest first. --tail caps the count
231
+ (default 100, max 1000).`,
232
+ 'delete': 'myapi container delete <id> [--org <id>]',
233
+ };
234
+ export async function run(subcommand, args, flags) {
235
+ if (!subcommand || (flags.help && !subcommand)) {
236
+ info(`Usage: myapi container <subcommand>
237
+
238
+ Run containers — long-running services, background workers, and scheduled
239
+ jobs. The heavier-duty sibling of edge functions (myapi fn), for native
240
+ dependencies and long execution.
241
+
242
+ Subcommands:
243
+ create Register a container and get its scoped API key (returned once)
244
+ deploy <id> <image> Ship a pre-built image and go live
245
+ list List containers in your org
246
+ get <id> Inspect a container
247
+ logs <id> Show recent runtime logs (--tail <n>)
248
+ delete <id> Soft-delete and revoke its scoped API key
249
+
250
+ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
251
+ return;
252
+ }
253
+ if (flags.help) {
254
+ const usage = SUBCOMMAND_USAGE[subcommand];
255
+ if (usage)
256
+ info(`Usage: ${usage}`);
257
+ else
258
+ info(`Unknown subcommand: ${subcommand}. Run "myapi container --help" for the list.`);
259
+ return;
260
+ }
261
+ switch (subcommand) {
262
+ case 'create': return create(flags);
263
+ case 'deploy': return deploy(args[0], args[1], flags);
264
+ case 'list': return list(flags);
265
+ case 'get': return get(args[0], flags);
266
+ case 'logs': return logs(args[0], flags);
267
+ case 'delete': return del(args[0], flags);
268
+ default: error(`Unknown subcommand: ${subcommand}. Run "myapi container --help" for a list of valid subcommands.`);
269
+ }
270
+ }
@@ -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
  });
@@ -75,7 +75,13 @@ export async function visits(flags) {
75
75
  printJson(res);
76
76
  return;
77
77
  }
78
- printTable(res.visits);
78
+ // `type` is constant ('visit') across this list — omitted from the table.
79
+ printTable(res.visits.map(v => ({
80
+ pixel_id: v.pixel_id,
81
+ from_url: v.from_url,
82
+ to_url: v.to_url,
83
+ ts: v.ts,
84
+ })));
79
85
  info(`Total: ${res.total} | Showing: ${res.limit} | Offset: ${res.offset}`);
80
86
  }
81
87
  // Engagement events (open / click / page_visit / sent) — filterable by
@@ -106,7 +112,14 @@ export async function events(flags) {
106
112
  printJson(res);
107
113
  return;
108
114
  }
109
- printTable(res.events);
115
+ // `type` is constant ('event') across this list — omitted from the table.
116
+ printTable(res.events.map(e => ({
117
+ pixel_id: e.pixel_id,
118
+ event_type: e.event_type,
119
+ url: e.url ?? '',
120
+ campaign_id: e.campaign_id ?? '',
121
+ ts: e.ts,
122
+ })));
110
123
  info(`Total: ${res.total} | Showing: ${res.limit} | Offset: ${res.offset}`);
111
124
  }
112
125
  // Geographic distribution sample of the org's pixel audience.
@@ -1,7 +1,7 @@
1
1
  import type { Flags } from '../helpers.js';
2
2
  import type { Exposes } from '../exposes.js';
3
3
  export declare const EXPOSES: Exposes;
4
+ export declare function cachedLatestVersion(): string | null;
4
5
  export declare function checkForUpdate(currentVersion: string): Promise<void>;
5
6
  export declare function update(flags?: Flags): Promise<void>;
6
- export declare function latestVersion(): Promise<string | null>;
7
7
  export declare function isNewer(latest: string, current: string): boolean;