@myapihq/cli 1.1.0-wip.0 → 1.1.0-wip.2
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/auth.js +75 -33
- package/dist/commands/billing.js +3 -5
- package/dist/commands/domain.js +8 -1
- package/dist/commands/email/campaign.d.ts +2 -0
- package/dist/commands/email/campaign.js +152 -0
- package/dist/commands/email/index.d.ts +4 -0
- package/dist/commands/email/index.js +98 -0
- package/dist/commands/email/mailbox.d.ts +2 -0
- package/dist/commands/email/mailbox.js +88 -0
- package/dist/commands/email/message.d.ts +2 -0
- package/dist/commands/email/message.js +115 -0
- package/dist/commands/email/template.d.ts +2 -0
- package/dist/commands/email/template.js +106 -0
- package/dist/commands/email/warmup.d.ts +2 -0
- package/dist/commands/email/warmup.js +43 -0
- package/dist/commands/email.js +71 -51
- package/dist/commands/funnel.d.ts +1 -1
- package/dist/commands/funnel.js +31 -9
- package/dist/commands/keys.d.ts +1 -1
- package/dist/commands/keys.js +6 -9
- package/dist/commands/org.js +17 -34
- package/dist/commands/setup.js +143 -115
- package/dist/commands/update.js +5 -0
- package/dist/commands/webhook.d.ts +1 -1
- package/dist/commands/webhook.js +23 -7
- package/dist/commands/workflow.d.ts +1 -1
- package/dist/commands/workflow.js +45 -6
- package/dist/config.js +10 -5
- package/dist/helpers.js +8 -6
- package/dist/index.js +9 -3
- package/dist/output.d.ts +3 -1
- package/dist/output.js +12 -0
- package/dist/prompt.d.ts +24 -0
- package/dist/prompt.js +41 -0
- package/dist/skills/my-email-api/README.md +1 -1
- package/dist/skills/my-email-api/SKILL.md +4 -6
- package/dist/skills/my-webhook-api/README.md +1 -1
- package/dist/skills/my-webhook-api/SKILL.md +77 -4
- package/dist/skills/my-workflow-api/README.md +3 -4
- package/dist/skills/my-workflow-api/SKILL.md +79 -10
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +32 -0
- package/package.json +1 -1
package/dist/helpers.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { error } from './output.js';
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// error() returns `never`, so after `if (!x) error(...)` TS narrows x to a
|
|
3
|
+
// non-falsy value and the casts disappear.
|
|
4
4
|
export function requireOrg(flags, config, usage) {
|
|
5
|
-
const orgId = flags.org || config.default_org;
|
|
6
|
-
if (!orgId
|
|
5
|
+
const orgId = (typeof flags.org === 'string' ? flags.org : '') || config.default_org;
|
|
6
|
+
if (!orgId) {
|
|
7
7
|
error(`Missing required arguments.\nUsage: ${usage}\n(Or set default: myapi config set-org <id>)`);
|
|
8
8
|
}
|
|
9
9
|
return orgId;
|
|
10
10
|
}
|
|
11
11
|
export function requireDomain(arg, flags, config, usage) {
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const fromFlag = typeof flags.domain === 'string' ? flags.domain : '';
|
|
13
|
+
const fromConfig = typeof config.default_domain === 'string' ? config.default_domain : '';
|
|
14
|
+
const domain = arg || fromFlag || fromConfig;
|
|
15
|
+
if (!domain) {
|
|
14
16
|
error(`Missing required arguments.\nUsage: ${usage}\n(Or set default: myapi config set-domain <domain>)`);
|
|
15
17
|
}
|
|
16
18
|
return domain;
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as domainCmd from './commands/domain.js';
|
|
|
15
15
|
import * as funnelCmd from './commands/funnel.js';
|
|
16
16
|
import * as webhookCmd from './commands/webhook.js';
|
|
17
17
|
import * as workflowCmd from './commands/workflow.js';
|
|
18
|
-
import * as emailCmd from './commands/email.js';
|
|
18
|
+
import * as emailCmd from './commands/email/index.js';
|
|
19
19
|
import * as authCmd from './commands/auth.js';
|
|
20
20
|
import * as configCmd from './commands/config.js';
|
|
21
21
|
// Each command file declares the value flags it understands. We union them
|
|
@@ -58,6 +58,9 @@ const ERROR_MESSAGES = {
|
|
|
58
58
|
RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
|
|
59
59
|
INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
|
|
60
60
|
INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
|
|
61
|
+
// invalid_json_response intentionally absent — the SDK's MyApiError now
|
|
62
|
+
// builds a useful detailed message for that case (status + URL + body
|
|
63
|
+
// snippet), and friendlyError(err.code) would override it.
|
|
61
64
|
};
|
|
62
65
|
function friendlyError(code) {
|
|
63
66
|
return ERROR_MESSAGES[code] || code;
|
|
@@ -161,7 +164,10 @@ async function main() {
|
|
|
161
164
|
error(friendlyError(err.code) || err.message);
|
|
162
165
|
}
|
|
163
166
|
else {
|
|
164
|
-
|
|
167
|
+
// Don't JSON.stringify Error instances — that returns "{}" because
|
|
168
|
+
// Error's enumerable surface is empty. Prefer .message; fall back to
|
|
169
|
+
// String() which Errors stringify reasonably ("Error: ...").
|
|
170
|
+
error(err?.message ?? String(err));
|
|
165
171
|
}
|
|
166
172
|
}
|
|
167
173
|
}
|
|
@@ -250,4 +256,4 @@ Run "myapi <command> --help" for subcommand help.
|
|
|
250
256
|
|
|
251
257
|
${quickStart}`);
|
|
252
258
|
}
|
|
253
|
-
main().catch(err => { error(err
|
|
259
|
+
main().catch(err => { error(err?.message ?? String(err)); });
|
package/dist/output.d.ts
CHANGED
|
@@ -6,10 +6,12 @@ export declare function error(message: string): never;
|
|
|
6
6
|
export declare function info(message: string): void;
|
|
7
7
|
export declare function banner(message: string): void;
|
|
8
8
|
export declare function printJson(data: unknown): void;
|
|
9
|
+
export declare function spinnerFrame(i: number): string;
|
|
10
|
+
export declare function clearLine(): void;
|
|
9
11
|
export interface PrintTableOptions {
|
|
10
12
|
/** Pass `flags` so `--json` (any truthy form) routes to JSON output. */
|
|
11
13
|
flags?: TableFlags;
|
|
12
14
|
/** Hint shown when the table is empty (instead of "No data found."). */
|
|
13
15
|
empty?: string;
|
|
14
16
|
}
|
|
15
|
-
export declare function printTable(rows:
|
|
17
|
+
export declare function printTable<T extends object>(rows: T[], opts?: PrintTableOptions): void;
|
package/dist/output.js
CHANGED
|
@@ -15,6 +15,18 @@ export function banner(message) {
|
|
|
15
15
|
export function printJson(data) {
|
|
16
16
|
console.log(JSON.stringify(data, null, 2));
|
|
17
17
|
}
|
|
18
|
+
// Spinner / line-clear primitives. Used by polling helpers (utils.pollJob)
|
|
19
|
+
// and any handler that wants its own progress UI.
|
|
20
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
21
|
+
export function spinnerFrame(i) {
|
|
22
|
+
return SPINNER_FRAMES[i % SPINNER_FRAMES.length];
|
|
23
|
+
}
|
|
24
|
+
export function clearLine() {
|
|
25
|
+
process.stdout.write('\r\x1b[K');
|
|
26
|
+
}
|
|
27
|
+
// Generic so callers don't need `as unknown as Record<string, unknown>[]`.
|
|
28
|
+
// Field names come from the first row; if SDK renames a field, the projector
|
|
29
|
+
// (passed in by the caller) is where it'll fail at compile time.
|
|
18
30
|
export function printTable(rows, opts = {}) {
|
|
19
31
|
if (opts.flags && opts.flags.json) {
|
|
20
32
|
printJson(rows);
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
/**
|
|
3
|
+
* Ask a free-form question on stdin. Returns the trimmed answer.
|
|
4
|
+
*
|
|
5
|
+
* Handles the readline lifecycle so callers don't open/close interfaces
|
|
6
|
+
* inline. Replaces five copies of the same boilerplate across commands.
|
|
7
|
+
*/
|
|
8
|
+
export declare function ask(question: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Yes/No confirm. `defaultYes` controls how Enter (no input) is interpreted.
|
|
11
|
+
*
|
|
12
|
+
* - `confirm('› Continue? (Y/n) ', true)` — Enter means yes.
|
|
13
|
+
* - `confirm('› Delete? (y/N) ', false)` — Enter means no.
|
|
14
|
+
*
|
|
15
|
+
* Accepted yes inputs: 'y', 'yes' (case-insensitive). Anything else is no
|
|
16
|
+
* when `defaultYes=false`; when `defaultYes=true`, only an explicit 'n' /
|
|
17
|
+
* 'no' counts as no, matching the existing Y/n convention in the CLI.
|
|
18
|
+
*/
|
|
19
|
+
export declare function confirm(question: string, defaultYes: boolean): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* Lower-level escape hatch: get a readline.Interface, do something with it,
|
|
22
|
+
* close it on the way out (even if your callback throws).
|
|
23
|
+
*/
|
|
24
|
+
export declare function withReadline<T>(fn: (rl: readline.Interface) => Promise<T>): Promise<T>;
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
/**
|
|
3
|
+
* Ask a free-form question on stdin. Returns the trimmed answer.
|
|
4
|
+
*
|
|
5
|
+
* Handles the readline lifecycle so callers don't open/close interfaces
|
|
6
|
+
* inline. Replaces five copies of the same boilerplate across commands.
|
|
7
|
+
*/
|
|
8
|
+
export async function ask(question) {
|
|
9
|
+
return withReadline(rl => new Promise(resolve => {
|
|
10
|
+
rl.question(question, ans => resolve(ans.trim()));
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Yes/No confirm. `defaultYes` controls how Enter (no input) is interpreted.
|
|
15
|
+
*
|
|
16
|
+
* - `confirm('› Continue? (Y/n) ', true)` — Enter means yes.
|
|
17
|
+
* - `confirm('› Delete? (y/N) ', false)` — Enter means no.
|
|
18
|
+
*
|
|
19
|
+
* Accepted yes inputs: 'y', 'yes' (case-insensitive). Anything else is no
|
|
20
|
+
* when `defaultYes=false`; when `defaultYes=true`, only an explicit 'n' /
|
|
21
|
+
* 'no' counts as no, matching the existing Y/n convention in the CLI.
|
|
22
|
+
*/
|
|
23
|
+
export async function confirm(question, defaultYes) {
|
|
24
|
+
const ans = (await ask(question)).toLowerCase();
|
|
25
|
+
if (defaultYes)
|
|
26
|
+
return ans !== 'n' && ans !== 'no';
|
|
27
|
+
return ans === 'y' || ans === 'yes';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Lower-level escape hatch: get a readline.Interface, do something with it,
|
|
31
|
+
* close it on the way out (even if your callback throws).
|
|
32
|
+
*/
|
|
33
|
+
export async function withReadline(fn) {
|
|
34
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
35
|
+
try {
|
|
36
|
+
return await fn(rl);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
rl.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -16,7 +16,7 @@ Send transactional email and run drip campaigns from mailboxes on your own regis
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
# Create a mailbox + activate sending
|
|
19
|
-
myapi email mailbox create
|
|
19
|
+
myapi email mailbox create hello@yourdomain.com
|
|
20
20
|
myapi email mailbox activate-sending --address hello@yourdomain.com
|
|
21
21
|
|
|
22
22
|
# Send
|
|
@@ -19,7 +19,7 @@ Account-scoped email infrastructure tied to your registered domains. Mailboxes b
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
# 1. Create a mailbox on your registered domain
|
|
22
|
-
myapi email mailbox create
|
|
22
|
+
myapi email mailbox create hello@yourdomain.com --display-name "Hello"
|
|
23
23
|
|
|
24
24
|
# 2. Activate sending (required before send/campaign)
|
|
25
25
|
myapi email mailbox activate-sending --address hello@yourdomain.com
|
|
@@ -50,9 +50,8 @@ myapi email message outbox hello@yourdomain.com
|
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
# Generate a template — polls the LLM job (up to 90s)
|
|
53
|
-
myapi email template generate \
|
|
54
|
-
--prompt "A welcome email with our brand colors and a CTA to /onboarding"
|
|
55
|
-
--name welcome-v1
|
|
53
|
+
myapi email template generate welcome-v1 \
|
|
54
|
+
--prompt "A welcome email with our brand colors and a CTA to /onboarding"
|
|
56
55
|
|
|
57
56
|
# List, edit, delete
|
|
58
57
|
myapi email template list
|
|
@@ -67,8 +66,7 @@ Campaigns send a template to a contact list at a per-day rate.
|
|
|
67
66
|
|
|
68
67
|
```bash
|
|
69
68
|
# 1. Create a campaign linked to an existing template + sender mailbox
|
|
70
|
-
myapi email campaign create \
|
|
71
|
-
--name "Welcome series" \
|
|
69
|
+
myapi email campaign create "Welcome series" \
|
|
72
70
|
--template-id <template_id> \
|
|
73
71
|
--from hello@yourdomain.com \
|
|
74
72
|
--per-day 50
|
|
@@ -14,7 +14,7 @@ Inbound HTTP endpoints that receive POSTs from third-party services and store ev
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
# Create an endpoint
|
|
17
|
-
myapi webhook create
|
|
17
|
+
myapi webhook create "stripe-events"
|
|
18
18
|
# → prints: Inbound URL: https://api.myapihq.com/webhook/in/<slug>
|
|
19
19
|
|
|
20
20
|
# POST to it from anywhere
|
|
@@ -19,7 +19,7 @@ Per-org HTTP endpoints that accept inbound POSTs and durably store every deliver
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
# 1. Create a webhook endpoint
|
|
22
|
-
myapi webhook create
|
|
22
|
+
myapi webhook create "stripe-events" --description "Stripe payment events"
|
|
23
23
|
# → prints: Inbound URL: https://api.myapihq.com/webhook/in/<slug>
|
|
24
24
|
|
|
25
25
|
# 2. POST to the inbound URL from your service
|
|
@@ -47,15 +47,88 @@ Webhooks become useful when a workflow fires on each inbound POST. See **myworkf
|
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
49
|
# Create the webhook
|
|
50
|
-
WID=$(myapi webhook create
|
|
50
|
+
WID=$(myapi webhook create "leads" --json | jq -r .id)
|
|
51
51
|
|
|
52
52
|
# Bind a workflow that sends an email each time the webhook fires
|
|
53
|
-
myapi workflow create \
|
|
54
|
-
--name "Notify on lead" \
|
|
53
|
+
myapi workflow create "Notify on lead" \
|
|
55
54
|
--endpoint-id $WID \
|
|
56
55
|
--steps '[{"type":"send_email","to":"sales@yourdomain.com","subject":"New lead"}]'
|
|
57
56
|
```
|
|
58
57
|
|
|
58
|
+
## Payload contract
|
|
59
|
+
|
|
60
|
+
The inbound URL accepts **any JSON body** — there's no enforced schema, no
|
|
61
|
+
required fields. Whatever you POST is stored verbatim alongside the headers
|
|
62
|
+
and a timestamp. The shape is whatever the sender chooses to send.
|
|
63
|
+
|
|
64
|
+
That has two consequences:
|
|
65
|
+
|
|
66
|
+
1. **You decide the shape.** A form, a Stripe event, a custom payload — all
|
|
67
|
+
work. Be consistent within one webhook so downstream workflows can
|
|
68
|
+
reference fields reliably.
|
|
69
|
+
2. **Workflows reference fields with `{{ payload.field }}` templating.**
|
|
70
|
+
If your form POSTs `{"email": "alice@x.com", "message": "hi"}`, a
|
|
71
|
+
workflow step can do `--to "{{ payload.email }}"` to send a reply to
|
|
72
|
+
whoever submitted.
|
|
73
|
+
|
|
74
|
+
Inbound responses: 200 OK on success, 4xx if the endpoint is missing or
|
|
75
|
+
the body isn't valid JSON.
|
|
76
|
+
|
|
77
|
+
## End-to-end recipe — contact form on your website
|
|
78
|
+
|
|
79
|
+
Goal: a contact form on yoursite.com. When someone submits, send a
|
|
80
|
+
thank-you email to them and a Slack ping to your team.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 1. Create a webhook to receive form submissions
|
|
84
|
+
WID=$(myapi webhook create "contact-form" --json | jq -r .id)
|
|
85
|
+
WURL=$(myapi webhook list --json | jq -r ".[] | select(.id==\"$WID\") | .url")
|
|
86
|
+
|
|
87
|
+
# 2. Create the workflow that fires on every submission
|
|
88
|
+
myapi workflow create "Contact form handler" \
|
|
89
|
+
--endpoint-id $WID \
|
|
90
|
+
--steps "$(cat <<'JSON'
|
|
91
|
+
[
|
|
92
|
+
{
|
|
93
|
+
"type": "send_email",
|
|
94
|
+
"from": "hello@yoursite.com",
|
|
95
|
+
"to": "{{ payload.email }}",
|
|
96
|
+
"subject": "Thanks for reaching out, {{ payload.name }}!",
|
|
97
|
+
"body": "We got your message and will reply within 24 hours."
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"type": "slack",
|
|
101
|
+
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
102
|
+
"text": "New contact form submission from {{ payload.name }} ({{ payload.email }}): {{ payload.message }}"
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
JSON
|
|
106
|
+
)"
|
|
107
|
+
|
|
108
|
+
# 3. Drop this form in your HTML — it POSTs JSON to the webhook
|
|
109
|
+
echo "<form id='contact'>
|
|
110
|
+
<input name='name' required>
|
|
111
|
+
<input name='email' type='email' required>
|
|
112
|
+
<textarea name='message' required></textarea>
|
|
113
|
+
<button>Send</button>
|
|
114
|
+
</form>
|
|
115
|
+
<script>
|
|
116
|
+
document.getElementById('contact').addEventListener('submit', async e => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
const data = Object.fromEntries(new FormData(e.target));
|
|
119
|
+
await fetch('$WURL', {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {'Content-Type': 'application/json'},
|
|
122
|
+
body: JSON.stringify(data),
|
|
123
|
+
});
|
|
124
|
+
alert('Thanks! Check your inbox.');
|
|
125
|
+
});
|
|
126
|
+
</script>"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The form sends `{"name": "...", "email": "...", "message": "..."}` →
|
|
130
|
+
webhook stores it → workflow fires → email goes out → Slack gets pinged.
|
|
131
|
+
|
|
59
132
|
## Notes
|
|
60
133
|
|
|
61
134
|
- The inbound URL is public — anyone with the URL can POST to it. Treat the slug as a secret.
|
|
@@ -6,7 +6,7 @@ Run a chain of actions (send email, post to Slack, call an HTTP endpoint) every
|
|
|
6
6
|
## What it does
|
|
7
7
|
|
|
8
8
|
- Bind ordered step chains to webhook endpoints
|
|
9
|
-
- Steps can: send transactional email,
|
|
9
|
+
- Steps can: send transactional email, post to Slack
|
|
10
10
|
- Template values from the inbound webhook payload (`{{ payload.field }}`)
|
|
11
11
|
- Per-run status tracking (attempt, error, started/finished timestamps)
|
|
12
12
|
- Enable/disable without losing config
|
|
@@ -15,9 +15,8 @@ Run a chain of actions (send email, post to Slack, call an HTTP endpoint) every
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
# Create a webhook + workflow that sends Slack on every POST
|
|
18
|
-
WID=$(myapi webhook create
|
|
19
|
-
myapi workflow create \
|
|
20
|
-
--name "Slack on lead" \
|
|
18
|
+
WID=$(myapi webhook create "leads" --json | jq -r .id)
|
|
19
|
+
myapi workflow create "Slack on lead" \
|
|
21
20
|
--endpoint-id $WID \
|
|
22
21
|
--steps '[{"type":"slack","webhook_url":"..."}]'
|
|
23
22
|
```
|
|
@@ -18,11 +18,10 @@ Workflows bind a list of steps to a webhook endpoint. Every time the webhook rec
|
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
# 1. Create the webhook that will trigger the workflow
|
|
21
|
-
WID=$(myapi webhook create
|
|
21
|
+
WID=$(myapi webhook create "leads" --json | jq -r .id)
|
|
22
22
|
|
|
23
23
|
# 2. Create a workflow bound to that webhook
|
|
24
|
-
myapi workflow create \
|
|
25
|
-
--name "Slack on new lead" \
|
|
24
|
+
myapi workflow create "Slack on new lead" \
|
|
26
25
|
--endpoint-id $WID \
|
|
27
26
|
--steps '[{"type":"slack","webhook_url":"https://hooks.slack.com/..."}]'
|
|
28
27
|
|
|
@@ -52,31 +51,101 @@ myapi workflow get-run <run_id>
|
|
|
52
51
|
|
|
53
52
|
`--steps` is a JSON array of step objects. Each step has a `type` and type-specific fields.
|
|
54
53
|
|
|
54
|
+
Supported step types (alias forms in parentheses):
|
|
55
|
+
|
|
56
|
+
| `type` | Aliases | Required fields |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `send_email` | `email` | `from`, `to`, `subject`, plus one of `body` / `html` / `template_id` |
|
|
59
|
+
| `slack_message` | `slack` | `webhook_url`, `text` |
|
|
60
|
+
|
|
55
61
|
```json
|
|
56
62
|
[
|
|
57
63
|
{
|
|
58
|
-
"type": "
|
|
64
|
+
"type": "email",
|
|
59
65
|
"from": "hello@yourdomain.com",
|
|
60
66
|
"to": "{{ payload.email }}",
|
|
61
67
|
"subject": "Welcome, {{ payload.name }}",
|
|
62
68
|
"template_id": "<template_id>"
|
|
63
69
|
},
|
|
64
70
|
{
|
|
65
|
-
"type": "
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
71
|
+
"type": "slack",
|
|
72
|
+
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
73
|
+
"text": "New submission from {{ payload.name }}"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Unknown step types are rejected at workflow create time, so typos surface
|
|
79
|
+
immediately instead of after 3 failed retries during execution.
|
|
80
|
+
|
|
81
|
+
The webhook payload is available as `{{ payload }}` and individual fields
|
|
82
|
+
as `{{ payload.fieldname }}`. Whitespace inside the braces is fine —
|
|
83
|
+
both `{{ payload.email }}` and `{{payload.email}}` work.
|
|
84
|
+
|
|
85
|
+
## End-to-end recipe — react to a contact form submission
|
|
86
|
+
|
|
87
|
+
Goal: form on yoursite.com posts to a webhook → workflow sends a thank-you
|
|
88
|
+
email and pings Slack. Full HTML + commands:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# 1. Create the webhook to receive submissions
|
|
92
|
+
WID=$(myapi webhook create "contact-form" --json | jq -r .id)
|
|
93
|
+
WURL=$(myapi webhook list --json | jq -r ".[] | select(.id==\"$WID\") | .url")
|
|
94
|
+
|
|
95
|
+
# 2. Workflow that fires on each submission. Two steps: thank-you email
|
|
96
|
+
# + Slack ping. Both reference fields from the form payload via {{ }}.
|
|
97
|
+
myapi workflow create "Contact form handler" \
|
|
98
|
+
--endpoint-id $WID \
|
|
99
|
+
--steps "$(cat <<'JSON'
|
|
100
|
+
[
|
|
101
|
+
{
|
|
102
|
+
"type": "send_email",
|
|
103
|
+
"from": "hello@yoursite.com",
|
|
104
|
+
"to": "{{ payload.email }}",
|
|
105
|
+
"subject": "Thanks for reaching out, {{ payload.name }}!",
|
|
106
|
+
"body": "We got your message and will reply within 24 hours."
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"type": "slack",
|
|
110
|
+
"webhook_url": "https://hooks.slack.com/services/T.../B.../xxx",
|
|
111
|
+
"text": "New submission from {{ payload.name }} ({{ payload.email }}): {{ payload.message }}"
|
|
69
112
|
}
|
|
70
113
|
]
|
|
114
|
+
JSON
|
|
115
|
+
)"
|
|
116
|
+
|
|
117
|
+
# 3. The HTML form on yoursite.com — POSTs JSON to the webhook
|
|
118
|
+
echo "<form id='contact'>
|
|
119
|
+
<input name='name' required>
|
|
120
|
+
<input name='email' type='email' required>
|
|
121
|
+
<textarea name='message' required></textarea>
|
|
122
|
+
<button>Send</button>
|
|
123
|
+
</form>
|
|
124
|
+
<script>
|
|
125
|
+
document.getElementById('contact').addEventListener('submit', async e => {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
const data = Object.fromEntries(new FormData(e.target));
|
|
128
|
+
await fetch('$WURL', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {'Content-Type': 'application/json'},
|
|
131
|
+
body: JSON.stringify(data),
|
|
132
|
+
});
|
|
133
|
+
alert('Thanks! Check your inbox.');
|
|
134
|
+
});
|
|
135
|
+
</script>"
|
|
71
136
|
```
|
|
72
137
|
|
|
73
|
-
The
|
|
138
|
+
The form sends `{"name": "...", "email": "...", "message": "..."}`. The
|
|
139
|
+
webhook stores it. The workflow runs both steps in order: the user gets
|
|
140
|
+
an email at the address they typed, your team gets a Slack ping with the
|
|
141
|
+
message. Each invocation is recorded — `myapi workflow runs <id>` shows
|
|
142
|
+
the history.
|
|
74
143
|
|
|
75
144
|
## Lifecycle
|
|
76
145
|
|
|
77
146
|
```bash
|
|
78
147
|
# Create disabled, test, then enable
|
|
79
|
-
myapi workflow create
|
|
148
|
+
myapi workflow create "X" --endpoint-id $WID --steps '...' --no-enable
|
|
80
149
|
|
|
81
150
|
# Trigger via webhook POST, inspect
|
|
82
151
|
myapi workflow runs <id>
|
package/dist/utils.d.ts
CHANGED
|
@@ -4,3 +4,29 @@ export declare function sleep(ms: number): Promise<unknown>;
|
|
|
4
4
|
* Strips Go's " +0000 UTC" suffix before parsing.
|
|
5
5
|
*/
|
|
6
6
|
export declare function formatDate(str: string): string;
|
|
7
|
+
export interface PollOptions<T> {
|
|
8
|
+
/** Fetch current state. Called once per interval. */
|
|
9
|
+
check: () => Promise<T>;
|
|
10
|
+
/** Inspect a state and decide if we should resolve with it. */
|
|
11
|
+
isDone: (state: T) => boolean;
|
|
12
|
+
/** Inspect a state and decide if the job has failed (calls error()). */
|
|
13
|
+
isFailed?: (state: T) => boolean;
|
|
14
|
+
/** Label shown next to the spinner (e.g. "Importing"). */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Total wall-clock budget. Defaults to 90s. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
/** Delay between checks. Defaults to 3s. */
|
|
19
|
+
intervalMs?: number;
|
|
20
|
+
/** Message shown via error() on timeout. Defaults to a generic phrase. */
|
|
21
|
+
timeoutMessage?: string;
|
|
22
|
+
/** Message shown via error() when isFailed returns true. */
|
|
23
|
+
failedMessage?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Generic spinner+poll helper. Used wherever the CLI kicks off a long-running
|
|
27
|
+
* server-side job and needs to wait for it (template generate, sync-brand).
|
|
28
|
+
*
|
|
29
|
+
* Returns the final `T` from `check()` once `isDone(state)` is true.
|
|
30
|
+
* Calls error() (which exits with code 1) on timeout or `isFailed`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function pollJob<T>(opts: PollOptions<T>): Promise<T>;
|
package/dist/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spinnerFrame, clearLine, error } from './output.js';
|
|
1
2
|
export function sleep(ms) {
|
|
2
3
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
3
4
|
}
|
|
@@ -12,3 +13,34 @@ export function formatDate(str) {
|
|
|
12
13
|
return str;
|
|
13
14
|
return date.toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Generic spinner+poll helper. Used wherever the CLI kicks off a long-running
|
|
18
|
+
* server-side job and needs to wait for it (template generate, sync-brand).
|
|
19
|
+
*
|
|
20
|
+
* Returns the final `T` from `check()` once `isDone(state)` is true.
|
|
21
|
+
* Calls error() (which exits with code 1) on timeout or `isFailed`.
|
|
22
|
+
*/
|
|
23
|
+
export async function pollJob(opts) {
|
|
24
|
+
const timeoutMs = opts.timeoutMs ?? 90_000;
|
|
25
|
+
const intervalMs = opts.intervalMs ?? 3_000;
|
|
26
|
+
process.stdout.write(`${opts.label} `);
|
|
27
|
+
let i = 0;
|
|
28
|
+
let elapsed = 0;
|
|
29
|
+
while (elapsed < timeoutMs) {
|
|
30
|
+
const state = await opts.check();
|
|
31
|
+
if (opts.isDone(state)) {
|
|
32
|
+
clearLine();
|
|
33
|
+
return state;
|
|
34
|
+
}
|
|
35
|
+
if (opts.isFailed && opts.isFailed(state)) {
|
|
36
|
+
clearLine();
|
|
37
|
+
error(opts.failedMessage ?? `${opts.label} failed`);
|
|
38
|
+
}
|
|
39
|
+
process.stdout.write(`\r${opts.label} ${spinnerFrame(i++)}`);
|
|
40
|
+
await sleep(intervalMs);
|
|
41
|
+
elapsed += intervalMs;
|
|
42
|
+
}
|
|
43
|
+
clearLine();
|
|
44
|
+
error(opts.timeoutMessage ??
|
|
45
|
+
`${opts.label} timed out after ${Math.round(timeoutMs / 1000)}s. The job may still complete in the background — re-run to retry.`);
|
|
46
|
+
}
|