@myapihq/cli 1.0.54 → 1.0.59

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.
@@ -1,3 +1,3 @@
1
- export declare function link(flags?: Record<string, string | boolean>): Promise<void>;
1
+ export declare function link(flags?: Record<string, string | boolean>, emailArg?: string): Promise<void>;
2
2
  export declare function whoami(flags?: Record<string, string | boolean>): Promise<void>;
3
3
  export declare function switchCmd(flags?: Record<string, string | boolean>, indexArg?: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import * as readline from 'readline';
2
2
  import { loadConfig, saveConfig, addAccount, switchAccount, listAccounts } from '../config.js';
3
- import { info, success, error } from '../output.js';
3
+ import { info, success, error, printJson } from '../output.js';
4
4
  import { installSkills } from './setup.js';
5
5
  import { hq } from '@myapihq/sdk';
6
6
  const API_BASE = process.env.MYAPI_API_BASE ?? 'https://api.myapihq.com';
@@ -39,9 +39,9 @@ async function patch(path, body, apiKey) {
39
39
  return json.data ?? json;
40
40
  }
41
41
  // myapi auth link — upgrade anonymous session to registered account.
42
- export async function link(flags = {}) {
42
+ export async function link(flags = {}, emailArg) {
43
43
  if (flags.help) {
44
- info('Usage: myapi auth link\n\nLinks an email to your anonymous account, upgrading it to a registered account.\nA verification code will be sent you must enter it interactively.\nIf the email is already in use, adds it as a second account instead.');
44
+ info('Usage: myapi auth link [email]\n\nAttaches a verified email to your current anonymous account, converting it into\na registered account. A one-time code will be sent to the email to confirm.\n\nIf that email already belongs to an existing MyAPI account, you will be signed\ninto that account instead — it is added as a second active session alongside\nyour current one.\n\nTo create a brand-new account from scratch, use: myapi auth setup');
45
45
  return;
46
46
  }
47
47
  const config = loadConfig();
@@ -51,7 +51,9 @@ export async function link(flags = {}) {
51
51
  error('Already registered. Use your existing account.');
52
52
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
53
53
  try {
54
- const email = (await ask(rl, '› Email? ')).trim();
54
+ const email = emailArg || (await ask(rl, '› Email? ')).trim();
55
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
56
+ error(`Invalid email address: "${email}".`);
55
57
  let upgradeOk = true;
56
58
  try {
57
59
  await patch('/hq/account/upgrade', { email }, config.api_key);
@@ -108,25 +110,36 @@ export async function link(flags = {}) {
108
110
  // myapi auth whoami — show current session info.
109
111
  export async function whoami(flags = {}) {
110
112
  if (flags.help) {
111
- info('Usage: myapi auth whoami\n\nDisplays the active account details: email, account ID, default org, default funnel, account type, and current balance.');
113
+ info('Usage: myapi auth whoami [--json]\n\nDisplays the active account details: email, account ID, default org, default funnel, account type, and current balance.\n\nFlags:\n --json Output raw JSON');
112
114
  return;
113
115
  }
114
116
  const config = loadConfig();
115
117
  if (!config?.api_key)
116
118
  error('Not configured. Run: myapi auth setup');
119
+ let balance = null;
120
+ try {
121
+ balance = await hq.getBalance(config.api_key);
122
+ }
123
+ catch { }
124
+ if (flags.json) {
125
+ printJson({
126
+ email: config.email ?? null,
127
+ account_id: config.account_id,
128
+ default_org: config.default_org ?? null,
129
+ default_funnel: config.default_funnel ?? null,
130
+ is_anonymous: config.is_anonymous ?? false,
131
+ balance: balance?.balance_display ?? null,
132
+ credits: balance?.credits_display ?? null,
133
+ });
134
+ return;
135
+ }
117
136
  if (config.email)
118
137
  info(`Email: ${config.email}`);
119
138
  info(`Account: ${config.account_id}`);
120
139
  info(`Org: ${config.default_org ?? '(none)'}`);
121
140
  info(`Funnel: ${config.default_funnel ?? '(none)'}`);
122
141
  info(`Type: ${config.is_anonymous ? 'anonymous' : 'registered'}`);
123
- try {
124
- const bal = await hq.getBalance(config.api_key);
125
- info(`Balance: ${bal.balance_display} | Credits: ${bal.credits_display}`);
126
- }
127
- catch {
128
- info(`Balance: (unavailable)`);
129
- }
142
+ info(`Balance: ${balance ? `${balance.balance_display} | Credits: ${balance.credits_display} (not usable for domains)` : '(unavailable)'}`);
130
143
  }
131
144
  // myapi auth switch [index] — switch between saved accounts.
132
145
  export async function switchCmd(flags = {}, indexArg) {
@@ -139,6 +152,7 @@ export async function switchCmd(flags = {}, indexArg) {
139
152
  error('No accounts configured. Run: myapi auth setup');
140
153
  if (accounts.length === 1) {
141
154
  info(`Only one account configured: ${accounts[0].email ?? accounts[0].account_id}`);
155
+ info('To add another account, run: myapi auth setup');
142
156
  return;
143
157
  }
144
158
  info('Accounts:');
@@ -5,15 +5,23 @@ import { success, error, printTable, info, printJson } from '../output.js';
5
5
  import { formatDate } from '../utils.js';
6
6
  export async function balance(flags) {
7
7
  if (flags.help) {
8
- info('Usage: myapi billing balance\n\nShows your current account balance, credits, and payment method status.');
8
+ info('Usage: myapi billing balance [--json]\n\nShows your current account balance, credits, and payment method status.\n\nFlags:\n --json Output raw JSON');
9
9
  return;
10
10
  }
11
11
  const config = requireConfig();
12
12
  const result = await hq.getBalance(config.api_key);
13
+ if (flags.json) {
14
+ printJson({
15
+ balance: result.balance_display,
16
+ credits: result.credits_display,
17
+ has_payment_method: result.has_payment_method,
18
+ });
19
+ return;
20
+ }
13
21
  const pm = result.has_payment_method ? 'yes' : 'no';
14
22
  const accountType = config.is_anonymous ? 'anonymous' : `registered (${config.email ?? ''})`;
15
23
  info(`Account: ${accountType}`);
16
- info(`Balance: ${result.balance_display} | Credits: ${result.credits_display} | Payment method: ${pm}`);
24
+ info(`Balance: ${result.balance_display} | Credits: ${result.credits_display} (not usable for domains) | Payment method: ${pm}`);
17
25
  }
18
26
  export async function history(flags) {
19
27
  if (flags.help) {
@@ -40,7 +48,7 @@ export async function history(flags) {
40
48
  }
41
49
  export async function topup(amountStr, flags) {
42
50
  if (flags.help) {
43
- info('Usage: myapi billing topup <amount>\n\nAmount is in whole dollars (e.g. "10" charges $10).\nUse --yes to skip confirmation.\nExample: myapi billing topup 10\n\nFlags:\n --yes Skip confirmation prompt');
51
+ info('Usage: myapi billing topup <amount>\n\nAmount is in whole dollars (e.g. "10" charges $10).\nConfirmation is required for amounts of $50 or more. Use --yes to skip it.\nExample: myapi billing topup 10\n\nFlags:\n --yes Skip confirmation prompt for large amounts');
44
52
  return;
45
53
  }
46
54
  const amount = Math.round(parseFloat(amountStr));
@@ -1,24 +1,37 @@
1
+ import { hq, funnel as sdkFunnel } from '@myapihq/sdk';
1
2
  import { requireConfig, saveConfig } from '../config.js';
2
3
  import { success, error, info } from '../output.js';
4
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
5
  export async function setOrg(id, flags) {
4
6
  if (!id || flags.help) {
5
7
  info("Usage: myapi config set-org <id>\n\nSets a default organization ID for subsequent commands.");
6
8
  return;
7
9
  }
10
+ if (!UUID_RE.test(id))
11
+ error(`Invalid organization ID: "${id}". Expected a UUID (e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`);
8
12
  const config = requireConfig();
9
- config.default_org = id;
13
+ info('› Validating…');
14
+ const org = await hq.getOrg(config.api_key, id);
15
+ config.default_org = org.id;
10
16
  saveConfig(config);
11
- success(`Default organization set to: ${id}`);
17
+ success(`Default organization set to: ${org.name || org.id}`);
12
18
  }
13
19
  export async function setFunnel(id, flags) {
14
20
  if (!id || flags.help) {
15
21
  info("Usage: myapi config set-funnel <id>\n\nSets a default funnel ID for subsequent commands.");
16
22
  return;
17
23
  }
24
+ if (!UUID_RE.test(id))
25
+ error(`Invalid funnel ID: "${id}". Expected a UUID (e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`);
18
26
  const config = requireConfig();
19
- config.default_funnel = id;
27
+ const orgId = config.default_org;
28
+ if (!orgId)
29
+ error('No default organization set. Run: myapi auth config set-org <id>');
30
+ info('› Validating…');
31
+ const funnel = await sdkFunnel.getFunnel(config.api_key, orgId, id);
32
+ config.default_funnel = funnel.id;
20
33
  saveConfig(config);
21
- success(`Default funnel set to: ${id}`);
34
+ success(`Default funnel set to: ${funnel.id}`);
22
35
  }
23
36
  export async function setDomain(domain, flags) {
24
37
  if (!domain || flags.help) {
@@ -41,8 +54,9 @@ export async function view(flags) {
41
54
  info(`Default Domain: ${config.default_domain || 'Not set'}`);
42
55
  }
43
56
  export async function run(subcommand, args, flags) {
57
+ const via = flags._via || 'auth config';
44
58
  if (!subcommand || (flags.help && !subcommand)) {
45
- info('Usage: myapi config <subcommand>\n\nSubcommands:\n view View current config\n set-org Set default organization\n set-funnel Set default funnel\n set-domain Set default domain');
59
+ info(`Usage: myapi ${via} <subcommand>\n\nSubcommands:\n view View current config\n set-org Set default organization\n set-funnel Set default funnel\n set-domain Set default domain`);
46
60
  return;
47
61
  }
48
62
  if (subcommand === 'view')
@@ -1,6 +1,7 @@
1
1
  import { domain as sdkDomain } from '@myapihq/sdk';
2
2
  import { requireConfig } from '../config.js';
3
3
  import { success, error, info, printTable, printJson } from '../output.js';
4
+ import { formatDate } from '../utils.js';
4
5
  export async function check(domainArg, flags) {
5
6
  const config = requireConfig();
6
7
  const orgId = flags.org || config.default_org;
@@ -8,6 +9,10 @@ export async function check(domainArg, flags) {
8
9
  if (!orgId || !domain)
9
10
  error("Missing required arguments.\nUsage: myapi domain check <domain> --org <id>\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
10
11
  const res = await sdkDomain.checkDomain(config.api_key, orgId, domain);
12
+ if (flags.json) {
13
+ printJson(res);
14
+ return;
15
+ }
11
16
  if (res.available) {
12
17
  const price = res.price_display || `$${(res.price_cents / 100).toFixed(2)}`;
13
18
  info(`${domain} — Available ✓ ${price}/yr`);
@@ -25,7 +30,8 @@ export async function register(domainArg, flags) {
25
30
  error("Missing required arguments.\nUsage: myapi domain register <domain> --org <id> [--years <num>]\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
26
31
  const years = parseInt(flags.years) || 1;
27
32
  const res = await sdkDomain.registerDomain(config.api_key, orgId, domain, years);
28
- success(`Registered ${domain}!\n${JSON.stringify(res, null, 2)}`);
33
+ success(`Registered ${domain}!`);
34
+ info(`Next: assign it to your org so your funnel goes live:\n myapi domain assign ${domain} --org ${orgId}`);
29
35
  }
30
36
  export async function importDomain(domainArg, flags) {
31
37
  const config = requireConfig();
@@ -48,17 +54,20 @@ export async function list(flags) {
48
54
  error("Missing required arguments.\nUsage: myapi domain list --org <id> [--filter=all|unassigned|org]\n(Set default: myapi auth config set-org <id>)");
49
55
  const filter = flags.filter;
50
56
  const domains = await sdkDomain.listDomains(config.api_key, orgId, filter);
57
+ if (domains.length === 0) {
58
+ info('No domains found in this organization. Register one with: myapi domain register <name>');
59
+ return;
60
+ }
51
61
  printTable(domains);
52
62
  }
53
63
  export async function assign(domainArg, flags) {
54
64
  const config = requireConfig();
55
65
  const orgId = flags.org || config.default_org;
56
66
  const domain = domainArg || config.default_domain;
57
- const targetOrgId = flags.target;
58
- if (!orgId || !domain || !targetOrgId)
59
- error("Missing required arguments.\nUsage: myapi domain assign <domain> --org <id> --target <target_org_id>\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
60
- await sdkDomain.assignDomain(config.api_key, orgId, domain, targetOrgId);
61
- success(`Assigned ${domain} to org ${targetOrgId}`);
67
+ if (!orgId || !domain)
68
+ error("Missing required arguments.\nUsage: myapi domain assign <domain> --org <id>\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
69
+ await sdkDomain.assignDomain(config.api_key, orgId, domain);
70
+ success(`Assigned ${domain} to org ${orgId}`);
62
71
  }
63
72
  export async function unassign(domainArg, flags) {
64
73
  const config = requireConfig();
@@ -76,13 +85,14 @@ export async function status(domainArg, flags) {
76
85
  if (!orgId || !domain)
77
86
  error("Missing required arguments.\nUsage: myapi domain status <domain> --org <id>\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
78
87
  const res = await sdkDomain.getDomainStatus(config.api_key, orgId, domain);
79
- const display = {
80
- domain: res.domain,
81
- status: res.status,
82
- };
88
+ if (flags.json) {
89
+ printJson(res);
90
+ return;
91
+ }
92
+ info(`Domain: ${res.domain}`);
93
+ info(`Status: ${res.status}`);
83
94
  if (res.expires_at)
84
- display.expires_at = res.expires_at;
85
- printJson(display);
95
+ info(`Expires: ${formatDate(res.expires_at)}`);
86
96
  }
87
97
  export async function settings(domainArg, flags) {
88
98
  const config = requireConfig();
@@ -111,24 +121,24 @@ export async function updateSettings(domainArg, flags) {
111
121
  }
112
122
  export async function run(subcommand, args, flags) {
113
123
  if (!subcommand || (flags.help && !subcommand)) {
114
- info('Usage: myapi domain <subcommand>\n\nSubcommands:\n list List domains\n check Check domain availability\n register Register a domain\n import Import an existing domain\n assign Assign domain to org\n unassign Unassign domain from its current org\n status Get domain status\n settings Get domain settings\n update-settings Update domain settings\n\nTip: Set defaults with "myapi auth config set-org <id>" to skip --org on every command.');
124
+ info('Usage: myapi domain <subcommand>\n\nSubcommands:\n list List domains\n check Check domain availability\n register Register a domain\n import Import an existing domain\n assign Assign domain to an org\n unassign Unassign domain from its current org\n status Get domain status\n settings Get domain settings\n update-settings Update domain settings\n\nTip: Set defaults with "myapi auth config set-org <id>" to skip --org on every command.');
115
125
  return;
116
126
  }
117
127
  if (flags.help) {
118
128
  if (subcommand === 'list')
119
129
  info('Usage: myapi domain list --org <id> [--filter=all|unassigned|org]\n\nLists all registered domains in your account.\n --filter=all Lists all domains owned by the account.\n --filter=unassigned Lists domains not assigned to an org.\n --filter=org (Default) Lists domains assigned to the current org.');
120
130
  else if (subcommand === 'check')
121
- info('Usage: myapi domain check <domain> --org <id>\n\nChecks if a domain name is available for registration.');
131
+ info('Usage: myapi domain check <domain> --org <id> [--json]\n\nChecks if a domain name is available for registration.\n\nFlags:\n --json Output raw JSON');
122
132
  else if (subcommand === 'register')
123
133
  info('Usage: myapi domain register <domain> --org <id> [--years <num>]\n\nRegisters a new domain name.');
124
134
  else if (subcommand === 'import')
125
135
  info('Usage: myapi domain import <domain> --org <id> --namecheap-user <user> --namecheap-key <key>\n\nImports an existing domain from Namecheap.');
126
136
  else if (subcommand === 'assign')
127
- info('Usage: myapi domain assign <domain> --org <id> --target <target_org_id>\n\nAssigns a domain to a different organization.');
137
+ info('Usage: myapi domain assign <domain> --org <id>\n\nAssigns a domain to the specified organization.');
128
138
  else if (subcommand === 'unassign')
129
139
  info('Usage: myapi domain unassign <domain> --org <id>\n\nUnassigns a domain from its current organization.');
130
140
  else if (subcommand === 'status')
131
- info('Usage: myapi domain status <domain> --org <id>\n\nGets the registration status of a domain.');
141
+ info('Usage: myapi domain status <domain> --org <id> [--json]\n\nGets the registration status of a domain.\n\nFlags:\n --json Output raw JSON');
132
142
  else if (subcommand === 'settings')
133
143
  info('Usage: myapi domain settings <domain> --org <id>\n\nGets the DNS settings and configuration for a domain.');
134
144
  else if (subcommand === 'update-settings')
@@ -1,13 +1,19 @@
1
1
  import { funnel as sdkFunnel, hq } from '@myapihq/sdk';
2
2
  import { requireConfig } from '../config.js';
3
3
  import { success, error, printTable, info, printJson } from '../output.js';
4
+ import { formatDate } from '../utils.js';
4
5
  export async function list(flags) {
5
6
  const config = requireConfig();
6
7
  const orgId = flags.org || config.default_org;
7
8
  if (!orgId)
8
9
  error("Missing required arguments.\nUsage: myapi funnel list --org <id>\n(Or set defaults via: myapi config set-org <id>)");
9
10
  const funnels = await sdkFunnel.listFunnels(config.api_key, orgId);
10
- printTable(funnels);
11
+ const rows = funnels.map(f => ({ id: f.id, created_at: formatDate(f.created_at), updated_at: formatDate(f.updated_at) }));
12
+ if (rows.length === 0) {
13
+ info('No funnels found. Create one with: myapi funnel create');
14
+ return;
15
+ }
16
+ printTable(rows);
11
17
  }
12
18
  export async function create(flags) {
13
19
  const config = requireConfig();
@@ -42,7 +48,11 @@ export async function push(id, slug, flags) {
42
48
  const config = requireConfig();
43
49
  const orgId = flags.org || config.default_org;
44
50
  const funnelId = id || config.default_funnel;
45
- const finalSlug = slug || flags.slug || '/';
51
+ if (slug && flags.slug && slug !== flags.slug) {
52
+ info(`› Note: positional slug "${slug}" takes precedence over --slug "${flags.slug}".`);
53
+ }
54
+ const rawSlug = slug || flags.slug || '/';
55
+ const finalSlug = rawSlug.startsWith('/') ? rawSlug : `/${rawSlug}`;
46
56
  if (!orgId || !funnelId) {
47
57
  error("Missing required arguments.\nUsage: myapi funnel push [funnel_id] [slug] < index.html\n(Defaults to your configured funnel and slug '/' when omitted)");
48
58
  }
@@ -53,8 +63,8 @@ export async function push(id, slug, flags) {
53
63
  process.stdin.on('end', () => resolve(data));
54
64
  process.stdin.on('error', reject);
55
65
  });
56
- if (!html)
57
- error("No HTML provided via stdin");
66
+ if (!html.trim())
67
+ error("No content provided via stdin. Usage: echo '<h1>Hello</h1>' | myapi funnel push [id] [slug]");
58
68
  const result = await sdkFunnel.pushFunnelPage(config.api_key, orgId, funnelId, { slug: finalSlug, html });
59
69
  success(`Pushed page to ${finalSlug}`);
60
70
  if (result?.url) {
@@ -1,6 +1,7 @@
1
1
  import { hq } from '@myapihq/sdk';
2
2
  import { requireConfig } from '../config.js';
3
3
  import { success, error, printTable, info, printJson } from '../output.js';
4
+ import { formatDate } from '../utils.js';
4
5
  import * as readline from 'readline';
5
6
  export async function createNew(flags) {
6
7
  if (flags.help) {
@@ -33,15 +34,8 @@ export async function list(flags) {
33
34
  return;
34
35
  }
35
36
  const formattedKeys = keysList.map(k => {
36
- // Format creation date
37
- const createdDate = new Date(k.created_at);
38
- const createdStr = isNaN(createdDate.getTime()) ? k.created_at : createdDate.toLocaleString();
39
- // Format last used date
40
- let lastUsedStr = 'Never';
41
- if (k.last_used_at) {
42
- const lastUsedDate = new Date(k.last_used_at);
43
- lastUsedStr = isNaN(lastUsedDate.getTime()) ? k.last_used_at : lastUsedDate.toLocaleString();
44
- }
37
+ const createdStr = formatDate(k.created_at);
38
+ const lastUsedStr = k.last_used_at ? formatDate(k.last_used_at) : 'Never';
45
39
  return {
46
40
  Name: k.name || 'Unnamed',
47
41
  Prefix: k.prefix,
@@ -43,7 +43,7 @@ export async function list(flags) {
43
43
  }
44
44
  export async function get(id, flags) {
45
45
  if (flags.help) {
46
- info('Usage: myapi org get <id>\n\nFetches details of a specific organization.');
46
+ info('Usage: myapi org get <id> [--json]\n\nFetches details of a specific organization.\n\nFlags:\n --json Output raw JSON');
47
47
  return;
48
48
  }
49
49
  if (!id) {
@@ -52,10 +52,18 @@ export async function get(id, flags) {
52
52
  }
53
53
  const config = requireConfig();
54
54
  const org = await hq.getOrg(config.api_key, id);
55
- printJson(org);
56
- if (org.preview_subdomain) {
57
- info(`Preview domain: https://${org.preview_subdomain}.makeautonomous.com`);
55
+ if (flags.json) {
56
+ printJson(org);
57
+ return;
58
58
  }
59
+ info(`ID: ${org.id}`);
60
+ info(`Name: ${org.name ?? '(none)'}`);
61
+ if (org.tagline)
62
+ info(`Tagline: ${org.tagline}`);
63
+ if (org.created_at)
64
+ info(`Created: ${formatDate(org.created_at)}`);
65
+ if (org.preview_subdomain)
66
+ info(`Preview: https://${org.preview_subdomain}.makeautonomous.com`);
59
67
  }
60
68
  export async function del(id, flags) {
61
69
  if (flags.help) {
package/dist/index.js CHANGED
@@ -16,10 +16,25 @@ import * as domainCmd from './commands/domain.js';
16
16
  import * as funnelCmd from './commands/funnel.js';
17
17
  import * as authCmd from './commands/auth.js';
18
18
  import * as configCmd from './commands/config.js';
19
+ const ERROR_MESSAGES = {
20
+ DOMAIN_NOT_FOUND: 'Domain not found.',
21
+ INVALID_DOMAIN: 'Invalid domain name.',
22
+ ORG_NOT_FOUND: 'Organization not found.',
23
+ org_not_found: 'Organization not found.',
24
+ FUNNEL_NOT_FOUND: 'Funnel not found.',
25
+ funnel_not_found: 'Funnel not found.',
26
+ db_error: 'Resource not found or invalid ID.',
27
+ NOT_FOUND: 'Resource not found.',
28
+ FORBIDDEN: 'You do not have permission to perform this action.',
29
+ RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
30
+ };
31
+ function friendlyError(code) {
32
+ return ERROR_MESSAGES[code] || code;
33
+ }
19
34
  async function main() {
20
35
  const updatePromise = updateCmd.checkForUpdate(pkg.version).catch(() => { });
21
36
  const { args, flags } = parseArgs(process.argv.slice(2));
22
- if (flags.version || flags.v) {
37
+ if (flags.version || flags.v || flags.V) {
23
38
  const [latest] = await Promise.all([updateCmd.latestVersion(), updatePromise]);
24
39
  const updateNote = latest && updateCmd.isNewer(latest, pkg.version)
25
40
  ? ` (update available: ${latest})`
@@ -27,6 +42,10 @@ async function main() {
27
42
  info(`myapi ${pkg.version}${updateNote}`);
28
43
  return;
29
44
  }
45
+ if (flags.help && args.length === 0) {
46
+ printHelp();
47
+ return;
48
+ }
30
49
  if (args.length === 0) {
31
50
  const config = loadConfig();
32
51
  if (!config?.api_key) {
@@ -42,7 +61,7 @@ async function main() {
42
61
  switch (command) {
43
62
  case 'auth':
44
63
  if (!subcommand || (flags.help && !subcommand)) {
45
- info('Usage: myapi auth <subcommand>\n\nSubcommands:\n setup Configure your account\n import-key Import an existing API key non-interactively\n whoami Show current account\n link Link an email to upgrade your anonymous account\n switch [index] Switch between accounts\n config Manage CLI defaults (org_id, domain…)\n install-skills Install the MyAPI skills pack\n api-keys Manage API keys');
64
+ info('Usage: myapi auth <subcommand>\n\nSubcommands:\n setup Configure your account\n import-key Import an existing API key non-interactively\n whoami Show current account\n link [email] Attach an email to your anonymous account (upgrades it to registered)\n If the email already has an account, signs you into it as a second session\n switch [index] Switch between accounts\n config Manage CLI defaults (org_id, domain…)\n install-skills Install the MyAPI skills pack\n api-keys Manage API keys');
46
65
  break;
47
66
  }
48
67
  if (subcommand === 'setup')
@@ -52,7 +71,7 @@ async function main() {
52
71
  else if (subcommand === 'whoami')
53
72
  await authCmd.whoami(flags);
54
73
  else if (subcommand === 'link')
55
- await authCmd.link(flags);
74
+ await authCmd.link(flags, restArgs[0]);
56
75
  else if (subcommand === 'switch')
57
76
  await authCmd.switchCmd(flags, restArgs[0]);
58
77
  else if (subcommand === 'install-skills') {
@@ -131,8 +150,26 @@ async function main() {
131
150
  case 'setup':
132
151
  await setupCmd.setup(flags);
133
152
  break;
153
+ case 'whoami':
154
+ await authCmd.whoami(flags);
155
+ break;
156
+ case 'keys':
157
+ if (!subcommand) {
158
+ info('Usage: myapi keys <list|create|revoke>');
159
+ break;
160
+ }
161
+ if (subcommand === 'create')
162
+ await keysCmd.createNew(flags);
163
+ else if (subcommand === 'list')
164
+ await keysCmd.list(flags);
165
+ else if (subcommand === 'revoke')
166
+ await keysCmd.revoke(restArgs[0], flags);
167
+ break;
134
168
  case 'config':
135
- await configCmd.run(subcommand, restArgs, flags);
169
+ await configCmd.run(subcommand, restArgs, { ...flags, _via: 'config' });
170
+ break;
171
+ case 'help':
172
+ printHelp();
136
173
  break;
137
174
  default:
138
175
  error(`Unknown command: ${command}. Run "myapi" for available commands.`);
@@ -151,7 +188,7 @@ async function main() {
151
188
  error(`Insufficient balance. Run: myapi billing topup <amount>`);
152
189
  }
153
190
  else
154
- error(err.code || err.message);
191
+ error(friendlyError(err.code) || err.message);
155
192
  }
156
193
  else {
157
194
  error(err.message || (typeof err === 'object' ? JSON.stringify(err) : String(err)));
@@ -179,6 +216,12 @@ Commands:
179
216
  domain Manage domain configurations
180
217
  funnel Manage headless funnels and pages
181
218
 
219
+ Aliases:
220
+ whoami → myapi auth whoami
221
+ keys → myapi auth api-keys
222
+ setup → myapi auth setup
223
+ config → myapi auth config
224
+
182
225
  Run "myapi <command> --help" for subcommand help.
183
226
 
184
227
  ${quickStart}`);
package/dist/output.js CHANGED
@@ -1,8 +1,9 @@
1
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
1
2
  export function success(message) {
2
- console.log(`\x1b[32m✓\x1b[0m ${message}`);
3
+ console.log(useColor ? `\x1b[32m✓\x1b[0m ${message}` : `✓ ${message}`);
3
4
  }
4
5
  export function error(message) {
5
- console.error(`\x1b[31m✗\x1b[0m ${message}`);
6
+ console.error(useColor ? `\x1b[31m✗\x1b[0m ${message}` : `✗ ${message}`);
6
7
  process.exit(1);
7
8
  }
8
9
  export function info(message) {
package/dist/utils.js CHANGED
@@ -3,7 +3,13 @@ export function parseArgs(argv) {
3
3
  const flags = {};
4
4
  for (let i = 0; i < argv.length; i++) {
5
5
  const arg = argv[i];
6
- if (arg.startsWith('--')) {
6
+ if (arg === '-h') {
7
+ flags['help'] = true;
8
+ }
9
+ else if (arg === '-v') {
10
+ flags['version'] = true;
11
+ }
12
+ else if (arg.startsWith('--')) {
7
13
  if (arg.includes('=')) {
8
14
  const [key, value] = arg.slice(2).split('=', 2);
9
15
  flags[key] = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
- "version": "1.0.54",
3
+ "version": "1.0.59",
4
4
  "description": "MyAPI command-line interface",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  import * as readline from 'readline';
2
2
 
3
3
  import { loadConfig, saveConfig, addAccount, switchAccount, listAccounts } from '../config.js';
4
- import { info, success, error } from '../output.js';
4
+ import { info, success, error, printJson } from '../output.js';
5
5
  import { installSkills } from './setup.js';
6
6
  import { hq } from '@myapihq/sdk';
7
7
 
@@ -42,9 +42,9 @@ async function patch(path: string, body: unknown, apiKey: string): Promise<unkno
42
42
  }
43
43
 
44
44
  // myapi auth link — upgrade anonymous session to registered account.
45
- export async function link(flags: Record<string, string | boolean> = {}) {
45
+ export async function link(flags: Record<string, string | boolean> = {}, emailArg?: string) {
46
46
  if (flags.help) {
47
- info('Usage: myapi auth link\n\nLinks an email to your anonymous account, upgrading it to a registered account.\nA verification code will be sent you must enter it interactively.\nIf the email is already in use, adds it as a second account instead.');
47
+ info('Usage: myapi auth link [email]\n\nAttaches a verified email to your current anonymous account, converting it into\na registered account. A one-time code will be sent to the email to confirm.\n\nIf that email already belongs to an existing MyAPI account, you will be signed\ninto that account instead — it is added as a second active session alongside\nyour current one.\n\nTo create a brand-new account from scratch, use: myapi auth setup');
48
48
  return;
49
49
  }
50
50
  const config = loadConfig();
@@ -53,7 +53,8 @@ export async function link(flags: Record<string, string | boolean> = {}) {
53
53
 
54
54
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
55
55
  try {
56
- const email = (await ask(rl, '› Email? ')).trim();
56
+ const email = emailArg || (await ask(rl, '› Email? ')).trim();
57
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) error(`Invalid email address: "${email}".`);
57
58
 
58
59
  let upgradeOk = true;
59
60
  try {
@@ -116,24 +117,36 @@ export async function link(flags: Record<string, string | boolean> = {}) {
116
117
  // myapi auth whoami — show current session info.
117
118
  export async function whoami(flags: Record<string, string | boolean> = {}) {
118
119
  if (flags.help) {
119
- info('Usage: myapi auth whoami\n\nDisplays the active account details: email, account ID, default org, default funnel, account type, and current balance.');
120
+ info('Usage: myapi auth whoami [--json]\n\nDisplays the active account details: email, account ID, default org, default funnel, account type, and current balance.\n\nFlags:\n --json Output raw JSON');
120
121
  return;
121
122
  }
122
123
  const config = loadConfig();
123
124
  if (!config?.api_key) error('Not configured. Run: myapi auth setup');
124
125
 
126
+ let balance: { balance_display: string; credits_display: string } | null = null;
127
+ try {
128
+ balance = await hq.getBalance(config!.api_key);
129
+ } catch {}
130
+
131
+ if (flags.json) {
132
+ printJson({
133
+ email: config!.email ?? null,
134
+ account_id: config!.account_id,
135
+ default_org: config!.default_org ?? null,
136
+ default_funnel: config!.default_funnel ?? null,
137
+ is_anonymous: config!.is_anonymous ?? false,
138
+ balance: balance?.balance_display ?? null,
139
+ credits: balance?.credits_display ?? null,
140
+ });
141
+ return;
142
+ }
143
+
125
144
  if (config!.email) info(`Email: ${config!.email}`);
126
145
  info(`Account: ${config!.account_id}`);
127
146
  info(`Org: ${config!.default_org ?? '(none)'}`);
128
147
  info(`Funnel: ${config!.default_funnel ?? '(none)'}`);
129
148
  info(`Type: ${config!.is_anonymous ? 'anonymous' : 'registered'}`);
130
-
131
- try {
132
- const bal = await hq.getBalance(config!.api_key);
133
- info(`Balance: ${bal.balance_display} | Credits: ${bal.credits_display}`);
134
- } catch {
135
- info(`Balance: (unavailable)`);
136
- }
149
+ info(`Balance: ${balance ? `${balance.balance_display} | Credits: ${balance.credits_display} (not usable for domains)` : '(unavailable)'}`);
137
150
  }
138
151
 
139
152
  // myapi auth switch [index] — switch between saved accounts.
@@ -146,6 +159,7 @@ export async function switchCmd(flags: Record<string, string | boolean> = {}, in
146
159
  if (accounts.length === 0) error('No accounts configured. Run: myapi auth setup');
147
160
  if (accounts.length === 1) {
148
161
  info(`Only one account configured: ${accounts[0].email ?? accounts[0].account_id}`);
162
+ info('To add another account, run: myapi auth setup');
149
163
  return;
150
164
  }
151
165
 
@@ -6,16 +6,25 @@ import { formatDate } from '../utils.js';
6
6
 
7
7
  export async function balance(flags: Record<string, string | boolean>) {
8
8
  if (flags.help) {
9
- info('Usage: myapi billing balance\n\nShows your current account balance, credits, and payment method status.');
9
+ info('Usage: myapi billing balance [--json]\n\nShows your current account balance, credits, and payment method status.\n\nFlags:\n --json Output raw JSON');
10
10
  return;
11
11
  }
12
12
  const config = requireConfig();
13
13
  const result = await hq.getBalance(config.api_key);
14
14
 
15
+ if (flags.json) {
16
+ printJson({
17
+ balance: result.balance_display,
18
+ credits: result.credits_display,
19
+ has_payment_method: result.has_payment_method,
20
+ });
21
+ return;
22
+ }
23
+
15
24
  const pm = result.has_payment_method ? 'yes' : 'no';
16
25
  const accountType = config.is_anonymous ? 'anonymous' : `registered (${config.email ?? ''})`;
17
26
  info(`Account: ${accountType}`);
18
- info(`Balance: ${result.balance_display} | Credits: ${result.credits_display} | Payment method: ${pm}`);
27
+ info(`Balance: ${result.balance_display} | Credits: ${result.credits_display} (not usable for domains) | Payment method: ${pm}`);
19
28
  }
20
29
 
21
30
  export async function history(flags: Record<string, string | boolean>) {
@@ -48,7 +57,7 @@ export async function history(flags: Record<string, string | boolean>) {
48
57
 
49
58
  export async function topup(amountStr: string, flags: Record<string, string | boolean>) {
50
59
  if (flags.help) {
51
- info('Usage: myapi billing topup <amount>\n\nAmount is in whole dollars (e.g. "10" charges $10).\nUse --yes to skip confirmation.\nExample: myapi billing topup 10\n\nFlags:\n --yes Skip confirmation prompt');
60
+ info('Usage: myapi billing topup <amount>\n\nAmount is in whole dollars (e.g. "10" charges $10).\nConfirmation is required for amounts of $50 or more. Use --yes to skip it.\nExample: myapi billing topup 10\n\nFlags:\n --yes Skip confirmation prompt for large amounts');
52
61
  return;
53
62
  }
54
63
  const amount = Math.round(parseFloat(amountStr));
@@ -1,15 +1,21 @@
1
+ import { hq, funnel as sdkFunnel } from '@myapihq/sdk';
1
2
  import { requireConfig, saveConfig } from '../config.js';
2
3
  import { success, error, info } from '../output.js';
3
4
 
5
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+
4
7
  export async function setOrg(id: string, flags: Record<string, string | boolean>) {
5
8
  if (!id || flags.help) {
6
9
  info("Usage: myapi config set-org <id>\n\nSets a default organization ID for subsequent commands.");
7
10
  return;
8
11
  }
12
+ if (!UUID_RE.test(id)) error(`Invalid organization ID: "${id}". Expected a UUID (e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`);
9
13
  const config = requireConfig();
10
- config.default_org = id;
14
+ info('› Validating…');
15
+ const org = await hq.getOrg(config.api_key, id);
16
+ config.default_org = org.id;
11
17
  saveConfig(config);
12
- success(`Default organization set to: ${id}`);
18
+ success(`Default organization set to: ${org.name || org.id}`);
13
19
  }
14
20
 
15
21
  export async function setFunnel(id: string, flags: Record<string, string | boolean>) {
@@ -17,10 +23,15 @@ export async function setFunnel(id: string, flags: Record<string, string | boole
17
23
  info("Usage: myapi config set-funnel <id>\n\nSets a default funnel ID for subsequent commands.");
18
24
  return;
19
25
  }
26
+ if (!UUID_RE.test(id)) error(`Invalid funnel ID: "${id}". Expected a UUID (e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).`);
20
27
  const config = requireConfig();
21
- config.default_funnel = id;
28
+ const orgId = config.default_org;
29
+ if (!orgId) error('No default organization set. Run: myapi auth config set-org <id>');
30
+ info('› Validating…');
31
+ const funnel = await sdkFunnel.getFunnel(config.api_key, orgId, id);
32
+ config.default_funnel = funnel.id;
22
33
  saveConfig(config);
23
- success(`Default funnel set to: ${id}`);
34
+ success(`Default funnel set to: ${funnel.id}`);
24
35
  }
25
36
 
26
37
  export async function setDomain(domain: string, flags: Record<string, string | boolean>) {
@@ -46,8 +57,9 @@ export async function view(flags: Record<string, string | boolean>) {
46
57
  }
47
58
 
48
59
  export async function run(subcommand: string | undefined, args: string[], flags: Record<string, string | boolean>) {
60
+ const via = (flags._via as string) || 'auth config';
49
61
  if (!subcommand || (flags.help && !subcommand)) {
50
- info('Usage: myapi config <subcommand>\n\nSubcommands:\n view View current config\n set-org Set default organization\n set-funnel Set default funnel\n set-domain Set default domain');
62
+ info(`Usage: myapi ${via} <subcommand>\n\nSubcommands:\n view View current config\n set-org Set default organization\n set-funnel Set default funnel\n set-domain Set default domain`);
51
63
  return;
52
64
  }
53
65
 
@@ -1,6 +1,7 @@
1
1
  import { domain as sdkDomain } from '@myapihq/sdk';
2
2
  import { requireConfig } from '../config.js';
3
3
  import { success, error, info, printTable, printJson } from '../output.js';
4
+ import { formatDate } from '../utils.js';
4
5
 
5
6
  export async function check(domainArg: string, flags: Record<string, string | boolean>) {
6
7
  const config = requireConfig();
@@ -8,6 +9,10 @@ export async function check(domainArg: string, flags: Record<string, string | bo
8
9
  const domain = domainArg || (config.default_domain as string);
9
10
  if (!orgId || !domain) error("Missing required arguments.\nUsage: myapi domain check <domain> --org <id>\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
10
11
  const res = await sdkDomain.checkDomain(config.api_key, orgId, domain) as { available: boolean; price_cents: number; price_display?: string; message?: unknown };
12
+ if (flags.json) {
13
+ printJson(res);
14
+ return;
15
+ }
11
16
  if (res.available) {
12
17
  const price = res.price_display || `$${(res.price_cents / 100).toFixed(2)}`;
13
18
  info(`${domain} — Available ✓ ${price}/yr`);
@@ -24,7 +29,8 @@ export async function register(domainArg: string, flags: Record<string, string |
24
29
  if (!orgId || !domain) error("Missing required arguments.\nUsage: myapi domain register <domain> --org <id> [--years <num>]\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
25
30
  const years = parseInt(flags.years as string) || 1;
26
31
  const res = await sdkDomain.registerDomain(config.api_key, orgId, domain, years);
27
- success(`Registered ${domain}!\n${JSON.stringify(res, null, 2)}`);
32
+ success(`Registered ${domain}!`);
33
+ info(`Next: assign it to your org so your funnel goes live:\n myapi domain assign ${domain} --org ${orgId}`);
28
34
  }
29
35
 
30
36
  export async function importDomain(domainArg: string, flags: Record<string, string | boolean>) {
@@ -45,6 +51,7 @@ export async function list(flags: Record<string, string | boolean>) {
45
51
  if (!orgId) error("Missing required arguments.\nUsage: myapi domain list --org <id> [--filter=all|unassigned|org]\n(Set default: myapi auth config set-org <id>)");
46
52
  const filter = flags.filter as 'all' | 'unassigned' | 'org' | undefined;
47
53
  const domains = await sdkDomain.listDomains(config.api_key, orgId, filter);
54
+ if ((domains as unknown[]).length === 0) { info('No domains found in this organization. Register one with: myapi domain register <name>'); return; }
48
55
  printTable(domains as unknown as Record<string, unknown>[]);
49
56
  }
50
57
 
@@ -52,10 +59,9 @@ export async function assign(domainArg: string, flags: Record<string, string | b
52
59
  const config = requireConfig();
53
60
  const orgId = (flags.org as string) || config.default_org;
54
61
  const domain = domainArg || (config.default_domain as string);
55
- const targetOrgId = flags.target as string;
56
- if (!orgId || !domain || !targetOrgId) error("Missing required arguments.\nUsage: myapi domain assign <domain> --org <id> --target <target_org_id>\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
57
- await sdkDomain.assignDomain(config.api_key, orgId, domain, targetOrgId);
58
- success(`Assigned ${domain} to org ${targetOrgId}`);
62
+ if (!orgId || !domain) error("Missing required arguments.\nUsage: myapi domain assign <domain> --org <id>\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
63
+ await sdkDomain.assignDomain(config.api_key, orgId, domain);
64
+ success(`Assigned ${domain} to org ${orgId}`);
59
65
  }
60
66
 
61
67
  export async function unassign(domainArg: string, flags: Record<string, string | boolean>) {
@@ -73,12 +79,13 @@ export async function status(domainArg: string, flags: Record<string, string | b
73
79
  const domain = domainArg || (config.default_domain as string);
74
80
  if (!orgId || !domain) error("Missing required arguments.\nUsage: myapi domain status <domain> --org <id>\n(Set defaults: myapi auth config set-org <id> / myapi auth config set-domain <domain>)");
75
81
  const res = await sdkDomain.getDomainStatus(config.api_key, orgId, domain);
76
- const display: Record<string, unknown> = {
77
- domain: res.domain,
78
- status: res.status,
79
- };
80
- if (res.expires_at) display.expires_at = res.expires_at;
81
- printJson(display);
82
+ if (flags.json) {
83
+ printJson(res);
84
+ return;
85
+ }
86
+ info(`Domain: ${res.domain}`);
87
+ info(`Status: ${res.status}`);
88
+ if ((res as any).expires_at) info(`Expires: ${formatDate((res as any).expires_at)}`);
82
89
  }
83
90
 
84
91
  export async function settings(domainArg: string, flags: Record<string, string | boolean>) {
@@ -106,18 +113,18 @@ export async function updateSettings(domainArg: string, flags: Record<string, st
106
113
 
107
114
  export async function run(subcommand: string | undefined, args: string[], flags: Record<string, string | boolean>) {
108
115
  if (!subcommand || (flags.help && !subcommand)) {
109
- info('Usage: myapi domain <subcommand>\n\nSubcommands:\n list List domains\n check Check domain availability\n register Register a domain\n import Import an existing domain\n assign Assign domain to org\n unassign Unassign domain from its current org\n status Get domain status\n settings Get domain settings\n update-settings Update domain settings\n\nTip: Set defaults with "myapi auth config set-org <id>" to skip --org on every command.');
116
+ info('Usage: myapi domain <subcommand>\n\nSubcommands:\n list List domains\n check Check domain availability\n register Register a domain\n import Import an existing domain\n assign Assign domain to an org\n unassign Unassign domain from its current org\n status Get domain status\n settings Get domain settings\n update-settings Update domain settings\n\nTip: Set defaults with "myapi auth config set-org <id>" to skip --org on every command.');
110
117
  return;
111
118
  }
112
119
 
113
120
  if (flags.help) {
114
121
  if (subcommand === 'list') info('Usage: myapi domain list --org <id> [--filter=all|unassigned|org]\n\nLists all registered domains in your account.\n --filter=all Lists all domains owned by the account.\n --filter=unassigned Lists domains not assigned to an org.\n --filter=org (Default) Lists domains assigned to the current org.');
115
- else if (subcommand === 'check') info('Usage: myapi domain check <domain> --org <id>\n\nChecks if a domain name is available for registration.');
122
+ else if (subcommand === 'check') info('Usage: myapi domain check <domain> --org <id> [--json]\n\nChecks if a domain name is available for registration.\n\nFlags:\n --json Output raw JSON');
116
123
  else if (subcommand === 'register') info('Usage: myapi domain register <domain> --org <id> [--years <num>]\n\nRegisters a new domain name.');
117
124
  else if (subcommand === 'import') info('Usage: myapi domain import <domain> --org <id> --namecheap-user <user> --namecheap-key <key>\n\nImports an existing domain from Namecheap.');
118
- else if (subcommand === 'assign') info('Usage: myapi domain assign <domain> --org <id> --target <target_org_id>\n\nAssigns a domain to a different organization.');
125
+ else if (subcommand === 'assign') info('Usage: myapi domain assign <domain> --org <id>\n\nAssigns a domain to the specified organization.');
119
126
  else if (subcommand === 'unassign') info('Usage: myapi domain unassign <domain> --org <id>\n\nUnassigns a domain from its current organization.');
120
- else if (subcommand === 'status') info('Usage: myapi domain status <domain> --org <id>\n\nGets the registration status of a domain.');
127
+ else if (subcommand === 'status') info('Usage: myapi domain status <domain> --org <id> [--json]\n\nGets the registration status of a domain.\n\nFlags:\n --json Output raw JSON');
121
128
  else if (subcommand === 'settings') info('Usage: myapi domain settings <domain> --org <id>\n\nGets the DNS settings and configuration for a domain.');
122
129
  else if (subcommand === 'update-settings') info('Usage: myapi domain update-settings <domain> --org <id> [--security=essentially_off|medium|high|under_attack] [--browser-check=on|off] [--purge-cache]\n\nUpdates edge settings for a domain.');
123
130
  return;
@@ -1,6 +1,7 @@
1
1
  import { funnel as sdkFunnel, hq } from '@myapihq/sdk';
2
2
  import { requireConfig } from '../config.js';
3
3
  import { success, error, printTable, info, printJson } from '../output.js';
4
+ import { formatDate } from '../utils.js';
4
5
  import * as fs from 'fs';
5
6
 
6
7
  export async function list(flags: Record<string, string | boolean>) {
@@ -8,7 +9,9 @@ export async function list(flags: Record<string, string | boolean>) {
8
9
  const orgId = (flags.org as string) || config.default_org;
9
10
  if (!orgId) error("Missing required arguments.\nUsage: myapi funnel list --org <id>\n(Or set defaults via: myapi config set-org <id>)");
10
11
  const funnels = await sdkFunnel.listFunnels(config.api_key, orgId);
11
- printTable(funnels as unknown as Record<string, unknown>[]);
12
+ const rows = funnels.map(f => ({ id: f.id, created_at: formatDate(f.created_at), updated_at: formatDate(f.updated_at) }));
13
+ if (rows.length === 0) { info('No funnels found. Create one with: myapi funnel create'); return; }
14
+ printTable(rows as unknown as Record<string, unknown>[]);
12
15
  }
13
16
 
14
17
  export async function create(flags: Record<string, string | boolean>) {
@@ -45,7 +48,11 @@ export async function push(id: string, slug: string, flags: Record<string, strin
45
48
  const config = requireConfig();
46
49
  const orgId = (flags.org as string) || config.default_org;
47
50
  const funnelId = id || config.default_funnel;
48
- const finalSlug = slug || (flags.slug as string) || '/';
51
+ if (slug && flags.slug && slug !== flags.slug) {
52
+ info(`› Note: positional slug "${slug}" takes precedence over --slug "${flags.slug}".`);
53
+ }
54
+ const rawSlug = slug || (flags.slug as string) || '/';
55
+ const finalSlug = rawSlug.startsWith('/') ? rawSlug : `/${rawSlug}`;
49
56
 
50
57
  if (!orgId || !funnelId) {
51
58
  error("Missing required arguments.\nUsage: myapi funnel push [funnel_id] [slug] < index.html\n(Defaults to your configured funnel and slug '/' when omitted)");
@@ -59,7 +66,7 @@ export async function push(id: string, slug: string, flags: Record<string, strin
59
66
  process.stdin.on('error', reject);
60
67
  });
61
68
 
62
- if (!html) error("No HTML provided via stdin");
69
+ if (!html.trim()) error("No content provided via stdin. Usage: echo '<h1>Hello</h1>' | myapi funnel push [id] [slug]");
63
70
 
64
71
  const result = await sdkFunnel.pushFunnelPage(config.api_key, orgId, funnelId, { slug: finalSlug, html });
65
72
  success(`Pushed page to ${finalSlug}`);
@@ -1,6 +1,7 @@
1
1
  import { hq } from '@myapihq/sdk';
2
2
  import { requireConfig } from '../config.js';
3
3
  import { success, error, printTable, info, printJson } from '../output.js';
4
+ import { formatDate } from '../utils.js';
4
5
  import * as readline from 'readline';
5
6
 
6
7
  export async function createNew(flags: Record<string, string | boolean>) {
@@ -37,16 +38,8 @@ export async function list(flags: Record<string, string | boolean>) {
37
38
  }
38
39
 
39
40
  const formattedKeys = keysList.map(k => {
40
- // Format creation date
41
- const createdDate = new Date(k.created_at);
42
- const createdStr = isNaN(createdDate.getTime()) ? k.created_at : createdDate.toLocaleString();
43
-
44
- // Format last used date
45
- let lastUsedStr = 'Never';
46
- if (k.last_used_at) {
47
- const lastUsedDate = new Date(k.last_used_at);
48
- lastUsedStr = isNaN(lastUsedDate.getTime()) ? k.last_used_at : lastUsedDate.toLocaleString();
49
- }
41
+ const createdStr = formatDate(k.created_at);
42
+ const lastUsedStr = k.last_used_at ? formatDate(k.last_used_at) : 'Never';
50
43
 
51
44
  return {
52
45
  Name: k.name || 'Unnamed',
@@ -43,7 +43,7 @@ export async function list(flags: Record<string, string | boolean>) {
43
43
 
44
44
  export async function get(id: string, flags: Record<string, string | boolean>) {
45
45
  if (flags.help) {
46
- info('Usage: myapi org get <id>\n\nFetches details of a specific organization.');
46
+ info('Usage: myapi org get <id> [--json]\n\nFetches details of a specific organization.\n\nFlags:\n --json Output raw JSON');
47
47
  return;
48
48
  }
49
49
  if (!id) {
@@ -52,10 +52,15 @@ export async function get(id: string, flags: Record<string, string | boolean>) {
52
52
  }
53
53
  const config = requireConfig();
54
54
  const org = await hq.getOrg(config.api_key, id);
55
- printJson(org);
56
- if (org.preview_subdomain) {
57
- info(`Preview domain: https://${org.preview_subdomain}.makeautonomous.com`);
55
+ if (flags.json) {
56
+ printJson(org);
57
+ return;
58
58
  }
59
+ info(`ID: ${org.id}`);
60
+ info(`Name: ${org.name ?? '(none)'}`);
61
+ if ((org as any).tagline) info(`Tagline: ${(org as any).tagline}`);
62
+ if ((org as any).created_at) info(`Created: ${formatDate((org as any).created_at)}`);
63
+ if (org.preview_subdomain) info(`Preview: https://${org.preview_subdomain}.makeautonomous.com`);
59
64
  }
60
65
 
61
66
  export async function del(id: string, flags: Record<string, string | boolean>) {
package/src/index.ts CHANGED
@@ -18,12 +18,29 @@ import * as funnelCmd from './commands/funnel.js';
18
18
  import * as authCmd from './commands/auth.js';
19
19
  import * as configCmd from './commands/config.js';
20
20
 
21
+ const ERROR_MESSAGES: Record<string, string> = {
22
+ DOMAIN_NOT_FOUND: 'Domain not found.',
23
+ INVALID_DOMAIN: 'Invalid domain name.',
24
+ ORG_NOT_FOUND: 'Organization not found.',
25
+ org_not_found: 'Organization not found.',
26
+ FUNNEL_NOT_FOUND: 'Funnel not found.',
27
+ funnel_not_found: 'Funnel not found.',
28
+ db_error: 'Resource not found or invalid ID.',
29
+ NOT_FOUND: 'Resource not found.',
30
+ FORBIDDEN: 'You do not have permission to perform this action.',
31
+ RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
32
+ };
33
+
34
+ function friendlyError(code: string): string {
35
+ return ERROR_MESSAGES[code] || code;
36
+ }
37
+
21
38
  async function main() {
22
39
  const updatePromise = updateCmd.checkForUpdate(pkg.version).catch(() => {});
23
40
 
24
41
  const { args, flags } = parseArgs(process.argv.slice(2));
25
42
 
26
- if (flags.version || flags.v) {
43
+ if (flags.version || flags.v || flags.V) {
27
44
  const [latest] = await Promise.all([updateCmd.latestVersion(), updatePromise]);
28
45
  const updateNote = latest && updateCmd.isNewer(latest, pkg.version)
29
46
  ? ` (update available: ${latest})`
@@ -32,6 +49,11 @@ async function main() {
32
49
  return;
33
50
  }
34
51
 
52
+ if (flags.help && args.length === 0) {
53
+ printHelp();
54
+ return;
55
+ }
56
+
35
57
  if (args.length === 0) {
36
58
  const config = loadConfig();
37
59
  if (!config?.api_key) {
@@ -48,13 +70,13 @@ async function main() {
48
70
  switch (command) {
49
71
  case 'auth':
50
72
  if (!subcommand || (flags.help && !subcommand)) {
51
- info('Usage: myapi auth <subcommand>\n\nSubcommands:\n setup Configure your account\n import-key Import an existing API key non-interactively\n whoami Show current account\n link Link an email to upgrade your anonymous account\n switch [index] Switch between accounts\n config Manage CLI defaults (org_id, domain…)\n install-skills Install the MyAPI skills pack\n api-keys Manage API keys');
73
+ info('Usage: myapi auth <subcommand>\n\nSubcommands:\n setup Configure your account\n import-key Import an existing API key non-interactively\n whoami Show current account\n link [email] Attach an email to your anonymous account (upgrades it to registered)\n If the email already has an account, signs you into it as a second session\n switch [index] Switch between accounts\n config Manage CLI defaults (org_id, domain…)\n install-skills Install the MyAPI skills pack\n api-keys Manage API keys');
52
74
  break;
53
75
  }
54
76
  if (subcommand === 'setup') await setupCmd.setup(flags);
55
77
  else if (subcommand === 'import-key') await setupCmd.importKey(restArgs[0], flags);
56
78
  else if (subcommand === 'whoami') await authCmd.whoami(flags);
57
- else if (subcommand === 'link') await authCmd.link(flags);
79
+ else if (subcommand === 'link') await authCmd.link(flags, restArgs[0]);
58
80
  else if (subcommand === 'switch') await authCmd.switchCmd(flags, restArgs[0]);
59
81
  else if (subcommand === 'install-skills') {
60
82
  if (flags.help) {
@@ -111,8 +133,20 @@ async function main() {
111
133
  case 'setup':
112
134
  await setupCmd.setup(flags);
113
135
  break;
136
+ case 'whoami':
137
+ await authCmd.whoami(flags);
138
+ break;
139
+ case 'keys':
140
+ if (!subcommand) { info('Usage: myapi keys <list|create|revoke>'); break; }
141
+ if (subcommand === 'create') await keysCmd.createNew(flags);
142
+ else if (subcommand === 'list') await keysCmd.list(flags);
143
+ else if (subcommand === 'revoke') await keysCmd.revoke(restArgs[0], flags);
144
+ break;
114
145
  case 'config':
115
- await configCmd.run(subcommand, restArgs, flags);
146
+ await configCmd.run(subcommand, restArgs, { ...flags, _via: 'config' });
147
+ break;
148
+ case 'help':
149
+ printHelp();
116
150
  break;
117
151
  default:
118
152
  error(`Unknown command: ${command}. Run "myapi" for available commands.`);
@@ -125,7 +159,7 @@ async function main() {
125
159
  else if (err.code === 'NO_PAYMENT_METHOD') error('No payment method on file. Run: myapi billing setup');
126
160
  else error(`Insufficient balance. Run: myapi billing topup <amount>`);
127
161
  }
128
- else error(err.code || err.message);
162
+ else error(friendlyError(err.code) || err.message);
129
163
  } else {
130
164
  error(err.message || (typeof err === 'object' ? JSON.stringify(err) : String(err)));
131
165
  }
@@ -153,6 +187,12 @@ Commands:
153
187
  domain Manage domain configurations
154
188
  funnel Manage headless funnels and pages
155
189
 
190
+ Aliases:
191
+ whoami → myapi auth whoami
192
+ keys → myapi auth api-keys
193
+ setup → myapi auth setup
194
+ config → myapi auth config
195
+
156
196
  Run "myapi <command> --help" for subcommand help.
157
197
 
158
198
  ${quickStart}`);
package/src/output.ts CHANGED
@@ -1,9 +1,11 @@
1
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
2
+
1
3
  export function success(message: string): void {
2
- console.log(`\x1b[32m✓\x1b[0m ${message}`);
4
+ console.log(useColor ? `\x1b[32m✓\x1b[0m ${message}` : `✓ ${message}`);
3
5
  }
4
6
 
5
7
  export function error(message: string): never {
6
- console.error(`\x1b[31m✗\x1b[0m ${message}`);
8
+ console.error(useColor ? `\x1b[31m✗\x1b[0m ${message}` : `✗ ${message}`);
7
9
  process.exit(1);
8
10
  }
9
11
 
package/src/utils.ts CHANGED
@@ -4,7 +4,11 @@ export function parseArgs(argv: string[]) {
4
4
 
5
5
  for (let i = 0; i < argv.length; i++) {
6
6
  const arg = argv[i];
7
- if (arg.startsWith('--')) {
7
+ if (arg === '-h') {
8
+ flags['help'] = true;
9
+ } else if (arg === '-v') {
10
+ flags['version'] = true;
11
+ } else if (arg.startsWith('--')) {
8
12
  if (arg.includes('=')) {
9
13
  const [key, value] = arg.slice(2).split('=', 2);
10
14
  flags[key] = value;