@myapihq/cli 1.1.0-wip.2 → 1.1.0-wip.3

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.
@@ -108,13 +108,27 @@ export async function push(slug, flags) {
108
108
  if (resolvedFromOrg) {
109
109
  info(`(Used the org's only funnel: ${funnelId}. Set a default with: myapi config set-funnel ${funnelId})`);
110
110
  }
111
- if (result?.url) {
112
- info(`Preview: ${result.url}`);
113
- }
114
- else {
111
+ let liveUrl = result?.url;
112
+ if (!liveUrl) {
115
113
  const org = await hq.getOrg(config.api_key, orgId);
116
114
  if (org.preview_subdomain) {
117
- info(`Preview: https://${org.preview_subdomain}.makeautonomous.com${finalSlug}`);
115
+ liveUrl = `https://${org.preview_subdomain}.makeautonomous.com${finalSlug}`;
116
+ }
117
+ }
118
+ if (liveUrl)
119
+ info(`Preview: ${liveUrl}`);
120
+ // If we're still on the preview subdomain, hint at how to serve on a
121
+ // custom domain. Tailor the hint: if a default_domain is already
122
+ // configured (registered + meant to be used), suggest assigning it
123
+ // directly; otherwise point at register + assign.
124
+ const onPreview = liveUrl?.includes('.makeautonomous.com');
125
+ if (onPreview) {
126
+ const dom = config.default_domain;
127
+ if (dom) {
128
+ info(`(Serving on the preview subdomain. To serve on ${dom} instead: myapi domain assign ${dom})`);
129
+ }
130
+ else {
131
+ info(`(Serving on the preview subdomain. To serve on a custom domain: myapi domain register <domain> && myapi domain assign <domain>)`);
118
132
  }
119
133
  }
120
134
  }
@@ -149,6 +163,11 @@ const SUBCOMMAND_USAGE = {
149
163
  Reads HTML from stdin and publishes to <slug> on your funnel. Funnel is resolved
150
164
  from --funnel, the default funnel, or (only if the org has exactly one) auto-picked.
151
165
 
166
+ By default, your funnel is served on a preview subdomain (*.makeautonomous.com).
167
+ To serve on your own domain, register and assign one with:
168
+ myapi domain register <domain>
169
+ myapi domain assign <domain>
170
+
152
171
  Examples:
153
172
  echo '<h1>Hello</h1>' | myapi funnel push /
154
173
  cat about.html | myapi funnel push /about
@@ -13,16 +13,56 @@ export const SCHEMA = {
13
13
  // forms are accepted by the workflow runner. Keep this in sync if the
14
14
  // backend grows new step types.
15
15
  const SUPPORTED_STEP_TYPES = ['send_email', 'email', 'slack_message', 'slack'];
16
+ const SLACK_HOOK_RE = /^https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+\/[A-Za-z0-9_-]+/;
17
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
18
+ // Per-step-type required fields. Validated client-side so typos and
19
+ // hallucinated shapes fail fast, before the network call. Backend
20
+ // performs the same validation as defense in depth.
16
21
  function validateSteps(steps) {
17
22
  if (!Array.isArray(steps))
18
23
  error('--steps must be a JSON array of step objects.');
24
+ if (steps.length === 0)
25
+ error('--steps cannot be an empty array.');
19
26
  steps.forEach((s, i) => {
27
+ const where = `step ${i}`;
20
28
  if (!s || typeof s !== 'object')
21
- error(`step ${i}: must be a JSON object.`);
29
+ error(`${where}: must be a JSON object.`);
22
30
  if (!s.type)
23
- error(`step ${i}: missing required field "type". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
31
+ error(`${where}: missing required field "type". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
24
32
  if (!SUPPORTED_STEP_TYPES.includes(s.type)) {
25
- error(`step ${i}: unknown type "${s.type}". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
33
+ error(`${where}: unknown type "${s.type}". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
34
+ }
35
+ if (s.type === 'send_email' || s.type === 'email') {
36
+ const required = ['from', 'to', 'subject'];
37
+ for (const f of required) {
38
+ if (!s[f] || typeof s[f] !== 'string') {
39
+ error(`${where} (${s.type}): missing required field "${f}".`);
40
+ }
41
+ }
42
+ const bodyForms = ['body', 'html', 'template_id'].filter(f => s[f] !== undefined && s[f] !== '');
43
+ if (bodyForms.length === 0) {
44
+ error(`${where} (${s.type}): must include exactly one of "body", "html", or "template_id".`);
45
+ }
46
+ if (bodyForms.length > 1) {
47
+ error(`${where} (${s.type}): can only use one of "body", "html", "template_id" — got: ${bodyForms.join(', ')}.`);
48
+ }
49
+ if (s.template_vars !== undefined && !s.template_id) {
50
+ error(`${where} (${s.type}): "template_vars" only makes sense with "template_id".`);
51
+ }
52
+ if (s.template_vars !== undefined && (typeof s.template_vars !== 'object' || Array.isArray(s.template_vars))) {
53
+ error(`${where} (${s.type}): "template_vars" must be a JSON object.`);
54
+ }
55
+ }
56
+ if (s.type === 'slack_message' || s.type === 'slack') {
57
+ if (!s.webhook_url || typeof s.webhook_url !== 'string') {
58
+ error(`${where} (${s.type}): missing required field "webhook_url".`);
59
+ }
60
+ if (!SLACK_HOOK_RE.test(s.webhook_url)) {
61
+ error(`${where} (${s.type}): "webhook_url" must look like https://hooks.slack.com/services/T.../B.../xxx — got "${s.webhook_url}".`);
62
+ }
63
+ if (!s.text || typeof s.text !== 'string') {
64
+ error(`${where} (${s.type}): missing required field "text".`);
65
+ }
26
66
  }
27
67
  });
28
68
  }
@@ -77,6 +117,9 @@ export async function create(nameArg, flags) {
77
117
  if (!name || !endpointId || !flags.steps) {
78
118
  error('Missing required arguments.\nUsage: myapi workflow create <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]\n or: myapi workflow create --name <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]');
79
119
  }
120
+ if (!UUID_RE.test(endpointId)) {
121
+ error(`Invalid --endpoint-id "${endpointId}". Expected a webhook endpoint UUID.\nList your endpoints with: myapi webhook list`);
122
+ }
80
123
  let steps;
81
124
  try {
82
125
  steps = JSON.parse(flags.steps);
@@ -107,8 +150,13 @@ export async function update(id, flags) {
107
150
  const payload = {};
108
151
  if (typeof flags.name === 'string')
109
152
  payload.name = flags.name;
110
- if (typeof flags['endpoint-id'] === 'string')
111
- payload.trigger_config = { endpoint_id: flags['endpoint-id'] };
153
+ if (typeof flags['endpoint-id'] === 'string') {
154
+ const eid = flags['endpoint-id'];
155
+ if (!UUID_RE.test(eid)) {
156
+ error(`Invalid --endpoint-id "${eid}". Expected a webhook endpoint UUID.\nList your endpoints with: myapi webhook list`);
157
+ }
158
+ payload.trigger_config = { endpoint_id: eid };
159
+ }
112
160
  if (typeof flags.steps === 'string') {
113
161
  try {
114
162
  payload.steps = JSON.parse(flags.steps);
@@ -180,19 +228,48 @@ const SUBCOMMAND_USAGE = {
180
228
 
181
229
  Either form works; the positional name is the recommended shape.
182
230
 
183
- --steps is a JSON array of step objects. Supported step types:
184
- send_email | email — fields: from, to, subject, body | template_id, template_vars
185
- slack_message | slack — fields: webhook_url, text
231
+ --steps is a JSON array of step objects. Each step has a "type" field
232
+ plus type-specific fields. Two types are supported today:
233
+
234
+ type: "send_email" (alias: "email")
235
+ Required:
236
+ from Sender mailbox address (must be activated for sending)
237
+ to Recipient address (templating allowed)
238
+ subject Email subject (templating allowed)
239
+ Body — exactly one of:
240
+ body Plain text body
241
+ html Raw HTML body
242
+ template_id UUID of an AI-generated template (myapi email template
243
+ generate). Recommended for nice-looking emails.
244
+ Optional:
245
+ template_vars JSON object of variable substitutions, only with
246
+ template_id (e.g. {"name": "{{ payload.name }}"}).
247
+
248
+ type: "slack_message" (alias: "slack")
249
+ Required:
250
+ webhook_url https://hooks.slack.com/services/T.../B.../xxx
251
+ Get one from https://api.slack.com/apps → your app →
252
+ Incoming Webhooks. Other URLs are rejected.
253
+ text Message text (templating allowed)
254
+
255
+ Templating: any string field can reference the incoming webhook payload
256
+ with {{ payload.field }} (whitespace optional). Example:
257
+ "to": "{{ payload.email }}" → the email field from the form submission.
258
+
259
+ Unknown step types AND missing/invalid fields are rejected at create
260
+ time — you find out about typos before any workflow run happens.
186
261
 
187
- Either alias works (e.g. type: "email" and type: "send_email" both run
188
- the same step). Field values support {{ payload.field }} templating to
189
- reference the incoming webhook payload, e.g. "to": "{{ payload.email }}".
190
- Unknown step types are rejected at create time, not at execute time.
262
+ There is NO on-failure notification mechanism today. Inspect run history
263
+ with: myapi workflow runs <id> · myapi workflow get-run <run_id>.
191
264
 
192
- Example — fire on every webhook POST, send a thank-you email:
265
+ Examples:
266
+ # send a thank-you email using an AI template, then ping Slack
193
267
  myapi workflow create "Contact handler" --endpoint-id <wid> --steps '[
194
- {"type":"send_email","from":"hello@x.com","to":"{{ payload.email }}",
195
- "subject":"Thanks!","body":"We got your message."}
268
+ {"type":"email","from":"hello@x.com","to":"{{ payload.email }}",
269
+ "subject":"Thanks!","template_id":"<tid>",
270
+ "template_vars":{"name":"{{ payload.name }}"}},
271
+ {"type":"slack","webhook_url":"https://hooks.slack.com/services/T0/B0/xxx",
272
+ "text":"New: {{ payload.message }}"}
196
273
  ]'
197
274
 
198
275
  See the my-workflow-api skill for the full form-to-email recipe.`,
package/dist/flags.js CHANGED
@@ -16,6 +16,7 @@ export function parseFlags(argv, schema = {}) {
16
16
  const merged = { ...GLOBAL_FLAGS, ...schema };
17
17
  const args = [];
18
18
  const flags = {};
19
+ const unknownFlags = [];
19
20
  for (let i = 0; i < argv.length; i++) {
20
21
  const arg = argv[i];
21
22
  if (arg === '-h') {
@@ -44,9 +45,24 @@ export function parseFlags(argv, schema = {}) {
44
45
  }
45
46
  const type = merged[key];
46
47
  // Unknown flags: tolerate as boolean (with --key=value still honored)
47
- // so additions to the schema don't silently break someone's script.
48
+ // so additions to the schema don't silently break someone's script
49
+ // BUT also collect them and emit a stderr warning at the end of
50
+ // parsing. Surfaces typos and AI-agent hallucinations like
51
+ // `--on-error <email>` without breaking back-compat.
48
52
  if (type === undefined) {
49
53
  flags[key] = inlineValue ?? true;
54
+ unknownFlags.push(`--${key}`);
55
+ // If no inline value and the next token doesn't look like another
56
+ // flag, the user almost certainly intended it as the flag's value
57
+ // (e.g. `--on-error someone@x.com`). Consume it so it doesn't
58
+ // become a stray positional arg later.
59
+ if (inlineValue === undefined) {
60
+ const next = argv[i + 1];
61
+ if (next !== undefined && !next.startsWith('--') && !next.startsWith('-h') && !next.startsWith('-v')) {
62
+ flags[key] = next;
63
+ i++;
64
+ }
65
+ }
50
66
  continue;
51
67
  }
52
68
  if (type === 'boolean') {
@@ -84,5 +100,11 @@ export function parseFlags(argv, schema = {}) {
84
100
  }
85
101
  args.push(arg);
86
102
  }
103
+ // Surface unknown flags so typos and hallucinated flags don't silently
104
+ // disappear. Don't block — the value is still in `flags` for any handler
105
+ // that wants it — just print one line on stderr.
106
+ if (unknownFlags.length > 0 && !process.env.MYAPI_QUIET_UNKNOWN_FLAGS) {
107
+ process.stderr.write(`› Note: ignoring unknown flag(s): ${unknownFlags.join(', ')}\n`);
108
+ }
87
109
  return { args, flags };
88
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
- "version": "1.1.0-wip.2",
3
+ "version": "1.1.0-wip.3",
4
4
  "description": "MyAPI command-line interface",
5
5
  "type": "module",
6
6
  "files": [