@myapihq/cli 1.1.0-wip.5 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/dist/commands/{email.d.ts → audience.d.ts} +2 -0
  2. package/dist/commands/audience.js +245 -0
  3. package/dist/commands/auth.d.ts +5 -2
  4. package/dist/commands/auth.js +133 -5
  5. package/dist/commands/billing.d.ts +2 -0
  6. package/dist/commands/billing.js +18 -1
  7. package/dist/commands/company.d.ts +6 -0
  8. package/dist/commands/company.js +149 -0
  9. package/dist/commands/config.d.ts +2 -0
  10. package/dist/commands/config.js +20 -0
  11. package/dist/commands/crm/companies.d.ts +6 -0
  12. package/dist/commands/crm/companies.js +193 -0
  13. package/dist/commands/crm/contacts.d.ts +6 -0
  14. package/dist/commands/crm/contacts.js +241 -0
  15. package/dist/commands/crm/index.d.ts +6 -0
  16. package/dist/commands/crm/index.js +74 -0
  17. package/dist/commands/database.d.ts +6 -0
  18. package/dist/commands/database.js +259 -0
  19. package/dist/commands/domain.d.ts +4 -0
  20. package/dist/commands/domain.js +166 -8
  21. package/dist/commands/email/campaign.d.ts +2 -0
  22. package/dist/commands/email/campaign.js +34 -1
  23. package/dist/commands/email/index.d.ts +2 -0
  24. package/dist/commands/email/index.js +9 -0
  25. package/dist/commands/email/mailbox.d.ts +2 -0
  26. package/dist/commands/email/mailbox.js +5 -0
  27. package/dist/commands/email/message.d.ts +2 -0
  28. package/dist/commands/email/message.js +8 -0
  29. package/dist/commands/email/template.d.ts +2 -0
  30. package/dist/commands/email/template.js +54 -0
  31. package/dist/commands/email/verify.d.ts +7 -0
  32. package/dist/commands/email/verify.js +52 -0
  33. package/dist/commands/email/warmup.d.ts +2 -0
  34. package/dist/commands/email/warmup.js +7 -0
  35. package/dist/commands/funnel.d.ts +3 -0
  36. package/dist/commands/funnel.js +31 -0
  37. package/dist/commands/image.d.ts +2 -0
  38. package/dist/commands/image.js +43 -9
  39. package/dist/commands/keys.d.ts +2 -0
  40. package/dist/commands/keys.js +5 -0
  41. package/dist/commands/llm.d.ts +6 -0
  42. package/dist/commands/llm.js +156 -0
  43. package/dist/commands/org.d.ts +3 -0
  44. package/dist/commands/org.js +55 -0
  45. package/dist/commands/people.d.ts +6 -0
  46. package/dist/commands/people.js +136 -0
  47. package/dist/commands/pixel.d.ts +11 -3
  48. package/dist/commands/pixel.js +93 -0
  49. package/dist/commands/setup.d.ts +2 -0
  50. package/dist/commands/setup.js +57 -5
  51. package/dist/commands/status.d.ts +7 -0
  52. package/dist/commands/status.js +154 -0
  53. package/dist/commands/storage.d.ts +2 -0
  54. package/dist/commands/storage.js +9 -1
  55. package/dist/commands/update.d.ts +2 -0
  56. package/dist/commands/update.js +3 -0
  57. package/dist/commands/url.d.ts +7 -2
  58. package/dist/commands/url.js +6 -0
  59. package/dist/commands/webhook.d.ts +2 -0
  60. package/dist/commands/webhook.js +31 -6
  61. package/dist/commands/workflow.d.ts +2 -0
  62. package/dist/commands/workflow.js +19 -1
  63. package/dist/completion.d.ts +3 -0
  64. package/dist/completion.js +78 -0
  65. package/dist/config.d.ts +2 -0
  66. package/dist/exposes.d.ts +2 -0
  67. package/dist/exposes.js +13 -0
  68. package/dist/exposes.test.js +99 -0
  69. package/dist/flags.test.js +84 -0
  70. package/dist/index.js +106 -14
  71. package/dist/prompt.d.ts +15 -0
  72. package/dist/prompt.js +27 -0
  73. package/dist/registrant.d.ts +6 -0
  74. package/dist/registrant.js +155 -0
  75. package/dist/services-sync.test.d.ts +1 -0
  76. package/dist/services-sync.test.js +32 -0
  77. package/dist/skills/my-api-hq/SKILL.md +51 -45
  78. package/dist/skills/my-crm-api/README.md +42 -0
  79. package/dist/skills/my-crm-api/SKILL.md +165 -0
  80. package/dist/skills/my-crm-api/claude/.claude-plugin/plugin.json +6 -0
  81. package/dist/skills/my-database-api/README.md +41 -0
  82. package/dist/skills/my-database-api/SKILL.md +107 -0
  83. package/dist/skills/my-database-api/claude/.claude-plugin/plugin.json +6 -0
  84. package/dist/skills/my-domain-api/SKILL.md +53 -26
  85. package/dist/skills/my-email-verify-api/README.md +33 -0
  86. package/dist/skills/my-email-verify-api/SKILL.md +91 -0
  87. package/dist/skills/my-email-verify-api/claude/.claude-plugin/plugin.json +6 -0
  88. package/dist/skills/my-funnel-api/SKILL.md +41 -37
  89. package/dist/skills/my-image-api/SKILL.md +48 -44
  90. package/dist/skills/my-llm-api/README.md +35 -0
  91. package/dist/skills/my-llm-api/SKILL.md +125 -0
  92. package/dist/skills/my-llm-api/claude/.claude-plugin/plugin.json +6 -0
  93. package/dist/skills/my-storage-api/README.md +1 -1
  94. package/dist/skills/my-storage-api/SKILL.md +44 -54
  95. package/dist/skills/my-webhook-api/README.md +1 -1
  96. package/dist/skills/my-webhook-api/SKILL.md +48 -92
  97. package/dist/skills/my-workflow-api/SKILL.md +46 -115
  98. package/dist/telemetry.d.ts +12 -0
  99. package/dist/telemetry.js +80 -0
  100. package/dist/telemetry.test.d.ts +1 -0
  101. package/dist/telemetry.test.js +101 -0
  102. package/package.json +13 -8
  103. package/dist/commands/email.js +0 -586
  104. package/dist/skills/my-email-api/README.md +0 -45
  105. package/dist/skills/my-email-api/SKILL.md +0 -104
  106. package/dist/skills/my-email-api/claude/.claude-plugin/plugin.json +0 -6
  107. package/dist/skills/my-email-api/make/.gitkeep +0 -0
  108. package/dist/skills/my-email-api/n8n/.gitkeep +0 -0
  109. package/dist/skills/my-email-api/openapi/.gitkeep +0 -0
  110. package/dist/skills/my-funnel-api/make/.gitkeep +0 -0
  111. package/dist/skills/my-funnel-api/n8n/.gitkeep +0 -0
  112. package/dist/skills/my-image-api/make/.gitkeep +0 -0
  113. package/dist/skills/my-image-api/n8n/.gitkeep +0 -0
  114. package/dist/skills/my-storage-api/make/.gitkeep +0 -0
  115. package/dist/skills/my-storage-api/n8n/.gitkeep +0 -0
  116. package/dist/skills/my-webhook-api/make/.gitkeep +0 -0
  117. package/dist/skills/my-webhook-api/n8n/.gitkeep +0 -0
  118. package/dist/skills/my-workflow-api/make/.gitkeep +0 -0
  119. package/dist/skills/my-workflow-api/n8n/.gitkeep +0 -0
  120. package/dist/utils.test.js +0 -48
  121. /package/dist/{utils.test.d.ts → exposes.test.d.ts} +0 -0
  122. /package/dist/skills/{my-api-hq/make → my-crm-api/openapi}/.gitkeep +0 -0
  123. /package/dist/skills/{my-api-hq/n8n → my-database-api/openapi}/.gitkeep +0 -0
  124. /package/dist/skills/{my-domain-api/make → my-email-verify-api/openapi}/.gitkeep +0 -0
  125. /package/dist/skills/{my-domain-api/n8n → my-llm-api/openapi}/.gitkeep +0 -0
