@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/commands/org.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import * as readline from 'readline';
|
|
2
1
|
import { hq, funnel as sdkFunnel } from '@myapihq/sdk';
|
|
3
2
|
import { requireConfig, saveConfig } from '../config.js';
|
|
4
3
|
import { success, error, printTable, printJson, info } from '../output.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { confirm } from '../prompt.js';
|
|
5
|
+
import { formatDate, pollJob } from '../utils.js';
|
|
6
|
+
import { requireOrg, requireArg } from '../helpers.js';
|
|
7
7
|
export const SCHEMA = {
|
|
8
8
|
name: 'string',
|
|
9
9
|
tagline: 'string',
|
|
@@ -34,13 +34,7 @@ export async function create(restArgs, flags) {
|
|
|
34
34
|
info(`Preview domain: https://${org.preview_subdomain}.makeautonomous.com`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
const setDefault = flags.yes || await
|
|
38
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
39
|
-
rl.question('› Set as default org and funnel? (Y/n) ', ans => {
|
|
40
|
-
rl.close();
|
|
41
|
-
resolve(ans.trim().toLowerCase() !== 'n');
|
|
42
|
-
});
|
|
43
|
-
});
|
|
37
|
+
const setDefault = !!flags.yes || await confirm('› Set as default org and funnel? (Y/n) ', true);
|
|
44
38
|
let funnelId;
|
|
45
39
|
if (setDefault) {
|
|
46
40
|
config.default_org = org.id;
|
|
@@ -108,8 +102,7 @@ export async function get(id, flags) {
|
|
|
108
102
|
info(`Preview: https://${org.preview_subdomain}.makeautonomous.com`);
|
|
109
103
|
}
|
|
110
104
|
export async function del(id, _flags) {
|
|
111
|
-
|
|
112
|
-
error('Missing org id. Usage: myapi org delete <id>');
|
|
105
|
+
requireArg(id, 'id', 'myapi org delete <id>');
|
|
113
106
|
const config = requireConfig();
|
|
114
107
|
await hq.deleteOrg(config.api_key, id);
|
|
115
108
|
success(`Org ${id} deleted`);
|
|
@@ -122,28 +115,18 @@ export async function importOrg(args, flags) {
|
|
|
122
115
|
error('Missing required arguments.\nUsage: myapi org sync-brand <domain> [--org <id>]\n(Or set default: myapi config set-domain <domain>)');
|
|
123
116
|
const result = await hq.importOrg(config.api_key, orgId, domain);
|
|
124
117
|
const importId = result.job_id;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (status.status === 'failed') {
|
|
138
|
-
process.stdout.write('\r\x1b[K');
|
|
139
|
-
error('Import failed');
|
|
140
|
-
}
|
|
141
|
-
process.stdout.write(`\rImporting ${chars[i++ % chars.length]}`);
|
|
142
|
-
await sleep(3000);
|
|
143
|
-
elapsed += 3000;
|
|
144
|
-
}
|
|
145
|
-
process.stdout.write('\r\x1b[K');
|
|
146
|
-
error(`Import timed out after ${SYNC_BRAND_TIMEOUT_MS / 1000}s. The job may still complete in the background — re-run "myapi org sync-brand" to retry.`);
|
|
118
|
+
await pollJob({
|
|
119
|
+
label: 'Importing',
|
|
120
|
+
timeoutMs: SYNC_BRAND_TIMEOUT_MS,
|
|
121
|
+
intervalMs: 3000,
|
|
122
|
+
check: () => hq.getOrgImportStatus(config.api_key, importId),
|
|
123
|
+
isDone: s => s.status === 'awaiting_confirm',
|
|
124
|
+
isFailed: s => s.status === 'failed',
|
|
125
|
+
failedMessage: 'Import failed',
|
|
126
|
+
timeoutMessage: `Import timed out after ${SYNC_BRAND_TIMEOUT_MS / 1000}s. The job may still complete in the background — re-run "myapi org sync-brand" to retry.`,
|
|
127
|
+
});
|
|
128
|
+
const org = await hq.confirmOrgImport(config.api_key, importId);
|
|
129
|
+
success(`Import complete! Org ID: ${org.id}`);
|
|
147
130
|
}
|
|
148
131
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
149
132
|
const SUBCOMMAND_USAGE = {
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import * as readline from 'readline';
|
|
5
4
|
import { loadConfig, saveConfig, addAccount, loadFullConfig } from '../config.js';
|
|
6
5
|
import { info, success } from '../output.js';
|
|
6
|
+
import { ask, confirm } from '../prompt.js';
|
|
7
7
|
import { hq, funnel as sdkFunnel } from '@myapihq/sdk';
|
|
8
|
-
function ask(rl, q) {
|
|
9
|
-
return new Promise(resolve => rl.question(q, resolve));
|
|
10
|
-
}
|
|
11
|
-
function yn(answer, defaultYes = true) {
|
|
12
|
-
const t = answer.trim().toLowerCase();
|
|
13
|
-
if (t === '')
|
|
14
|
-
return defaultYes;
|
|
15
|
-
return t === 'y' || t === 'yes';
|
|
16
|
-
}
|
|
17
8
|
// ---------------------------------------------------------------------------
|
|
18
9
|
// Skills installation
|
|
19
10
|
// ---------------------------------------------------------------------------
|
|
@@ -53,10 +44,15 @@ export async function installSkills() {
|
|
|
53
44
|
for (const skill of skills) {
|
|
54
45
|
const link = path.join(dir, skill);
|
|
55
46
|
const target = path.join(SKILLS_CANONICAL, skill);
|
|
47
|
+
// Only remove if it's already a symlink we own. Avoids clobbering a
|
|
48
|
+
// user-authored skill that happens to share the same `my-X-api/`
|
|
49
|
+
// name. ENOENT here just means "nothing to clean up".
|
|
56
50
|
try {
|
|
57
|
-
fs.
|
|
51
|
+
const stat = fs.lstatSync(link);
|
|
52
|
+
if (stat.isSymbolicLink())
|
|
53
|
+
fs.rmSync(link, { force: true });
|
|
58
54
|
}
|
|
59
|
-
catch { /*
|
|
55
|
+
catch { /* ENOENT: nothing there */ }
|
|
60
56
|
fs.symlinkSync(target, link);
|
|
61
57
|
}
|
|
62
58
|
info(` ✓ ${agent}`);
|
|
@@ -74,13 +70,48 @@ export async function installSkills() {
|
|
|
74
70
|
}
|
|
75
71
|
}
|
|
76
72
|
// ---------------------------------------------------------------------------
|
|
73
|
+
// Shared helpers
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Resolve the user's "install skills?" preference: explicit flag wins, else
|
|
76
|
+
// prompt (defaulting to yes). `--yes` short-circuits to true.
|
|
77
|
+
async function resolveSkillsPreference(flags) {
|
|
78
|
+
if (flags['install-skills'])
|
|
79
|
+
return true;
|
|
80
|
+
if (flags['no-skills'])
|
|
81
|
+
return false;
|
|
82
|
+
if (flags.yes)
|
|
83
|
+
return true;
|
|
84
|
+
return confirm('› Install the MyAPI skills pack? (Y/n) ', true);
|
|
85
|
+
}
|
|
86
|
+
// Resolve default org and funnel for a key by inspecting the account's orgs.
|
|
87
|
+
// Used by both `setup` and `import-key`. Picks the most recently-created
|
|
88
|
+
// org/funnel as the default; preserves the caller's hint if it still exists.
|
|
89
|
+
async function resolveDefaults(apiKey, hint = {}) {
|
|
90
|
+
let org = hint.org ?? '';
|
|
91
|
+
let funnel = hint.funnel ?? '';
|
|
92
|
+
try {
|
|
93
|
+
const orgs = await hq.listOrgs(apiKey);
|
|
94
|
+
if (orgs.length > 0) {
|
|
95
|
+
if (!org || !orgs.find(o => o.id === org)) {
|
|
96
|
+
org = orgs[orgs.length - 1].id;
|
|
97
|
+
}
|
|
98
|
+
const funnels = await sdkFunnel.listFunnels(apiKey, org);
|
|
99
|
+
if (funnels.length > 0 && (!funnel || !funnels.find((f) => f.id === funnel))) {
|
|
100
|
+
funnel = funnels[funnels.length - 1].id;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch { /* non-fatal */ }
|
|
105
|
+
return { org, funnel };
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
77
108
|
// Registered flow
|
|
78
109
|
// ---------------------------------------------------------------------------
|
|
79
|
-
async function registeredFlow(
|
|
80
|
-
const email =
|
|
110
|
+
async function registeredFlow() {
|
|
111
|
+
const email = await ask('› Email? ');
|
|
81
112
|
await hq.sendCode(email);
|
|
82
113
|
info(`› Sent a code to ${email} · paste it below`);
|
|
83
|
-
const code =
|
|
114
|
+
const code = await ask('› Code? ');
|
|
84
115
|
const data = await hq.verifyCode(email, code);
|
|
85
116
|
return { ...data, email };
|
|
86
117
|
}
|
|
@@ -90,16 +121,39 @@ async function anonymousFlow() {
|
|
|
90
121
|
// ---------------------------------------------------------------------------
|
|
91
122
|
// Main setup command
|
|
92
123
|
// ---------------------------------------------------------------------------
|
|
124
|
+
const IMPORT_KEY_HELP = `Usage: myapi auth import-key <api_key> [--install-skills] [--no-skills]
|
|
125
|
+
|
|
126
|
+
Imports an existing API key non-interactively. Use this in CI, Docker, or any
|
|
127
|
+
environment where the interactive "myapi auth setup" flow is not practical.
|
|
128
|
+
|
|
129
|
+
The key is validated against the API before being saved. Your default org and
|
|
130
|
+
funnel are auto-detected from the account and written to the local config.
|
|
131
|
+
|
|
132
|
+
Flags:
|
|
133
|
+
--install-skills Also install the MyAPI skills pack for AI agents after importing
|
|
134
|
+
--no-skills Skip skills installation even if previously installed
|
|
135
|
+
|
|
136
|
+
Examples:
|
|
137
|
+
myapi auth import-key hq_live_xxxxxxxxxxxxxxxxxxxx
|
|
138
|
+
myapi auth import-key hq_live_xxxxxxxxxxxxxxxxxxxx --install-skills`;
|
|
139
|
+
const SETUP_HELP = `Usage: myapi auth setup [--anonymous] [--yes] [--install-skills|--no-skills]
|
|
140
|
+
|
|
141
|
+
Configures your account and stores your default org and funnel so you don't
|
|
142
|
+
need to pass --org or --funnel on every command.
|
|
143
|
+
|
|
144
|
+
Flags:
|
|
145
|
+
--anonymous Skip registration, create anonymous account
|
|
146
|
+
--yes Skip confirmation prompts
|
|
147
|
+
--install-skills Auto-install skills pack
|
|
148
|
+
--no-skills Skip skills installation`;
|
|
93
149
|
// myapi auth import-key <key> — non-interactively import a raw API key.
|
|
94
150
|
export async function importKey(apiKey, flags) {
|
|
95
151
|
if (flags.help || !apiKey) {
|
|
96
|
-
info(
|
|
152
|
+
info(IMPORT_KEY_HELP);
|
|
97
153
|
return;
|
|
98
154
|
}
|
|
99
155
|
let accountId = '';
|
|
100
156
|
let email;
|
|
101
|
-
let defaultOrg = '';
|
|
102
|
-
let defaultFunnel = '';
|
|
103
157
|
try {
|
|
104
158
|
const me = await hq.getAccount(apiKey);
|
|
105
159
|
accountId = me.account_id ?? '';
|
|
@@ -108,25 +162,26 @@ export async function importKey(apiKey, flags) {
|
|
|
108
162
|
catch (e) {
|
|
109
163
|
throw new Error(`Could not verify API key: ${e.message}`);
|
|
110
164
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch { /* non-fatal */ }
|
|
121
|
-
const wantsSkills = flags['install-skills'] ? true : flags['no-skills'] ? false : true;
|
|
122
|
-
addAccount({ api_key: apiKey, account_id: accountId, email, default_org: defaultOrg, default_funnel: defaultFunnel, skills_installed: wantsSkills });
|
|
165
|
+
const { org: defaultOrg, funnel: defaultFunnel } = await resolveDefaults(apiKey);
|
|
166
|
+
const wantsSkills = flags['no-skills'] ? false : true;
|
|
167
|
+
addAccount({
|
|
168
|
+
api_key: apiKey, account_id: accountId, email,
|
|
169
|
+
default_org: defaultOrg, default_funnel: defaultFunnel,
|
|
170
|
+
skills_installed: wantsSkills,
|
|
171
|
+
});
|
|
123
172
|
success(`› ✓ Key imported${email ? ` · ${email}` : ''}`);
|
|
124
173
|
if (wantsSkills)
|
|
125
174
|
await installSkills();
|
|
126
175
|
}
|
|
176
|
+
// Persist a new account + (optionally) install skills.
|
|
177
|
+
async function installAndPersistSkills(account, wantsSkills) {
|
|
178
|
+
addAccount({ ...account, skills_installed: wantsSkills });
|
|
179
|
+
if (wantsSkills)
|
|
180
|
+
await installSkills();
|
|
181
|
+
}
|
|
127
182
|
export async function setup(flags = {}) {
|
|
128
183
|
if (flags.help) {
|
|
129
|
-
info(
|
|
184
|
+
info(SETUP_HELP);
|
|
130
185
|
return;
|
|
131
186
|
}
|
|
132
187
|
info('› Configuring MyAPI…');
|
|
@@ -139,27 +194,20 @@ export async function setup(flags = {}) {
|
|
|
139
194
|
const activeType = existing.is_anonymous ? 'anonymous' : 'registered';
|
|
140
195
|
const othersNote = total > 1 ? ` · ${total - 1} more saved` : '';
|
|
141
196
|
info(`› Connected: ${activeLabel} (${activeType})${othersNote}`);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
if (!yn(add, false)) {
|
|
197
|
+
const add = await confirm('› Connect a new account? (y/N) ', false);
|
|
198
|
+
if (!add) {
|
|
145
199
|
if (!existing.skills_installed) {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
if (yn(ans)) {
|
|
200
|
+
const wants = await resolveSkillsPreference(flags);
|
|
201
|
+
if (wants) {
|
|
149
202
|
await installSkills();
|
|
150
203
|
success('› Skills installed.');
|
|
151
204
|
saveConfig({ ...existing, skills_installed: true });
|
|
152
205
|
}
|
|
153
206
|
}
|
|
154
|
-
else {
|
|
155
|
-
rl.close();
|
|
156
|
-
}
|
|
157
207
|
return;
|
|
158
208
|
}
|
|
159
|
-
rl.close();
|
|
160
209
|
// Fall through to setup flow — new account will be added to the list.
|
|
161
210
|
}
|
|
162
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
163
211
|
let apiKey = '';
|
|
164
212
|
let accountId = '';
|
|
165
213
|
let defaultOrg = '';
|
|
@@ -167,82 +215,62 @@ export async function setup(flags = {}) {
|
|
|
167
215
|
let subdomainUrl = '';
|
|
168
216
|
let isAnonymous = false;
|
|
169
217
|
let email = '';
|
|
170
|
-
|
|
171
|
-
const
|
|
218
|
+
const useAnon = flags.anonymous || flags.anon;
|
|
219
|
+
const wantsRegister = useAnon ? false : await confirm('› Register with email? (Y/n, or N to continue anonymously) ', true);
|
|
220
|
+
if (wantsRegister) {
|
|
221
|
+
const data = await registeredFlow();
|
|
222
|
+
apiKey = data.api_key;
|
|
223
|
+
accountId = data.account_id;
|
|
224
|
+
defaultOrg = data.default_org;
|
|
225
|
+
defaultFunnel = data.default_funnel;
|
|
226
|
+
email = data.email;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
info('› Continuing as anonymous — sites ship to *.makeautonomous.com');
|
|
230
|
+
const data = await anonymousFlow();
|
|
231
|
+
apiKey = data.api_key;
|
|
232
|
+
accountId = data.account_id;
|
|
233
|
+
defaultOrg = data.default_org;
|
|
234
|
+
defaultFunnel = data.default_funnel;
|
|
235
|
+
subdomainUrl = data.subdomain_url;
|
|
236
|
+
isAnonymous = true;
|
|
237
|
+
}
|
|
238
|
+
const wantsSkills = await resolveSkillsPreference(flags);
|
|
239
|
+
await installAndPersistSkills({
|
|
240
|
+
api_key: apiKey,
|
|
241
|
+
account_id: accountId,
|
|
242
|
+
email: email || undefined,
|
|
243
|
+
default_org: defaultOrg,
|
|
244
|
+
default_funnel: defaultFunnel,
|
|
245
|
+
is_anonymous: isAnonymous,
|
|
246
|
+
}, wantsSkills);
|
|
247
|
+
success(`› ✓ saved to ~/.myapi/config.json`);
|
|
248
|
+
// Validate key and ensure org/funnel defaults are still correct.
|
|
249
|
+
// (resolveDefaults is non-fatal if the API call fails.)
|
|
172
250
|
try {
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const data = await anonymousFlow();
|
|
186
|
-
apiKey = data.api_key;
|
|
187
|
-
accountId = data.account_id;
|
|
188
|
-
defaultOrg = data.default_org;
|
|
189
|
-
defaultFunnel = data.default_funnel;
|
|
190
|
-
subdomainUrl = data.subdomain_url;
|
|
191
|
-
isAnonymous = true;
|
|
192
|
-
}
|
|
193
|
-
const wantsSkills = skillsFromFlag !== null ? skillsFromFlag : flags.yes ? true : yn(await ask(rl, '› Install the MyAPI skills pack? (Y/n) '));
|
|
194
|
-
addAccount({
|
|
195
|
-
api_key: apiKey,
|
|
196
|
-
account_id: accountId,
|
|
197
|
-
email: email || undefined,
|
|
198
|
-
default_org: defaultOrg,
|
|
199
|
-
default_funnel: defaultFunnel,
|
|
200
|
-
is_anonymous: isAnonymous,
|
|
201
|
-
skills_installed: wantsSkills,
|
|
202
|
-
});
|
|
203
|
-
success(`› ✓ saved to ~/.myapi/config.json`);
|
|
204
|
-
if (wantsSkills) {
|
|
205
|
-
await installSkills();
|
|
206
|
-
}
|
|
207
|
-
// Validate key and ensure org/funnel defaults are correct.
|
|
208
|
-
try {
|
|
209
|
-
await hq.getAccount(apiKey);
|
|
210
|
-
let resolvedOrg = defaultOrg;
|
|
211
|
-
let resolvedFunnel = defaultFunnel;
|
|
212
|
-
const orgs = await hq.listOrgs(apiKey);
|
|
213
|
-
if (orgs.length > 0) {
|
|
214
|
-
const latest = orgs[orgs.length - 1];
|
|
215
|
-
if (!resolvedOrg || !orgs.find(o => o.id === resolvedOrg)) {
|
|
216
|
-
resolvedOrg = latest.id;
|
|
217
|
-
info(`› Auto-selected org: ${latest.name} (${resolvedOrg})`);
|
|
218
|
-
}
|
|
219
|
-
const funnels = await sdkFunnel.listFunnels(apiKey, resolvedOrg);
|
|
220
|
-
if (funnels.length > 0 && (!resolvedFunnel || !funnels.find((f) => f.id === resolvedFunnel))) {
|
|
221
|
-
resolvedFunnel = funnels[funnels.length - 1].id;
|
|
222
|
-
info(`› Auto-selected funnel: ${resolvedFunnel}`);
|
|
223
|
-
}
|
|
224
|
-
if (resolvedOrg !== defaultOrg || resolvedFunnel !== defaultFunnel) {
|
|
225
|
-
const current = loadConfig();
|
|
226
|
-
if (current) {
|
|
227
|
-
saveConfig({ ...current, default_org: resolvedOrg, default_funnel: resolvedFunnel });
|
|
228
|
-
defaultOrg = resolvedOrg;
|
|
229
|
-
defaultFunnel = resolvedFunnel;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
251
|
+
await hq.getAccount(apiKey);
|
|
252
|
+
const resolved = await resolveDefaults(apiKey, { org: defaultOrg, funnel: defaultFunnel });
|
|
253
|
+
if (resolved.org !== defaultOrg || resolved.funnel !== defaultFunnel) {
|
|
254
|
+
const current = loadConfig();
|
|
255
|
+
if (current) {
|
|
256
|
+
saveConfig({ ...current, default_org: resolved.org, default_funnel: resolved.funnel });
|
|
257
|
+
if (resolved.org !== defaultOrg)
|
|
258
|
+
info(`› Auto-selected org: ${resolved.org}`);
|
|
259
|
+
if (resolved.funnel !== defaultFunnel)
|
|
260
|
+
info(`› Auto-selected funnel: ${resolved.funnel}`);
|
|
261
|
+
defaultOrg = resolved.org;
|
|
262
|
+
defaultFunnel = resolved.funnel;
|
|
232
263
|
}
|
|
233
264
|
}
|
|
234
|
-
catch { /* non-fatal */ }
|
|
235
|
-
if (isAnonymous) {
|
|
236
|
-
success('› Setup complete. No card, no email, ready to ship.');
|
|
237
|
-
if (subdomainUrl)
|
|
238
|
-
info(`› Your funnel: ${subdomainUrl}`);
|
|
239
|
-
info('› Upgrade anytime — myapi auth link');
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
success(`› Setup complete. Org ${defaultOrg} ready · $5.00 free credits added.`);
|
|
243
|
-
}
|
|
244
265
|
}
|
|
245
|
-
|
|
246
|
-
|
|
266
|
+
catch { /* non-fatal */ }
|
|
267
|
+
if (isAnonymous) {
|
|
268
|
+
success('› Setup complete. No card, no email, ready to ship.');
|
|
269
|
+
if (subdomainUrl)
|
|
270
|
+
info(`› Your funnel: ${subdomainUrl}`);
|
|
271
|
+
info('› Upgrade anytime — myapi auth link');
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
success(`› Setup complete. Org ${defaultOrg} ready · $5.00 free credits added.`);
|
|
247
275
|
}
|
|
248
276
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -126,6 +126,11 @@ export async function latestVersion() {
|
|
|
126
126
|
return null;
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
// Stable-only semver comparison (1.2.3). Prerelease versions (1.2.3-wip.0)
|
|
130
|
+
// produce NaN in the patch slot, and `n > NaN` is always false — which is
|
|
131
|
+
// what we want: a prerelease user should not be auto-rolled back to stable.
|
|
132
|
+
// Auto-update is opt-in via the `latest` dist-tag (always stable); see
|
|
133
|
+
// checkForUpdate() above.
|
|
129
134
|
export function isNewer(latest, current) {
|
|
130
135
|
const toNum = (v) => v.split('.').map(Number);
|
|
131
136
|
const [lMaj, lMin, lPat] = toNum(latest);
|
|
@@ -2,7 +2,7 @@ import type { FlagSchema } from '../flags.js';
|
|
|
2
2
|
import { type Flags } from '../helpers.js';
|
|
3
3
|
export declare const SCHEMA: FlagSchema;
|
|
4
4
|
export declare function list(flags: Flags): Promise<void>;
|
|
5
|
-
export declare function create(flags: Flags): Promise<void>;
|
|
5
|
+
export declare function create(nameArg: string | undefined, flags: Flags): Promise<void>;
|
|
6
6
|
export declare function del(id: string, flags: Flags): Promise<void>;
|
|
7
7
|
export declare function delivery(id: string, flags: Flags): Promise<void>;
|
|
8
8
|
export declare function run(subcommand: string | undefined, args: string[], flags: Flags): Promise<void>;
|
package/dist/commands/webhook.js
CHANGED
|
@@ -20,12 +20,12 @@ export async function list(flags) {
|
|
|
20
20
|
empty: 'No webhook endpoints yet. Create one with: myapi webhook create --name <name>',
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
export async function create(flags) {
|
|
23
|
+
export async function create(nameArg, flags) {
|
|
24
24
|
const config = requireConfig();
|
|
25
|
-
const orgId = requireOrg(flags, config, 'myapi webhook create
|
|
26
|
-
const name = flags.name;
|
|
25
|
+
const orgId = requireOrg(flags, config, 'myapi webhook create <name> [--description <desc>] [--org <id>]');
|
|
26
|
+
const name = nameArg || flags.name;
|
|
27
27
|
if (!name)
|
|
28
|
-
error('Missing required arguments.\nUsage: myapi webhook create --name <name> [--description <desc>] [--org <id>]');
|
|
28
|
+
error('Missing required arguments.\nUsage: myapi webhook create <name> [--description <desc>] [--org <id>]\n or: myapi webhook create --name <name> [--description <desc>] [--org <id>]');
|
|
29
29
|
const description = flags.description;
|
|
30
30
|
const res = await sdkWebhook.createEndpoint(config.api_key, orgId, name, description);
|
|
31
31
|
success(`Webhook created! ID: ${res.id}\nInbound URL: ${res.url}`);
|
|
@@ -49,7 +49,10 @@ export async function delivery(id, flags) {
|
|
|
49
49
|
// ── Dispatcher ───────────────────────────────────────────────────────────────
|
|
50
50
|
const SUBCOMMAND_USAGE = {
|
|
51
51
|
'list': 'myapi webhook list [--org <id>] [--json]',
|
|
52
|
-
'create':
|
|
52
|
+
'create': `myapi webhook create <name> [--description <desc>] [--org <id>]
|
|
53
|
+
myapi webhook create --name <name> [--description <desc>] [--org <id>]
|
|
54
|
+
|
|
55
|
+
Either form works; the positional name is the recommended shape.`,
|
|
53
56
|
'delete': 'myapi webhook delete <id> [--org <id>]',
|
|
54
57
|
'delivery': 'myapi webhook delivery <delivery_id> [--org <id>]',
|
|
55
58
|
};
|
|
@@ -63,7 +66,20 @@ Subcommands:
|
|
|
63
66
|
delete Delete a webhook endpoint
|
|
64
67
|
delivery Inspect a specific webhook delivery (payload, received_at)
|
|
65
68
|
|
|
66
|
-
All commands accept --org <id> (or set default: myapi config set-org <id>)
|
|
69
|
+
All commands accept --org <id> (or set default: myapi config set-org <id>).
|
|
70
|
+
|
|
71
|
+
Payload contract:
|
|
72
|
+
The inbound URL accepts ANY JSON body — there's no enforced schema.
|
|
73
|
+
Whatever you POST is stored verbatim alongside headers + timestamp.
|
|
74
|
+
Workflows reference fields with {{ payload.field }} templating.
|
|
75
|
+
|
|
76
|
+
Example — wire a contact form to email + Slack:
|
|
77
|
+
WID=$(myapi webhook create "contact-form" --json | jq -r .id)
|
|
78
|
+
myapi workflow create "Contact handler" --endpoint-id $WID --steps '[
|
|
79
|
+
{"type":"send_email","to":"{{ payload.email }}","subject":"Thanks!","body":"..."},
|
|
80
|
+
{"type":"slack","webhook_url":"https://hooks.slack.com/...","text":"New: {{ payload.message }}"}
|
|
81
|
+
]'
|
|
82
|
+
See the my-webhook-api skill for the full HTML + JS recipe.`);
|
|
67
83
|
return;
|
|
68
84
|
}
|
|
69
85
|
if (flags.help) {
|
|
@@ -76,7 +92,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
76
92
|
}
|
|
77
93
|
switch (subcommand) {
|
|
78
94
|
case 'list': return list(flags);
|
|
79
|
-
case 'create': return create(flags);
|
|
95
|
+
case 'create': return create(args[0], flags);
|
|
80
96
|
case 'delete': return del(args[0], flags);
|
|
81
97
|
case 'delivery': return delivery(args[0], flags);
|
|
82
98
|
default: error(`Unknown subcommand: ${subcommand}. Run "myapi webhook --help" for a list of valid subcommands.`);
|
|
@@ -3,7 +3,7 @@ import { type Flags } from '../helpers.js';
|
|
|
3
3
|
export declare const SCHEMA: FlagSchema;
|
|
4
4
|
export declare function list(flags: Flags): Promise<void>;
|
|
5
5
|
export declare function get(id: string, flags: Flags): Promise<void>;
|
|
6
|
-
export declare function create(flags: Flags): Promise<void>;
|
|
6
|
+
export declare function create(nameArg: string | undefined, flags: Flags): Promise<void>;
|
|
7
7
|
export declare function update(id: string, flags: Flags): Promise<void>;
|
|
8
8
|
export declare function enable(id: string, flags: Flags): Promise<void>;
|
|
9
9
|
export declare function disable(id: string, flags: Flags): Promise<void>;
|
|
@@ -9,6 +9,23 @@ export const SCHEMA = {
|
|
|
9
9
|
steps: 'string',
|
|
10
10
|
'no-enable': 'boolean',
|
|
11
11
|
};
|
|
12
|
+
// Mirrors the backend's SupportedStepTypes list. Both alias and underscore
|
|
13
|
+
// forms are accepted by the workflow runner. Keep this in sync if the
|
|
14
|
+
// backend grows new step types.
|
|
15
|
+
const SUPPORTED_STEP_TYPES = ['send_email', 'email', 'slack_message', 'slack'];
|
|
16
|
+
function validateSteps(steps) {
|
|
17
|
+
if (!Array.isArray(steps))
|
|
18
|
+
error('--steps must be a JSON array of step objects.');
|
|
19
|
+
steps.forEach((s, i) => {
|
|
20
|
+
if (!s || typeof s !== 'object')
|
|
21
|
+
error(`step ${i}: must be a JSON object.`);
|
|
22
|
+
if (!s.type)
|
|
23
|
+
error(`step ${i}: missing required field "type". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
|
|
24
|
+
if (!SUPPORTED_STEP_TYPES.includes(s.type)) {
|
|
25
|
+
error(`step ${i}: unknown type "${s.type}". Supported: ${SUPPORTED_STEP_TYPES.join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
12
29
|
function summarizeWorkflow(w) {
|
|
13
30
|
return {
|
|
14
31
|
id: w.id,
|
|
@@ -52,13 +69,13 @@ export async function get(id, flags) {
|
|
|
52
69
|
const wf = await sdkWorkflow.getWorkflow(config.api_key, orgId, id);
|
|
53
70
|
printJson(wf);
|
|
54
71
|
}
|
|
55
|
-
export async function create(flags) {
|
|
72
|
+
export async function create(nameArg, flags) {
|
|
56
73
|
const config = requireConfig();
|
|
57
|
-
const orgId = requireOrg(flags, config, 'myapi workflow create
|
|
58
|
-
const name = flags.name;
|
|
74
|
+
const orgId = requireOrg(flags, config, 'myapi workflow create <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]');
|
|
75
|
+
const name = nameArg || flags.name;
|
|
59
76
|
const endpointId = flags['endpoint-id'];
|
|
60
77
|
if (!name || !endpointId || !flags.steps) {
|
|
61
|
-
error('Missing required arguments.\nUsage: myapi workflow create --name <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]');
|
|
78
|
+
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>]');
|
|
62
79
|
}
|
|
63
80
|
let steps;
|
|
64
81
|
try {
|
|
@@ -67,6 +84,7 @@ export async function create(flags) {
|
|
|
67
84
|
catch {
|
|
68
85
|
error('Invalid JSON for --steps');
|
|
69
86
|
}
|
|
87
|
+
validateSteps(steps);
|
|
70
88
|
const wf = await sdkWorkflow.createWorkflow(config.api_key, orgId, {
|
|
71
89
|
name,
|
|
72
90
|
trigger_config: { endpoint_id: endpointId },
|
|
@@ -98,6 +116,7 @@ export async function update(id, flags) {
|
|
|
98
116
|
catch {
|
|
99
117
|
error('Invalid JSON for --steps');
|
|
100
118
|
}
|
|
119
|
+
validateSteps(payload.steps);
|
|
101
120
|
}
|
|
102
121
|
if (!payload.name && !payload.trigger_config && !payload.steps) {
|
|
103
122
|
error('Nothing to update. Provide at least one of --name, --endpoint-id, --steps.');
|
|
@@ -156,7 +175,27 @@ export async function getRun(runId, flags) {
|
|
|
156
175
|
const SUBCOMMAND_USAGE = {
|
|
157
176
|
'list': 'myapi workflow list [--org <id>] [--json]',
|
|
158
177
|
'get': 'myapi workflow get <id> [--org <id>]',
|
|
159
|
-
'create':
|
|
178
|
+
'create': `myapi workflow create <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]
|
|
179
|
+
myapi workflow create --name <name> --endpoint-id <id> --steps <json> [--no-enable] [--org <id>]
|
|
180
|
+
|
|
181
|
+
Either form works; the positional name is the recommended shape.
|
|
182
|
+
|
|
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
|
|
186
|
+
|
|
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.
|
|
191
|
+
|
|
192
|
+
Example — fire on every webhook POST, send a thank-you email:
|
|
193
|
+
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."}
|
|
196
|
+
]'
|
|
197
|
+
|
|
198
|
+
See the my-workflow-api skill for the full form-to-email recipe.`,
|
|
160
199
|
'update': 'myapi workflow update <id> [--name <str>] [--endpoint-id <id>] [--steps <json>] [--org <id>]',
|
|
161
200
|
'enable': 'myapi workflow enable <id> [--org <id>]',
|
|
162
201
|
'disable': 'myapi workflow disable <id> [--org <id>]',
|
|
@@ -197,7 +236,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
197
236
|
switch (subcommand) {
|
|
198
237
|
case 'list': return list(flags);
|
|
199
238
|
case 'get': return get(args[0], flags);
|
|
200
|
-
case 'create': return create(flags);
|
|
239
|
+
case 'create': return create(args[0], flags);
|
|
201
240
|
case 'update': return update(args[0], flags);
|
|
202
241
|
case 'enable': return enable(args[0], flags);
|
|
203
242
|
case 'disable': return disable(args[0], flags);
|
package/dist/config.js
CHANGED
|
@@ -48,8 +48,14 @@ export function loadConfig() {
|
|
|
48
48
|
config.api_key = envKey;
|
|
49
49
|
return config;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
// Single point of truth for writes. Ensures CONFIG_DIR exists and that the
|
|
52
|
+
// file is created with mode 0o600 (owner read/write only) — no caller has
|
|
53
|
+
// to remember either.
|
|
54
|
+
function writeFullConfig(full) {
|
|
52
55
|
ensureDir();
|
|
56
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { mode: 0o600 });
|
|
57
|
+
}
|
|
58
|
+
export function saveConfig(config) {
|
|
53
59
|
const full = loadFullConfig() ?? { active: 0, accounts: [] };
|
|
54
60
|
const { autocomplete_setup, ...account } = config;
|
|
55
61
|
if (full.accounts.length === 0)
|
|
@@ -58,10 +64,9 @@ export function saveConfig(config) {
|
|
|
58
64
|
full.accounts[full.active] = account;
|
|
59
65
|
if (autocomplete_setup !== undefined)
|
|
60
66
|
full.autocomplete_setup = autocomplete_setup;
|
|
61
|
-
|
|
67
|
+
writeFullConfig(full);
|
|
62
68
|
}
|
|
63
69
|
export function addAccount(account) {
|
|
64
|
-
ensureDir();
|
|
65
70
|
const full = loadFullConfig() ?? { active: 0, accounts: [] };
|
|
66
71
|
// Replace if same account_id already exists.
|
|
67
72
|
const existing = full.accounts.findIndex(a => a.account_id === account.account_id);
|
|
@@ -73,7 +78,7 @@ export function addAccount(account) {
|
|
|
73
78
|
full.accounts.push(account);
|
|
74
79
|
full.active = full.accounts.length - 1;
|
|
75
80
|
}
|
|
76
|
-
|
|
81
|
+
writeFullConfig(full);
|
|
77
82
|
return full.active;
|
|
78
83
|
}
|
|
79
84
|
export function switchAccount(index) {
|
|
@@ -81,7 +86,7 @@ export function switchAccount(index) {
|
|
|
81
86
|
if (!full || index < 0 || index >= full.accounts.length)
|
|
82
87
|
return false;
|
|
83
88
|
full.active = index;
|
|
84
|
-
|
|
89
|
+
writeFullConfig(full);
|
|
85
90
|
return true;
|
|
86
91
|
}
|
|
87
92
|
export function listAccounts() {
|