@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.
- package/dist/commands/funnel.js +24 -5
- package/dist/commands/workflow.js +92 -15
- package/dist/flags.js +23 -1
- package/package.json +1 -1
package/dist/commands/funnel.js
CHANGED
|
@@ -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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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(
|
|
29
|
+
error(`${where}: must be a JSON object.`);
|
|
22
30
|
if (!s.type)
|
|
23
|
-
error(
|
|
31
|
+
error(`${where}: missing required field "type". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
|
|
24
32
|
if (!SUPPORTED_STEP_TYPES.includes(s.type)) {
|
|
25
|
-
error(
|
|
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
|
-
|
|
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.
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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":"
|
|
195
|
-
"subject":"Thanks!","
|
|
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
|
}
|