@@ -70,4 +70,88 @@ describe('parseFlags', () => {
70
70
  const { flags } = parseFlags(['--mystery=42'], SCHEMA);
71
71
  expect(flags.mystery).toBe('42');
72
72
  });
73
+ // ── S-405: Strict flag validation behavior ────────────────────────────────
74
+ //
75
+ // The CLI is intentionally LENIENT on unknown flags: they're tolerated as
76
+ // boolean/string values, with a stderr warning. This is by design — agents
77
+ // hallucinate flags occasionally, and breaking their script with a hard
78
+ // error on `--unknown-flag` is worse UX than warning + continuing.
79
+ //
80
+ // These tests lock in the documented behavior so it can't drift.
81
+ it('unknown flag with a following value-shaped token consumes that token', () => {
82
+ const { args, flags } = parseFlags(['funnel', 'create', '--on-error', 'simon@x.com'], SCHEMA);
83
+ expect(flags['on-error']).toBe('simon@x.com');
84
+ expect(args).toEqual(['funnel', 'create']);
85
+ });
86
+ it('unknown flag with no following token defaults to boolean true', () => {
87
+ const { flags } = parseFlags(['--mystery'], SCHEMA);
88
+ expect(flags.mystery).toBe(true);
89
+ });
90
+ it('unknown flag does not consume a token that looks like another flag', () => {
91
+ const { flags } = parseFlags(['--mystery', '--json'], SCHEMA);
92
+ expect(flags.mystery).toBe(true);
93
+ expect(flags.json).toBe(true);
94
+ });
95
+ it('emits a stderr warning when unknown flags are present', () => {
96
+ const stderrChunks = [];
97
+ const origWrite = process.stderr.write.bind(process.stderr);
98
+ process.stderr.write = (chunk) => { stderrChunks.push(String(chunk)); return true; };
99
+ try {
100
+ parseFlags(['--mystery', '--also-unknown'], SCHEMA);
101
+ }
102
+ finally {
103
+ process.stderr.write = origWrite;
104
+ }
105
+ const out = stderrChunks.join('');
106
+ expect(out).toMatch(/ignoring unknown flag/i);
107
+ expect(out).toMatch(/--mystery/);
108
+ expect(out).toMatch(/--also-unknown/);
109
+ });
110
+ it('respects MYAPI_QUIET_UNKNOWN_FLAGS to suppress the warning', () => {
111
+ const stderrChunks = [];
112
+ const origWrite = process.stderr.write.bind(process.stderr);
113
+ const origEnv = process.env.MYAPI_QUIET_UNKNOWN_FLAGS;
114
+ process.env.MYAPI_QUIET_UNKNOWN_FLAGS = '1';
115
+ process.stderr.write = (chunk) => { stderrChunks.push(String(chunk)); return true; };
116
+ try {
117
+ parseFlags(['--mystery'], SCHEMA);
118
+ }
119
+ finally {
120
+ process.stderr.write = origWrite;
121
+ if (origEnv === undefined)
122
+ delete process.env.MYAPI_QUIET_UNKNOWN_FLAGS;
123
+ else
124
+ process.env.MYAPI_QUIET_UNKNOWN_FLAGS = origEnv;
125
+ }
126
+ const out = stderrChunks.join('');
127
+ expect(out).not.toMatch(/ignoring unknown flag/);
128
+ });
129
+ it('emits no warning when there are no unknown flags', () => {
130
+ const stderrChunks = [];
131
+ const origWrite = process.stderr.write.bind(process.stderr);
132
+ process.stderr.write = (chunk) => { stderrChunks.push(String(chunk)); return true; };
133
+ try {
134
+ parseFlags(['--name', 'Acme', '--yes'], SCHEMA);
135
+ }
136
+ finally {
137
+ process.stderr.write = origWrite;
138
+ }
139
+ expect(stderrChunks.join('')).toBe('');
140
+ });
141
+ it('declared number flag with non-numeric value falls back to the raw string', () => {
142
+ const { flags } = parseFlags(['--years', 'not-a-number'], SCHEMA);
143
+ expect(flags.years).toBe('not-a-number');
144
+ });
145
+ it('declared boolean flag with --key=false sets it to false', () => {
146
+ const { flags } = parseFlags(['--no-enable=false'], SCHEMA);
147
+ expect(flags['no-enable']).toBe(false);
148
+ });
149
+ it('declared boolean flag with --key=0 sets it to false', () => {
150
+ const { flags } = parseFlags(['--no-enable=0'], SCHEMA);
151
+ expect(flags['no-enable']).toBe(false);
152
+ });
153
+ it('declared boolean flag with --key=true is truthy', () => {
154
+ const { flags } = parseFlags(['--no-enable=true'], SCHEMA);
155
+ expect(flags['no-enable']).toBe(true);
156
+ });
73
157
  });
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { error, info, success } from './output.js';
2
+ import { error, info, success, banner } from './output.js';
3
3
  import { loadConfig } from './config.js';
