@myapihq/cli 2.31.4 → 2.31.6
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/domain.js +3 -0
- package/dist/commands/email/message-textpart.test.d.ts +1 -0
- package/dist/commands/email/message-textpart.test.js +64 -0
- package/dist/commands/email/message.d.ts +1 -0
- package/dist/commands/email/message.js +27 -2
- package/dist/commands/fn-create-json.test.d.ts +1 -0
- package/dist/commands/fn-create-json.test.js +49 -0
- package/dist/commands/fn.js +9 -0
- package/dist/commands/pixel.js +3 -3
- package/dist/commands/status.js +5 -0
- package/dist/errors.js +3 -0
- package/dist/errors.test.js +13 -0
- package/dist/index.js +10 -2
- package/dist/registrant.js +16 -1
- package/dist/registrant.test.d.ts +1 -0
- package/dist/registrant.test.js +39 -0
- package/package.json +4 -2
package/dist/commands/domain.js
CHANGED
|
@@ -51,6 +51,9 @@ export const SCHEMA = {
|
|
|
51
51
|
'registrant-state': 'string',
|
|
52
52
|
'registrant-postal-code': 'string',
|
|
53
53
|
'registrant-country': 'string',
|
|
54
|
+
// Alias for --registrant-country, kept because account declared this
|
|
55
|
+
// spelling for the same field. See fromFlags in registrant.ts.
|
|
56
|
+
'registrant-country-code': 'string',
|
|
54
57
|
'registrant-organization': 'string',
|
|
55
58
|
// DNS records sub-surface
|
|
56
59
|
type: 'string',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// An html-only `email message send` arrived with an EMPTY text/plain part.
|
|
2
|
+
//
|
|
3
|
+
// The backend adds the multipart alternative but does not fill it (filed
|
|
4
|
+
// 2026-08-23, still open backend-side). Found live by the deep probe's email
|
|
5
|
+
// journey on its FIRST run with a real fixture, 2026-08-25 — the exact defect
|
|
6
|
+
// the step was written for, invisible to everything that does not read a
|
|
7
|
+
// delivered message. The CLI now derives the plain part from the HTML, so the
|
|
8
|
+
// customer-facing defect is closed regardless of when the backend fills its
|
|
9
|
+
// half.
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
11
|
+
const sdk = vi.hoisted(() => ({
|
|
12
|
+
email: { sendEmail: vi.fn(async () => ({ message_id: 'm-1' })) },
|
|
13
|
+
MyApiError: class MyApiError extends Error {
|
|
14
|
+
code = '';
|
|
15
|
+
status = 0;
|
|
16
|
+
},
|
|
17
|
+
// retryFunds in utils.js wraps sends with the 402 auto-recharge loop.
|
|
18
|
+
withFundsRetry: (fn) => fn(),
|
|
19
|
+
isInsufficientFunds: () => false,
|
|
20
|
+
autoRechargeState: () => undefined,
|
|
21
|
+
}));
|
|
22
|
+
vi.mock('@myapihq/sdk', () => sdk);
|
|
23
|
+
vi.mock('../../config.js', () => ({
|
|
24
|
+
requireConfig: () => ({ api_key: 'hq_live_test' }),
|
|
25
|
+
loadConfig: () => ({ api_key: 'hq_live_test' }),
|
|
26
|
+
CONFIG_DIR: '/tmp/nowhere',
|
|
27
|
+
}));
|
|
28
|
+
import { run, plainTextFromHtml } from './message.js';
|
|
29
|
+
import { withSink } from '../../output.js';
|
|
30
|
+
const SINK = { out: () => { }, err: () => { }, fail: (m) => { throw new Error(m); } };
|
|
31
|
+
describe('html-only send carries a derived text part', () => {
|
|
32
|
+
it('fills text from the html when --body is absent', async () => {
|
|
33
|
+
await withSink(SINK, () => run('send', [], {
|
|
34
|
+
from: 'a@x.co', to: 'b@x.co', subject: 's',
|
|
35
|
+
html: '<h1>Hello</h1><p>Two & two.</p>',
|
|
36
|
+
}));
|
|
37
|
+
const payload = sdk.email.sendEmail.mock.calls.at(-1)[1];
|
|
38
|
+
expect(payload.text).toBe('Hello\nTwo & two.');
|
|
39
|
+
expect(payload.html).toContain('<h1>');
|
|
40
|
+
});
|
|
41
|
+
it('never overwrites an explicit --body', async () => {
|
|
42
|
+
await withSink(SINK, () => run('send', [], {
|
|
43
|
+
from: 'a@x.co', to: 'b@x.co', subject: 's',
|
|
44
|
+
html: '<p>rich</p>', body: 'my own words',
|
|
45
|
+
}));
|
|
46
|
+
expect(sdk.email.sendEmail.mock.calls.at(-1)[1].text).toBe('my own words');
|
|
47
|
+
});
|
|
48
|
+
it('text-only send is untouched', async () => {
|
|
49
|
+
await withSink(SINK, () => run('send', [], {
|
|
50
|
+
from: 'a@x.co', to: 'b@x.co', subject: 's', body: 'plain',
|
|
51
|
+
}));
|
|
52
|
+
const payload = sdk.email.sendEmail.mock.calls.at(-1)[1];
|
|
53
|
+
expect(payload.text).toBe('plain');
|
|
54
|
+
expect(payload.html).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('plainTextFromHtml', () => {
|
|
58
|
+
it('turns block boundaries into line breaks and strips the rest', () => {
|
|
59
|
+
expect(plainTextFromHtml('<div>a</div><div>b<br>c</div>')).toBe('a\nb\nc');
|
|
60
|
+
});
|
|
61
|
+
it('unescapes the common entities', () => {
|
|
62
|
+
expect(plainTextFromHtml('<p><tag> "q" & more here</p>')).toBe('<tag> "q" & more here');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
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 plainTextFromHtml(html: string): string;
|
|
4
5
|
export declare const SUBCOMMAND_USAGE: Record<string, string>;
|
|
5
6
|
export declare function run(sub: string | undefined, args: string[], flags: Flags): Promise<void>;
|
|
@@ -21,6 +21,21 @@ function summarizeMessage(m) {
|
|
|
21
21
|
received_at: m.received_at,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
// Block-level tags become line breaks, everything else is stripped, entities
|
|
25
|
+
// beyond the common five are left alone. Exported for the test.
|
|
26
|
+
export function plainTextFromHtml(html) {
|
|
27
|
+
return html
|
|
28
|
+
.replace(/<\s*(br|\/p|\/div|\/h[1-6]|\/li|\/tr)[^>]*>/gi, '\n')
|
|
29
|
+
.replace(/<[^>]+>/g, '')
|
|
30
|
+
.replace(/ /g, ' ')
|
|
31
|
+
.replace(/&/g, '&')
|
|
32
|
+
.replace(/</g, '<')
|
|
33
|
+
.replace(/>/g, '>')
|
|
34
|
+
.replace(/"/g, '"')
|
|
35
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
36
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
24
39
|
async function send(flags) {
|
|
25
40
|
const config = requireConfig();
|
|
26
41
|
// typeof check (not truthiness): a bare `--to` whose value got swallowed by
|
|
@@ -41,12 +56,22 @@ async function send(flags) {
|
|
|
41
56
|
error('--template-vars must be valid JSON');
|
|
42
57
|
}
|
|
43
58
|
}
|
|
59
|
+
// An html-only send used to arrive with an EMPTY text/plain part: the
|
|
60
|
+
// backend adds the alternative but does not fill it (filed 2026-08-23,
|
|
61
|
+
// still open), so text-only clients and some spam filters saw a blank
|
|
62
|
+
// message. The API accepts `text`, so derive one instead of waiting.
|
|
63
|
+
// The stripper is deliberately crude — the plain part's job is to carry the
|
|
64
|
+
// words, not the layout.
|
|
65
|
+
const html = flags.html;
|
|
66
|
+
let text = flags.body;
|
|
67
|
+
if (html && !text)
|
|
68
|
+
text = plainTextFromHtml(html);
|
|
44
69
|
const res = await retryFunds(() => sdkEmail.sendEmail(config.api_key, {
|
|
45
70
|
from: flags.from,
|
|
46
71
|
to: [flags.to],
|
|
47
72
|
subject: flags.subject,
|
|
48
|
-
text
|
|
49
|
-
html
|
|
73
|
+
text,
|
|
74
|
+
html,
|
|
50
75
|
template_id: flags['template-id'],
|
|
51
76
|
template_vars: templateVars,
|
|
52
77
|
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// `fn create --json` printed the human text, JSON nowhere.
|
|
2
|
+
//
|
|
3
|
+
// The scoped API key is returned exactly ONCE, at create. A script running
|
|
4
|
+
// under --json got the prose rendering instead of a parseable object, so the
|
|
5
|
+
// one-shot key was lost to anything that was not a human reading a terminal —
|
|
6
|
+
// the same ignored---json class as `storage upload` and `mailbox create`.
|
|
7
|
+
// Found by examples/a-to-z/run.mjs on its first real run, 2026-08-25.
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
const sdk = vi.hoisted(() => ({
|
|
10
|
+
fn: { createFunction: vi.fn() },
|
|
11
|
+
MyApiError: class MyApiError extends Error {
|
|
12
|
+
code = '';
|
|
13
|
+
status = 0;
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('@myapihq/sdk', () => sdk);
|
|
17
|
+
vi.mock('../config.js', () => ({
|
|
18
|
+
requireConfig: () => ({ api_key: 'hq_live_test', default_org: 'o-1' }),
|
|
19
|
+
loadConfig: () => ({ api_key: 'hq_live_test', default_org: 'o-1' }),
|
|
20
|
+
CONFIG_DIR: '/tmp/nowhere',
|
|
21
|
+
}));
|
|
22
|
+
import { create } from './fn.js';
|
|
23
|
+
import { withSink } from '../output.js';
|
|
24
|
+
const RESULT = {
|
|
25
|
+
function: { id: 'fn-1', name: 'demo', trigger_type: 'http', invocation_url: null },
|
|
26
|
+
scoped_api_key: 'hq_live_once',
|
|
27
|
+
scoped_api_key_id: 'key-1',
|
|
28
|
+
};
|
|
29
|
+
describe('fn create honours --json', () => {
|
|
30
|
+
// stdout and stderr are separate contracts: JSON rides stdout, the
|
|
31
|
+
// org-context banner rides stderr. Mixing them here would re-create the
|
|
32
|
+
// parse bug this file exists to prevent.
|
|
33
|
+
const out = [];
|
|
34
|
+
const err = [];
|
|
35
|
+
const SINK = { out: (m) => out.push(m), err: (m) => err.push(m), fail: (m) => { throw new Error(m); } };
|
|
36
|
+
beforeEach(() => { out.length = 0; err.length = 0; sdk.fn.createFunction.mockResolvedValue(RESULT); });
|
|
37
|
+
it('emits the full result as parseable JSON, including the one-shot key', async () => {
|
|
38
|
+
await withSink(SINK, () => create('demo', { json: true, org: 'o-1' }));
|
|
39
|
+
const parsed = JSON.parse(out.join('\n'));
|
|
40
|
+
expect(parsed.scoped_api_key).toBe('hq_live_once');
|
|
41
|
+
expect(parsed.function.id).toBe('fn-1');
|
|
42
|
+
});
|
|
43
|
+
it('without --json, keeps the human rendering', async () => {
|
|
44
|
+
await withSink(SINK, () => create('demo', { org: 'o-1' }));
|
|
45
|
+
const text = [...out, ...err].join('\n');
|
|
46
|
+
expect(text).toContain('Function created: fn-1');
|
|
47
|
+
expect(() => JSON.parse(text)).toThrow();
|
|
48
|
+
});
|
|
49
|
+
});
|
package/dist/commands/fn.js
CHANGED
|
@@ -94,6 +94,15 @@ export async function create(nameArg, flags) {
|
|
|
94
94
|
payload.scopes = scopes;
|
|
95
95
|
}
|
|
96
96
|
const result = await sdkFn.createFunction(config.api_key, orgId, payload);
|
|
97
|
+
// --json must carry the whole result — including the scoped key, which is
|
|
98
|
+
// returned exactly once. A script that created a function under --json got
|
|
99
|
+
// the human text instead (found by examples/a-to-z/run.mjs, the same
|
|
100
|
+
// ignored---json class as storage upload and mailbox create), and the
|
|
101
|
+
// one-shot key was lost to anything that wasn't a human reading a terminal.
|
|
102
|
+
if (flags.json) {
|
|
103
|
+
printJson(result);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
97
106
|
success(`Function created: ${result.function.id}`);
|
|
98
107
|
info(`Name: ${result.function.name}`);
|
|
99
108
|
info(`Trigger: ${result.function.trigger_type}${result.function.cron_schedule ? ` (${result.function.cron_schedule})` : ''}`);
|
package/dist/commands/pixel.js
CHANGED
|
@@ -26,7 +26,7 @@ export async function interactions(flags) {
|
|
|
26
26
|
const config = requireConfig();
|
|
27
27
|
const orgId = flags.org || config.default_org;
|
|
28
28
|
if (!orgId) {
|
|
29
|
-
error("Missing required arguments.\nUsage: myapi pixel interactions --
|
|
29
|
+
error("Missing required arguments.\nUsage: myapi pixel interactions (--website <host> | --campaign-id <id> | --domain <domain>) [--org <id>]\n --website: a site YOU own, where your pixel runs · --domain: a domain your visitors went to\n(Or set defaults via: myapi config set-org <id>)");
|
|
30
30
|
}
|
|
31
31
|
const params = {};
|
|
32
32
|
if (flags.website)
|
|
@@ -44,7 +44,7 @@ export async function interactions(flags) {
|
|
|
44
44
|
if (flags.offset)
|
|
45
45
|
params.offset = parseInt(flags.offset, 10);
|
|
46
46
|
if (!params.website && !params.campaign_id && !params.domain) {
|
|
47
|
-
error("You must provide at least one filter
|
|
47
|
+
error("You must provide at least one filter.\n --website <host> a site you own (where your pixel runs)\n --campaign-id <id> one email campaign\n --domain <domain> a domain your visitors went to");
|
|
48
48
|
}
|
|
49
49
|
const res = await sdkPixel.getInteractions(config.api_key, orgId, params);
|
|
50
50
|
if (flags.json) {
|
|
@@ -195,7 +195,7 @@ Subcommands:
|
|
|
195
195
|
identify Link a known email/user id to an anonymous pixel visitor
|
|
196
196
|
identity Resolve the identity graph (emails, IPs, profiles) for a pixel ID
|
|
197
197
|
interactions Get a unified timeline of visits and events
|
|
198
|
-
(requires
|
|
198
|
+
(requires one of: --website = a site you own · --campaign-id · --domain = a visited domain)
|
|
199
199
|
visits Page-visit timeline scoped to a website host
|
|
200
200
|
|
|
201
201
|
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
package/dist/commands/status.js
CHANGED
|
@@ -113,8 +113,13 @@ export async function run(_subcommand, _args, flags = {}) {
|
|
|
113
113
|
info(`Recharge: ${summary}`);
|
|
114
114
|
}
|
|
115
115
|
if (Array.isArray(freeTier) && freeTier.length > 0) {
|
|
116
|
+
// A zero allowance rendered as "domain 0/0" reads like an error and says
|
|
117
|
+
// nothing a user can act on — there is no free tier for that slot, which
|
|
118
|
+
// is the platform's default, not this account's state. Show a slot only
|
|
119
|
+
// when there is an allowance to consume or usage to explain.
|
|
116
120
|
const parts = freeTier
|
|
117
121
|
.filter((e) => e && typeof e.service === 'string')
|
|
122
|
+
.filter((e) => (e.allowance ?? 0) > 0 || (e.used ?? 0) > 0)
|
|
118
123
|
.map((e) => `${e.service} ${e.used ?? 0}/${e.allowance ?? '?'}`);
|
|
119
124
|
if (parts.length > 0)
|
|
120
125
|
info(`FreeTier: ${parts.join(' · ')}`);
|
package/dist/errors.js
CHANGED
|
@@ -3,6 +3,9 @@ import { currentOrg } from './helpers.js';
|
|
|
3
3
|
// index.ts) so it can be unit-tested without importing the CLI entrypoint,
|
|
4
4
|
// which runs main() on import.
|
|
5
5
|
export const ERROR_MESSAGES = {
|
|
6
|
+
// 402, but not about money: the mailbox exists and the wallet is fine —
|
|
7
|
+
// outbound sending was never switched on for this address.
|
|
8
|
+
SENDING_NOT_ACTIVATED: 'This address cannot send yet — sending was never activated (this is not a billing problem). Run: myapi email mailbox activate-sending --address <email>',
|
|
6
9
|
DOMAIN_NOT_FOUND: 'Domain not found.',
|
|
7
10
|
INVALID_DOMAIN: 'Invalid domain name.',
|
|
8
11
|
ORG_NOT_FOUND: 'Organization not found.',
|
package/dist/errors.test.js
CHANGED
|
@@ -165,3 +165,16 @@ describe('5xx advice distinguishes a read from a write', () => {
|
|
|
165
165
|
expect(friendlyError(err())).toContain('went through');
|
|
166
166
|
});
|
|
167
167
|
});
|
|
168
|
+
// SENDING_NOT_ACTIVATED arrives as HTTP 402, and the 402 handler's fallback
|
|
169
|
+
// assumed any unrecognized 402 was an empty wallet — so a user with $92 of
|
|
170
|
+
// credits was told "Insufficient balance. Top up". Money pointed at a problem
|
|
171
|
+
// money cannot fix. Found live by the deep probe's email journey, 2026-08-25.
|
|
172
|
+
describe('402 that is not about money', () => {
|
|
173
|
+
it('SENDING_NOT_ACTIVATED names the activation command, not a top-up', () => {
|
|
174
|
+
const e = new MyApiError('SENDING_NOT_ACTIVATED', 402, 'Activate sending for this address via POST /email/sending/activate');
|
|
175
|
+
const msg = friendlyError(e);
|
|
176
|
+
expect(msg).toContain('activate-sending');
|
|
177
|
+
expect(msg).toContain('not a billing problem');
|
|
178
|
+
expect(msg).not.toContain('topup');
|
|
179
|
+
});
|
|
180
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { error, info, success, banner, setResolvedContextSource } from './output.js';
|
|
3
3
|
import { loadConfig } from './config.js';
|
|
4
|
-
import { MyApiError, setUserAgent, hq } from '@myapihq/sdk';
|
|
4
|
+
import { MyApiError, setUserAgent, hq, isInsufficientFunds } from '@myapihq/sdk';
|
|
5
5
|
import { friendlyError } from './errors.js';
|
|
6
6
|
import { currentOrg, adoptSoleOrg } from './helpers.js';
|
|
7
7
|
import * as fs from 'fs';
|
|
@@ -412,8 +412,16 @@ async function main() {
|
|
|
412
412
|
// Anonymous accounts can't top up (no payment surface) — link to unlock.
|
|
413
413
|
else if (cfg?.is_anonymous)
|
|
414
414
|
error('Insufficient balance. Anonymous accounts have no free credit — link an email to unlock $5: myapi account link <email>');
|
|
415
|
-
else
|
|
415
|
+
else if (!err.code || isInsufficientFunds(err)) {
|
|
416
416
|
error('Insufficient balance. Top up: myapi billing topup <amount> — or keep it funded automatically: myapi billing auto-recharge set');
|
|
417
|
+
}
|
|
418
|
+
// A 402 whose code is NOT about money. SENDING_NOT_ACTIVATED arrives
|
|
419
|
+
// as 402 and landed in the branch above, so a user with $92 of
|
|
420
|
+
// credits was told to top up — money pointed at a problem money
|
|
421
|
+
// cannot fix, the same shape as "Invalid API key → run setup" on the
|
|
422
|
+
// wrong-host bug. Show what the platform actually said.
|
|
423
|
+
else
|
|
424
|
+
error(friendlyError(err));
|
|
417
425
|
}
|
|
418
426
|
}
|
|
419
427
|
else
|
package/dist/registrant.js
CHANGED
|
@@ -21,6 +21,21 @@ import { saveConfig } from './config.js';
|
|
|
21
21
|
// whether the partial is "complete enough" (we require all six base
|
|
22
22
|
// fields plus country_code; state is optional).
|
|
23
23
|
function fromFlags(flags) {
|
|
24
|
+
// Two spellings exist in the wild: account's schema declared
|
|
25
|
+
// `registrant-country-code` while this reader only ever read
|
|
26
|
+
// `registrant-country` — so the -code spelling was typed, swallowed a value,
|
|
27
|
+
// and did nothing. A scripted `account registrant set` then failed with
|
|
28
|
+
// "country_code missing" while the country sat in argv. Accept both;
|
|
29
|
+
// `registrant-country` is canonical (matching the rest of the family, which
|
|
30
|
+
// carries no type suffixes). Both-with-different-values is refused rather
|
|
31
|
+
// than resolved by precedence.
|
|
32
|
+
const country = flags['registrant-country'];
|
|
33
|
+
const countryAlias = flags['registrant-country-code'];
|
|
34
|
+
if (typeof country === 'string' && typeof countryAlias === 'string' && country !== countryAlias) {
|
|
35
|
+
error(`--registrant-country ("${country}") and --registrant-country-code ("${countryAlias}") disagree — they are the same field. Pass one.`);
|
|
36
|
+
}
|
|
37
|
+
const countryCode = typeof country === 'string' ? country
|
|
38
|
+
: typeof countryAlias === 'string' ? countryAlias : undefined;
|
|
24
39
|
return {
|
|
25
40
|
name: typeof flags['registrant-name'] === 'string' ? flags['registrant-name'] : undefined,
|
|
26
41
|
email: typeof flags['registrant-email'] === 'string' ? flags['registrant-email'] : undefined,
|
|
@@ -29,7 +44,7 @@ function fromFlags(flags) {
|
|
|
29
44
|
city: typeof flags['registrant-city'] === 'string' ? flags['registrant-city'] : undefined,
|
|
30
45
|
state: typeof flags['registrant-state'] === 'string' ? flags['registrant-state'] : undefined,
|
|
31
46
|
postal_code: typeof flags['registrant-postal-code'] === 'string' ? flags['registrant-postal-code'] : undefined,
|
|
32
|
-
country_code:
|
|
47
|
+
country_code: countryCode,
|
|
33
48
|
organization: typeof flags['registrant-organization'] === 'string' ? flags['registrant-organization'] : undefined,
|
|
34
49
|
};
|
|
35
50
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { resolveRegistrantForRegister } from './registrant.js';
|
|
3
|
+
import { withSink, CommandFailed } from './output.js';
|
|
4
|
+
// error() exits the process unless a sink captures it — same pattern as
|
|
5
|
+
// dispatch-in-process.test.ts.
|
|
6
|
+
const SINK = { out: () => { }, err: () => { }, fail: (m) => { throw new CommandFailed(m); } };
|
|
7
|
+
// `--registrant-country-code` was declared in account's SCHEMA but read by
|
|
8
|
+
// nothing: fromFlags only ever looked at `registrant-country`. The -code
|
|
9
|
+
// spelling consumed a value and discarded it, so a scripted registrant set
|
|
10
|
+
// failed with "country_code missing" while the country sat in argv. Found
|
|
11
|
+
// 2026-08-24 by dumping every SCHEMA key and asking which have no consumer.
|
|
12
|
+
// Both spellings are read now; these tests pin that to behaviour, not to a
|
|
13
|
+
// schema entry a lint could satisfy without a consumer.
|
|
14
|
+
const BASE = {
|
|
15
|
+
'registrant-name': 'Ada Lovelace',
|
|
16
|
+
'registrant-email': 'ada@example.com',
|
|
17
|
+
'registrant-phone': '+44.2071234567',
|
|
18
|
+
'registrant-street': '12 Analytical Way',
|
|
19
|
+
'registrant-city': 'London',
|
|
20
|
+
'registrant-postal-code': 'EC1A 1AA',
|
|
21
|
+
};
|
|
22
|
+
const CONFIG = {};
|
|
23
|
+
describe('registrant country spellings', () => {
|
|
24
|
+
it('canonical --registrant-country reaches country_code', async () => {
|
|
25
|
+
const r = await resolveRegistrantForRegister({ ...BASE, 'registrant-country': 'GB' }, CONFIG);
|
|
26
|
+
expect(r.country_code).toBe('GB');
|
|
27
|
+
});
|
|
28
|
+
it('the alias --registrant-country-code reaches country_code too', async () => {
|
|
29
|
+
const r = await resolveRegistrantForRegister({ ...BASE, 'registrant-country-code': 'GB' }, CONFIG);
|
|
30
|
+
expect(r.country_code).toBe('GB');
|
|
31
|
+
});
|
|
32
|
+
it('both spellings with different values are refused, not resolved by precedence', async () => {
|
|
33
|
+
await expect(withSink(SINK, () => resolveRegistrantForRegister({ ...BASE, 'registrant-country': 'GB', 'registrant-country-code': 'DE' }, CONFIG))).rejects.toThrow(/disagree/);
|
|
34
|
+
});
|
|
35
|
+
it('both spellings agreeing is fine', async () => {
|
|
36
|
+
const r = await resolveRegistrantForRegister({ ...BASE, 'registrant-country': 'GB', 'registrant-country-code': 'GB' }, CONFIG);
|
|
37
|
+
expect(r.country_code).toBe('GB');
|
|
38
|
+
});
|
|
39
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.31.
|
|
4
|
+
"version": "2.31.6",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"lint:help-order": "node scripts/lint-help-order.js",
|
|
36
36
|
"lint:exposes": "node scripts/lint-exposes.js",
|
|
37
37
|
"lint:query-params": "node scripts/lint-query-params.js",
|
|
38
|
+
"lint:flag-lexicon": "node scripts/lint-flag-lexicon.js",
|
|
39
|
+
"lint:verb-symmetry": "node scripts/lint-verb-symmetry.js",
|
|
38
40
|
"lint:request-fields": "node scripts/lint-request-fields.js",
|
|
39
41
|
"audit:doctor": "npm run build && node scripts/audit-doctor.js",
|
|
40
42
|
"lint:docs": "node scripts/lint-docs.js",
|
|
@@ -47,7 +49,7 @@
|
|
|
47
49
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
48
50
|
},
|
|
49
51
|
"dependencies": {
|
|
50
|
-
"@myapihq/sdk": "^2.31.
|
|
52
|
+
"@myapihq/sdk": "^2.31.6"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@types/node": "^25.6.0",
|