@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.
Files changed (43) hide show
  1. package/dist/commands/auth.js +75 -33
  2. package/dist/commands/billing.js +3 -5
  3. package/dist/commands/domain.js +8 -1
  4. package/dist/commands/email/campaign.d.ts +2 -0
  5. package/dist/commands/email/campaign.js +152 -0
  6. package/dist/commands/email/index.d.ts +4 -0
  7. package/dist/commands/email/index.js +98 -0
  8. package/dist/commands/email/mailbox.d.ts +2 -0
  9. package/dist/commands/email/mailbox.js +88 -0
  10. package/dist/commands/email/message.d.ts +2 -0
  11. package/dist/commands/email/message.js +115 -0
  12. package/dist/commands/email/template.d.ts +2 -0
  13. package/dist/commands/email/template.js +106 -0
  14. package/dist/commands/email/warmup.d.ts +2 -0
  15. package/dist/commands/email/warmup.js +43 -0
  16. package/dist/commands/email.js +71 -51
  17. package/dist/commands/funnel.d.ts +1 -1
  18. package/dist/commands/funnel.js +31 -9
  19. package/dist/commands/keys.d.ts +1 -1
  20. package/dist/commands/keys.js +6 -9
  21. package/dist/commands/org.js +17 -34
  22. package/dist/commands/setup.js +143 -115
  23. package/dist/commands/update.js +5 -0
  24. package/dist/commands/webhook.d.ts +1 -1
  25. package/dist/commands/webhook.js +23 -7
  26. package/dist/commands/workflow.d.ts +1 -1
  27. package/dist/commands/workflow.js +45 -6
  28. package/dist/config.js +10 -5
  29. package/dist/helpers.js +8 -6
  30. package/dist/index.js +9 -3
  31. package/dist/output.d.ts +3 -1
  32. package/dist/output.js +12 -0
  33. package/dist/prompt.d.ts +24 -0
  34. package/dist/prompt.js +41 -0
  35. package/dist/skills/my-email-api/README.md +1 -1
  36. package/dist/skills/my-email-api/SKILL.md +4 -6
  37. package/dist/skills/my-webhook-api/README.md +1 -1
  38. package/dist/skills/my-webhook-api/SKILL.md +77 -4
  39. package/dist/skills/my-workflow-api/README.md +3 -4
  40. package/dist/skills/my-workflow-api/SKILL.md +79 -10
  41. package/dist/utils.d.ts +26 -0
  42. package/dist/utils.js +32 -0
  43. package/package.json +1 -1
package/dist/helpers.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { error } from './output.js';
2
- // Resolve org id from --org or the configured default. On miss, exit with a
3
- // usage-aware error that points the user at both the flag and the config command.
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 || typeof orgId !== 'string') {
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 domain = arg || flags.domain || config.default_domain;
13
- if (!domain || typeof domain !== 'string') {
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
- error(err.message || (typeof err === 'object' ? JSON.stringify(err) : String(err)));
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.message || (typeof err === 'object' ? JSON.stringify(err) : String(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: Record<string, unknown>[], opts?: PrintTableOptions): void;
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);
@@ -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 --domain yourdomain.com --username hello
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 --domain yourdomain.com --username hello --display-name "Hello"
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 --name "stripe-events"
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 --name "stripe-events" --description "Stripe payment events"
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 --name "leads" --json | jq -r .id)
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, hit Slack, call any HTTP URL
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 --name "leads" --json | jq -r .id)
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 --name "leads" --json | jq -r .id)
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": "send_email",
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": "http",
66
- "method": "POST",
67
- "url": "https://crm.example.com/contacts",
68
- "body": "{{ payload | json }}"
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 webhook payload is available as `{{ payload }}` and individual fields as `{{ payload.fieldname }}`.
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 --name "X" --endpoint-id $WID --steps '...' --no-enable
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myapihq/cli",
3
- "version": "1.1.0-wip.0",
3
+ "version": "1.1.0-wip.2",
4
4
  "description": "MyAPI command-line interface",
5
5
  "type": "module",
6
6
  "files": [