4
4
  import { MyApiError } from '@myapihq/sdk';
5
5
  import * as fs from 'fs';
@@ -18,8 +18,18 @@ import * as workflowCmd from './commands/workflow.js';
18
18
  import * as emailCmd from './commands/email/index.js';
19
19
  import * as imageCmd from './commands/image.js';
20
20
  import * as storageCmd from './commands/storage.js';
21
+ import * as pixelCmd from './commands/pixel.js';
22
+ import * as urlCmd from './commands/url.js';
21
23
  import * as authCmd from './commands/auth.js';
22
24
  import * as configCmd from './commands/config.js';
25
+ import * as statusCmd from './commands/status.js';
26
+ import * as peopleCmd from './commands/people.js';
27
+ import * as companyCmd from './commands/company.js';
28
+ import * as audienceCmd from './commands/audience.js';
29
+ import * as llmCmd from './commands/llm.js';
30
+ import * as databaseCmd from './commands/database.js';
31
+ import * as crmCmd from './commands/crm/index.js';
32
+ import { initCompletion, installCompletion, uninstallCompletion } from './completion.js';
23
33
  // Each command file declares the value flags it understands. We union them
24
34
  // into a single schema for the upfront parse, so adding a new value flag in
25
35
  // one command means editing one file (its SCHEMA), not a global allowlist.
@@ -33,7 +43,16 @@ const COMBINED_SCHEMA = {
33
43
  ...imageCmd.SCHEMA,
34
44
  ...keysCmd.SCHEMA,
35
45
  ...orgCmd.SCHEMA,
46
+ ...pixelCmd.SCHEMA,
47
+ ...statusCmd.SCHEMA,
48
+ ...peopleCmd.SCHEMA,
49
+ ...companyCmd.SCHEMA,
50
+ ...audienceCmd.SCHEMA,
51
+ ...llmCmd.SCHEMA,
52
+ ...databaseCmd.SCHEMA,
53
+ ...crmCmd.SCHEMA,
36
54
  ...storageCmd.SCHEMA,
55
+ ...urlCmd.SCHEMA,
37
56
  ...webhookCmd.SCHEMA,
38
57
  ...workflowCmd.SCHEMA,
39
58
  // Top-level flags
@@ -62,6 +81,7 @@ const ERROR_MESSAGES = {
62
81
  RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
63
82
  INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
64
83
  INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
84
+ SERVICE_NOT_LAUNCHED: 'This service is disabled pre-launch. Track availability via: myapi status',
65
85
  // invalid_json_response intentionally absent — the SDK's MyApiError now
66
86
  // builds a useful detailed message for that case (status + URL + body
67
87
  // snippet), and friendlyError(err.code) would override it.
@@ -70,6 +90,9 @@ function friendlyError(code) {
70
90
  return ERROR_MESSAGES[code] || code;
71
91
  }
72
92
  async function main() {
93
+ // Shell autocomplete: if invoked by the shell with completion env vars,
94
+ // omelette short-circuits before normal dispatch. Always cheap on a real run.
95
+ initCompletion();
73
96
  const { args, flags } = parseFlags(process.argv.slice(2), COMBINED_SCHEMA);
74
97
  if (flags.version || flags.v || flags.V) {
75
98
  const latest = await updateCmd.latestVersion();
@@ -130,6 +153,30 @@ async function main() {
130
153
  case 'storage':
131
154
  await storageCmd.run(subcommand, restArgs, flags);
132
155
  break;
156
+ case 'pixel':
157
+ await pixelCmd.run(subcommand, restArgs, flags);
158
+ break;
159
+ case 'people':
160
+ await peopleCmd.run(subcommand, restArgs, flags);
161
+ break;
162
+ case 'company':
163
+ await companyCmd.run(subcommand, restArgs, flags);
164
+ break;
165
+ case 'audience':
166
+ await audienceCmd.run(subcommand, restArgs, flags);
167
+ break;
168
+ case 'llm':
169
+ await llmCmd.run(subcommand, restArgs, flags);
170
+ break;
171
+ case 'database':
172
+ await databaseCmd.run(subcommand, restArgs, flags);
173
+ break;
174
+ case 'crm':
175
+ await crmCmd.run(subcommand, restArgs, flags);
176
+ break;
177
+ case 'url':
178
+ await urlCmd.run(subcommand, restArgs, flags);
179
+ break;
133
180
  // Convenience aliases
134
181
  case 'setup':
135
182
  await setupCmd.setup(flags);
@@ -137,6 +184,9 @@ async function main() {
137
184
  case 'whoami':
138
185
  await authCmd.whoami(flags);
139
186
  break;
187
+ case 'status':
188
+ await statusCmd.run(subcommand, restArgs, flags);
189
+ break;
140
190
  case 'keys':
141
191
  await keysCmd.run(subcommand, restArgs, flags);
142
192
  break;
@@ -151,6 +201,20 @@ async function main() {
151
201
  await setupCmd.installSkills();
152
202
  success('› Skills installed.');
153
203
  break;
204
+ case 'completion':
205
+ // Order matters: omelette's setup/cleanup helpers call process.exit()
206
+ // unconditionally, so anything we want to show has to print first.
207
+ if (subcommand === 'uninstall') {
208
+ success('› Removing completion from shell init file…');
209
+ info(' Restart your shell (or `source ~/.bashrc` / `~/.zshrc`) to take effect.');
210
+ uninstallCompletion();
211
+ }
212
+ else {
213
+ success('› Installing tab completion…');
214
+ info(' Restart your shell (or `source ~/.bashrc` / `~/.zshrc`) to enable.');
215
+ installCompletion();
216
+ }
217
+ break;
154
218
  case 'help':
155
219
  await dispatchHelp(subcommand);
156
220
  break;
@@ -167,8 +231,16 @@ async function main() {
167
231
  error('A verified email is required. Run: myapi auth link');
168
232
  else if (err.code === 'NO_PAYMENT_METHOD')
169
233
  error('No payment method on file. Run: myapi billing setup');
170
- else
234
+ else {
235
+ // Anonymous accounts can't top up (no payment surface). The
236
+ // natural unblock is to link an email — the upgrade grants the
237
+ // free credit and opens the paid surface.
238
+ const cfg = loadConfig();
239
+ if (cfg?.is_anonymous) {
240
+ error('Insufficient balance. Anonymous accounts have no free credit — link an email to unlock $5: myapi auth link <email>');
241
+ }
171
242
  error('Insufficient balance. Run: myapi billing topup <amount>');
243
+ }
172
244
  }
173
245
  else
174
246
  error(friendlyError(err.code) || err.message);
@@ -193,6 +265,9 @@ async function dispatchAuth(subcommand, restArgs, flags) {
193
265
  case 'link': return authCmd.link(flags, restArgs[0]);
194
266
  case 'switch': return authCmd.switchCmd(flags, restArgs[0]);
195
267
  case 'install-skills':
268
+ // DEPRECATED: install-skills is now a top-level command. The auth subcommand
269
+ // form is kept for one minor version with a warning, then will be removed.
270
+ banner('› Note: `myapi auth install-skills` is deprecated. Use `myapi install-skills` instead.');
196
271
  if (flags.help) {
197
272
  info(authCmd.INSTALL_SKILLS_HELP);
198
273
  return;
@@ -201,6 +276,7 @@ async function dispatchAuth(subcommand, restArgs, flags) {
201
276
  success('› Skills installed.');
202
277
  return;
203
278
  case 'config': return configCmd.run(restArgs[0], restArgs.slice(1), flags);
279
+ case 'registrant': return authCmd.registrant(restArgs[0], flags);
204
280
  case 'api-keys':
205
281
  case 'keys': return keysCmd.runApiKeys(restArgs[0], restArgs.slice(1), flags);
206
282
  default: info('Unknown subcommand. Run: myapi auth --help');
@@ -214,6 +290,13 @@ const HELP_TARGETS = {
214
290
  email: f => emailCmd.run(undefined, [], f),
215
291
  image: f => imageCmd.run(undefined, [], f),
216
292
  storage: f => storageCmd.run(undefined, [], f),
293
+ status: f => statusCmd.run(undefined, [], f),
294
+ people: f => peopleCmd.run(undefined, [], f),
295
+ company: f => companyCmd.run(undefined, [], f),
296
+ audience: f => audienceCmd.run(undefined, [], f),
297
+ llm: f => llmCmd.run(undefined, [], f),
298
+ database: f => databaseCmd.run(undefined, [], f),
299
+ crm: f => crmCmd.run(undefined, [], f),
217
300
  org: f => orgCmd.run(undefined, [], f),
218
301
  billing: f => billingCmd.run(undefined, [], f),
219
302
  keys: f => keysCmd.run(undefined, [], f),
@@ -247,24 +330,33 @@ Usage: myapi <command> [subcommand] [args]
247
330
  myapi --version
248
331
 
249
332
  Commands:
250
- auth Manage account · setup · whoami · link
251
- billing Check balance and manage billing
252
- org Manage organizations (tip: myapi org create "name" --yes to auto-set as default)
253
- update Update CLI and skills to the latest version
254
- domain Manage domain configurations
255
- funnel Manage websites (publish pages, custom domains, funnels)
256
- webhook Manage inbound webhook endpoints and inspect deliveries
257
- email Manage mailboxes, send/read email, templates, and campaigns
258
- workflow Run actions (send email, post to Slack) when a webhook fires
259
- image Generate AI images and manage them in storage
260
- storage Upload, ingest, list, and serve assets from edge storage
333
+ auth Manage account · setup · whoami · link
334
+ status Single-screen view of account + resources in the default org
335
+ install-skills Install or update the MyAPI skills pack for AI agents
336
+ billing Check balance and manage billing
337
+ org Manage organizations (tip: myapi org create "name" --yes to auto-set as default)
338
+ update Update CLI and skills to the latest version
339
+ domain Manage domain configurations
340
+ funnel Manage websites (publish pages, custom domains, funnels)
341
+ webhook Manage inbound webhook endpoints and inspect deliveries
342
+ email Manage mailboxes, send/read email, templates, and campaigns
343
+ workflow Run actions (send email, post to Slack) when a webhook fires
344
+ image Generate AI images and manage them in storage
345
+ storage Upload, ingest, list, and serve assets from edge storage
346
+ pixel Read pixel analytics: visits, events, identity resolution
347
+ people Search the contact database (filter by industry, seniority, country, ...)
348
+ company Search the company database (filter by industry, size, country, ...)
349
+ audience Save filter snapshots as named audiences (people or companies)
350
+ llm Run LLM completions and embeddings (chat + embed, with usage/cost)
351
+ database KV store with namespaces + CAS — the substrate for stateful agent apps
352
+ crm Canonical store of engaged contacts + companies, with auto-ingest from webhooks
353
+ url Shorten URLs to compact myurlto.com links
261
354
 
262
355
  Aliases:
263
356
  whoami → myapi auth whoami
264
357
  keys → myapi auth api-keys
265
358
  setup → myapi auth setup
266
359
  config → myapi auth config
267
- install-skills → myapi auth install-skills
268
360
 
269
361
  Run "myapi <command> --help" for subcommand help.
270
362
 
package/dist/prompt.d.ts CHANGED
@@ -1,4 +1,19 @@
1
1
  import * as readline from 'readline';
2
+ /**
3
+ * True when no human is available to answer prompts.
4
+ *
5
+ * Detects three cases:
6
+ * - Piped / redirected stdin (most CI runners, shell pipes)
7
+ * - `CI=true` (GitHub Actions, GitLab CI, CircleCI, etc.)
8
+ * - `MYAPI_NON_INTERACTIVE=1` (explicit opt-out, used by agent harnesses
9
+ * that allocate a PTY for output but have no human at the keyboard)
10
+ *
11
+ * Some agent test-harnesses allocate a PTY for the child process so its
12
+ * coloured output renders correctly. In that case `process.stdin.isTTY`
13
+ * is true but there's no human to type 'y'/'n' — the prompt would hang
14
+ * until timeout. The env-var escape hatches handle that case.
15
+ */
16
+ export declare function isNonInteractive(): boolean;
2
17
  /**
3
18
  * Ask a free-form question on stdin. Returns the trimmed answer.
4
19
  *
package/dist/prompt.js CHANGED
@@ -1,4 +1,23 @@
1
1
  import * as readline from 'readline';
2
+ /**
3
+ * True when no human is available to answer prompts.
4
+ *
5
+ * Detects three cases:
6
+ * - Piped / redirected stdin (most CI runners, shell pipes)
7
+ * - `CI=true` (GitHub Actions, GitLab CI, CircleCI, etc.)
8
+ * - `MYAPI_NON_INTERACTIVE=1` (explicit opt-out, used by agent harnesses
9
+ * that allocate a PTY for output but have no human at the keyboard)
10
+ *
11
+ * Some agent test-harnesses allocate a PTY for the child process so its
12
+ * coloured output renders correctly. In that case `process.stdin.isTTY`
13
+ * is true but there's no human to type 'y'/'n' — the prompt would hang
14
+ * until timeout. The env-var escape hatches handle that case.
15
+ */
16
+ export function isNonInteractive() {
17
+ return (!process.stdin.isTTY ||
18
+ process.env.CI === 'true' ||
19
+ process.env.MYAPI_NON_INTERACTIVE === '1');
20
+ }
2
21
  /**
3
22
  * Ask a free-form question on stdin. Returns the trimmed answer.
4
23
  *
@@ -6,6 +25,14 @@ import * as readline from 'readline';
6
25
  * inline. Replaces five copies of the same boilerplate across commands.
7
26
  */
8
27
  export async function ask(question) {
28
+ // Non-interactive contexts (CI, agent subshells, piped stdin) used to
29
+ // hang here forever waiting for input that never arrives. Return an
30
+ // empty answer immediately so callers can fall back to their default.
31
+ if (isNonInteractive()) {
32
+ process.stdout.write(question);
33
+ process.stdout.write('\n');
34
+ return '';
35
+ }
9
36
  return withReadline(rl => new Promise(resolve => {
10
37
  rl.question(question, ans => resolve(ans.trim()));
11
38
  }));
@@ -0,0 +1,6 @@
1
+ import type { domain as sdkDomain } from '@myapihq/sdk';
2
+ import { type Config } from './config.js';
3
+ import type { Flags } from './helpers.js';
4
+ export type Registrant = sdkDomain.Registrant;
5
+ export declare function resolveRegistrantForRegister(flags: Flags, config: Config): Promise<Registrant>;
6
+ export declare function promptAndSave(config: Config, prefill?: Partial<Registrant>): Promise<Registrant>;
@@ -0,0 +1,155 @@
1
+ // Resolution + prompting helpers for the ICANN WHOIS registrant required
2
+ // at `myapi domain register` time.
3
+ //
4
+ // Sources, in priority order:
5
+ // 1. --registrant-json '<inline JSON>'
6
+ // 2. Per-field flags (--registrant-name, --registrant-email, …)
7
+ // 3. Stored on the active account (set via `myapi auth registrant set`)
8
+ // 4. Interactive TTY prompt — offers to save to config
9
+ // 5. Hard error in non-interactive contexts
10
+ //
11
+ // Validation is intentionally light. We trust the backend to enforce
12
+ // ICANN rules (E.164 phone, ISO country code, state-required-for-US/CA).
13
+ // Local validation here would silently diverge from server-side as rules
14
+ // evolve. We only catch obvious typos (empty required fields, invalid
15
+ // country length).
16
+ import { error } from './output.js';
17
+ import { ask, confirm, isNonInteractive } from './prompt.js';
18
+ import { saveConfig } from './config.js';
19
+ // ── Flag-based extraction ──────────────────────────────────────────────────
20
+ // Returns a partial registrant from per-field flags. Caller decides
21
+ // whether the partial is "complete enough" (we require all six base
22
+ // fields plus country_code; state is optional).
23
+ function fromFlags(flags) {
24
+ return {
25
+ name: typeof flags['registrant-name'] === 'string' ? flags['registrant-name'] : undefined,
26
+ email: typeof flags['registrant-email'] === 'string' ? flags['registrant-email'] : undefined,
27
+ phone: typeof flags['registrant-phone'] === 'string' ? flags['registrant-phone'] : undefined,
28
+ street: typeof flags['registrant-street'] === 'string' ? flags['registrant-street'] : undefined,
29
+ city: typeof flags['registrant-city'] === 'string' ? flags['registrant-city'] : undefined,
30
+ state: typeof flags['registrant-state'] === 'string' ? flags['registrant-state'] : undefined,
31
+ postal_code: typeof flags['registrant-postal-code'] === 'string' ? flags['registrant-postal-code'] : undefined,
32
+ country_code: typeof flags['registrant-country'] === 'string' ? flags['registrant-country'] : undefined,
33
+ organization: typeof flags['registrant-organization'] === 'string' ? flags['registrant-organization'] : undefined,
34
+ };
35
+ }
36
+ function fromJson(raw) {
37
+ try {
38
+ return JSON.parse(raw);
39
+ }
40
+ catch (e) {
41
+ error(`--registrant-json is not valid JSON: ${e.message}`);
42
+ }
43
+ }
44
+ // Merge sources, later sources win on a per-field basis. Empty strings are
45
+ // ignored so a partial flag set doesn't clobber config values.
46
+ function merge(...sources) {
47
+ const out = {};
48
+ for (const src of sources) {
49
+ if (!src)
50
+ continue;
51
+ for (const [k, v] of Object.entries(src)) {
52
+ if (v !== undefined && v !== '')
53
+ out[k] = v;
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+ // Required fields (state is conditional — backend validates US/CA specifically).
59
+ const REQUIRED = ['name', 'email', 'phone', 'street', 'city', 'postal_code', 'country_code'];
60
+ function missingFields(r) {
61
+ return REQUIRED.filter(k => !r[k]);
62
+ }
63
+ function validate(r) {
64
+ if (r.country_code.length !== 2) {
65
+ return `country_code "${r.country_code}" must be ISO 3166-1 alpha-2 (e.g. "US", "DE", "FR")`;
66
+ }
67
+ if ((r.country_code === 'US' || r.country_code === 'CA') && !r.state) {
68
+ return `state is required for US and CA registrants`;
69
+ }
70
+ return null;
71
+ }
72
+ // ── Interactive prompt ─────────────────────────────────────────────────────
73
+ const FIELDS = [
74
+ { key: 'name', label: 'Full name', required: true },
75
+ { key: 'email', label: 'Email', required: true },
76
+ { key: 'phone', label: 'Phone (E.164, e.g. +33612345678)', required: true },
77
+ { key: 'street', label: 'Street', required: true },
78
+ { key: 'city', label: 'City', required: true },
79
+ { key: 'postal_code', label: 'Postal code', required: true },
80
+ { key: 'country_code', label: 'Country code (ISO alpha-2, e.g. "US")', required: true },
81
+ { key: 'state', label: 'State (US/CA only — press Enter to skip)', required: false },
82
+ { key: 'organization', label: 'Organization (optional — press Enter to skip)', required: false },
83
+ ];
84
+ // Walk the field list interactively. `existing` fills the default for each
85
+ // prompt (Enter accepts) so the user can re-confirm without retyping.
86
+ async function promptRegistrant(existing = {}) {
87
+ const out = { ...existing };
88
+ for (const f of FIELDS) {
89
+ const current = out[f.key];
90
+ const suffix = current ? ` [${current}]` : '';
91
+ let ans = await ask(` ${f.label}${suffix}: `);
92
+ if (!ans && current)
93
+ ans = String(current);
94
+ if (!ans && f.required) {
95
+ error(`${f.key} is required.`);
96
+ }
97
+ if (ans)
98
+ out[f.key] = ans.trim();
99
+ }
100
+ // Validate the assembled record; bail with a clear message on common issues.
101
+ const reason = validate(out);
102
+ if (reason)
103
+ error(reason);
104
+ return out;
105
+ }
106
+ // ── Public resolver — called by `myapi domain register` ────────────────────
107
+ export async function resolveRegistrantForRegister(flags, config) {
108
+ // 1 + 2: flags
109
+ const jsonSource = typeof flags['registrant-json'] === 'string' ? fromJson(flags['registrant-json']) : undefined;
110
+ const flagSource = fromFlags(flags);
111
+ // 3: stored config
112
+ const stored = config.registrant;
113
+ // Merge: stored < json < per-flag (later wins).
114
+ const merged = merge(stored, jsonSource, flagSource);
115
+ const missing = missingFields(merged);
116
+ if (missing.length === 0) {
117
+ const reason = validate(merged);
118
+ if (reason)
119
+ error(reason);
120
+ return merged;
121
+ }
122
+ // 4: TTY prompt if available.
123
+ if (!isNonInteractive()) {
124
+ console.error('› Domain registration requires WHOIS contact info (ICANN). Filling in missing fields:');
125
+ const r = await promptRegistrant(merged);
126
+ if (!stored) {
127
+ const save = await confirm(' Save as your default for future domain registrations? (Y/n) ', true);
128
+ if (save) {
129
+ saveConfig({ ...config, registrant: r });
130
+ console.error(' ✓ Saved. Manage with: myapi auth registrant <get|clear>');
131
+ }
132
+ }
133
+ return r;
134
+ }
135
+ // 5: hard error.
136
+ error(`Missing registrant field(s): ${missing.join(', ')}\n\n` +
137
+ `Provide them via one of:\n` +
138
+ ` myapi auth registrant set (one-time interactive setup)\n` +
139
+ ` --registrant-json '{"name":"...","email":"...","phone":"...","street":"...","city":"...","postal_code":"...","country_code":"DE"}'\n` +
140
+ ` Per-field flags: --registrant-name, --registrant-email, --registrant-phone,\n` +
141
+ ` --registrant-street, --registrant-city, --registrant-postal-code,\n` +
142
+ ` --registrant-country, --registrant-state (US/CA only)`);
143
+ }
144
+ // ── Used by `myapi auth registrant set` ────────────────────────────────────
145
+ // Public so the auth subcommand can drive the same prompt sequence without
146
+ // going through the register flow.
147
+ export async function promptAndSave(config, prefill = {}) {
148
+ if (isNonInteractive()) {
149
+ error('myapi auth registrant set requires a TTY. Use --json \'{...}\' instead.');
150
+ }
151
+ console.error('› Setting your WHOIS registrant info (used at every domain register call):');
152
+ const r = await promptRegistrant(prefill);
153
+ saveConfig({ ...config, registrant: r });
154
+ return r;
155
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ // CI enforcement that every skills/<name>/claude/.claude-plugin/plugin.json
2
+ // is regenerable from packages/sdk/src/services.ts without drift.
3
+ //
4
+ // If this test fails, run from the repo root:
5
+ // node scripts/canonical-sync.mjs
6
+ //
7
+ // That regenerates the affected plugin.json files; commit and the test
8
+ // passes again.
9
+ import { describe, it, expect } from 'vitest';
10
+ import { execFileSync } from 'node:child_process';
11
+ import * as path from 'node:path';
12
+ import * as url from 'node:url';
13
+ const __filename = url.fileURLToPath(import.meta.url);
14
+ const REPO_ROOT = path.resolve(path.dirname(__filename), '..', '..', '..');
15
+ describe('canonical-sync: services.ts → skills/*/plugin.json', () => {
16
+ it('every plugin.json matches its services.ts entry', () => {
17
+ let out = '';
18
+ let exitCode = 0;
19
+ try {
20
+ out = execFileSync('node', ['scripts/canonical-sync.mjs', '--check'], {
21
+ cwd: REPO_ROOT,
22
+ encoding: 'utf-8',
23
+ stdio: ['ignore', 'pipe', 'pipe'],
24
+ });
25
+ }
26
+ catch (err) {
27
+ exitCode = err.status ?? 1;
28
+ out = (err.stdout?.toString() ?? '') + (err.stderr?.toString() ?? '');
29
+ }
30
+ expect(exitCode, `canonical-sync --check failed (exit ${exitCode}). Output:\n${out}\n\nRun \`node scripts/canonical-sync.mjs\` from the repo root to regenerate the affected plugin.json files, then commit.`).toBe(0);
31
+ });
32
+ });