@myapihq/cli 2.3.1 → 2.4.0
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/account.d.ts +1 -1
- package/dist/commands/account.js +1 -0
- package/dist/commands/authproduct.js +7 -3
- package/dist/commands/billing.js +7 -2
- package/dist/commands/container.js +4 -3
- package/dist/commands/database.js +4 -3
- package/dist/commands/domain.js +39 -14
- package/dist/commands/email/message.js +6 -3
- package/dist/commands/email/template.js +3 -3
- package/dist/commands/fn.js +4 -3
- package/dist/commands/funnel.js +33 -13
- package/dist/commands/git.js +108 -35
- package/dist/commands/image.js +7 -4
- package/dist/commands/keys-validation.test.js +5 -0
- package/dist/commands/keys.js +6 -2
- package/dist/commands/llm.js +14 -10
- package/dist/commands/login-validation.test.d.ts +1 -0
- package/dist/commands/login-validation.test.js +43 -0
- package/dist/commands/login.d.ts +14 -0
- package/dist/commands/login.js +447 -0
- package/dist/commands/org.d.ts +1 -1
- package/dist/commands/org.js +24 -7
- package/dist/commands/queue.js +4 -3
- package/dist/commands/setup.js +6 -1
- package/dist/commands/storage.js +1 -1
- package/dist/commands/workflow.js +5 -4
- package/dist/completion.js +3 -3
- package/dist/config.js +3 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +62 -0
- package/dist/errors.test.d.ts +1 -0
- package/dist/errors.test.js +28 -0
- package/dist/exposes.test.js +1 -0
- package/dist/flags.js +5 -3
- package/dist/flags.test.js +1 -1
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +22 -0
- package/dist/index.js +10 -51
- package/dist/skills/my-api-hq/SKILL.md +2 -2
- package/dist/skills/my-domain-api/SKILL.md +5 -5
- package/dist/skills/my-email-verify-api/SKILL.md +6 -4
- package/dist/skills/my-funnel-api/SKILL.md +5 -4
- package/dist/skills/my-git-api/SKILL.md +14 -7
- package/dist/skills/my-image-api/SKILL.md +1 -1
- package/dist/skills/my-storage-api/SKILL.md +5 -5
- package/dist/skills/my-webhook-api/SKILL.md +1 -1
- package/dist/skills/my-workflow-api/SKILL.md +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -1
- package/package.json +3 -2
|
@@ -3,7 +3,7 @@ import type { Flags } from '../helpers.js';
|
|
|
3
3
|
import type { Exposes } from '../exposes.js';
|
|
4
4
|
export declare const EXPOSES: Exposes;
|
|
5
5
|
export declare const SCHEMA: FlagSchema;
|
|
6
|
-
export declare const HELP = "Usage: myapi account <subcommand>\n\nSubcommands:\n api-keys Manage API keys \u00B7 list / create / revoke\n config Manage CLI defaults (org, funnel, domain) \u00B7 supports set-org / set-funnel / set-domain\n import-key Import an existing API key non-interactively\n install-skills DEPRECATED \u2014 use `myapi install-skills` instead\n keys Alias for api-keys\n link [email] Upgrade anonymous account to registered (or add a second session)\n Use myapi account setup to create a completely new account\n mailing-address Get or set the account's mailing address (CAN-SPAM)\n registrant Manage stored WHOIS contact info for domain registration \u00B7 set / get / clear\n setup Configure your account\n switch [index] Switch active account by index or email\n whoami Show current account \u00B7 supports --json";
|
|
6
|
+
export declare const HELP = "Usage: myapi account <subcommand>\n\nSubcommands:\n api-keys Manage API keys \u00B7 list / create / revoke\n config Manage CLI defaults (org, funnel, domain) \u00B7 supports set-org / set-funnel / set-domain\n import-key Import an existing API key non-interactively\n install-skills DEPRECATED \u2014 use `myapi install-skills` instead\n keys Alias for api-keys\n link [email] Upgrade anonymous account to registered (or add a second session)\n Use myapi account setup to create a completely new account\n login Sign in via your browser \u2014 Google or email code (preview: --mock)\n mailing-address Get or set the account's mailing address (CAN-SPAM)\n registrant Manage stored WHOIS contact info for domain registration \u00B7 set / get / clear\n setup Configure your account\n switch [index] Switch active account by index or email\n whoami Show current account \u00B7 supports --json";
|
|
7
7
|
export declare const INSTALL_SKILLS_HELP = "Usage: myapi install-skills\n\nInstalls the MyAPI skills pack for AI coding agents (Claude, Codex, Gemini, \u2026).\n\nThis command writes skill definition files to:\n ~/.agents/skills/myapi/\n\nAnd creates symlinks in the appropriate agent config directories:\n ~/.claude/ (Claude)\n ~/.gemini/ (Gemini)\n ~/.cursor/ (Cursor, if detected)\n\nThese files teach agents how to use the MyAPI CLI and API directly.\nRun this command again to update existing skills to the latest version.\n\nNote: `myapi account install-skills` is deprecated and will be removed in the next minor.\nUse `myapi install-skills` going forward.";
|
|
8
8
|
export declare function link(flags?: Flags, emailArg?: string): Promise<void>;
|
|
9
9
|
export declare function whoami(flags?: Flags): Promise<void>;
|
package/dist/commands/account.js
CHANGED
|
@@ -36,6 +36,7 @@ Subcommands:
|
|
|
36
36
|
keys Alias for api-keys
|
|
37
37
|
link [email] Upgrade anonymous account to registered (or add a second session)
|
|
38
38
|
Use myapi account setup to create a completely new account
|
|
39
|
+
login Sign in via your browser — Google or email code (preview: --mock)
|
|
39
40
|
mailing-address Get or set the account's mailing address (CAN-SPAM)
|
|
40
41
|
registrant Manage stored WHOIS contact info for domain registration · set / get / clear
|
|
41
42
|
setup Configure your account
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { auth as sdkAuth } from '@myapihq/sdk';
|
|
1
|
+
import { auth as sdkAuth, MyApiError } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
3
|
import { confirm, isNonInteractive } from '../prompt.js';
|
|
4
4
|
import { success, error, info, printTable, printJson } from '../output.js';
|
|
@@ -270,11 +270,15 @@ async function domain(args, flags) {
|
|
|
270
270
|
let target = host;
|
|
271
271
|
if (!target) {
|
|
272
272
|
// No host given — resolve the current one so the user doesn't have to retype it.
|
|
273
|
+
// Only a 404 means "nothing configured"; a network/auth/server error must
|
|
274
|
+
// surface as itself, not be misreported as absent state.
|
|
273
275
|
try {
|
|
274
276
|
target = (await sdkAuth.getDomain(config.api_key, orgId)).domain;
|
|
275
277
|
}
|
|
276
|
-
catch {
|
|
277
|
-
|
|
278
|
+
catch (e) {
|
|
279
|
+
if (e instanceof MyApiError && e.status === 404)
|
|
280
|
+
error('No custom auth domain to delete.');
|
|
281
|
+
throw e;
|
|
278
282
|
}
|
|
279
283
|
}
|
|
280
284
|
await sdkAuth.deleteDomain(config.api_key, orgId, target);
|
package/dist/commands/billing.js
CHANGED
|
@@ -162,8 +162,13 @@ export async function usage(flags) {
|
|
|
162
162
|
info(`Total: ${res.total_display}`);
|
|
163
163
|
}
|
|
164
164
|
export async function topup(amountStr, flags) {
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
// Money-moving input is validated strictly: whole dollars only, no
|
|
166
|
+
// trailing garbage, no silent rounding ("10.9" must not charge $11).
|
|
167
|
+
if (!amountStr || !/^\d+$/.test(amountStr)) {
|
|
168
|
+
error('Amount must be a positive whole number of dollars (e.g. myapi billing topup 10)');
|
|
169
|
+
}
|
|
170
|
+
const amount = Number(amountStr);
|
|
171
|
+
if (amount <= 0) {
|
|
167
172
|
error('Amount must be a positive whole number of dollars (e.g. myapi billing topup 10)');
|
|
168
173
|
}
|
|
169
174
|
if (!flags.yes && !flags.y && amount >= 50) {
|
|
@@ -4,7 +4,7 @@ import { container as sdkContainer } from '@myapihq/sdk';
|
|
|
4
4
|
import { requireConfig } from '../config.js';
|
|
5
5
|
import { success, error, printTable, info, printJson, banner } from '../output.js';
|
|
6
6
|
import { formatDate, pollJob } from '../utils.js';
|
|
7
|
-
import { requireOrg } from '../helpers.js';
|
|
7
|
+
import { requireOrg, confirmDestructive } from '../helpers.js';
|
|
8
8
|
export const EXPOSES = [
|
|
9
9
|
'POST /container/orgs/{org_id}/containers',
|
|
10
10
|
'GET /container/orgs/{org_id}/containers',
|
|
@@ -162,11 +162,12 @@ export async function get(id, flags) {
|
|
|
162
162
|
}
|
|
163
163
|
export async function del(id, flags) {
|
|
164
164
|
const config = requireConfig();
|
|
165
|
-
const orgId = requireOrg(flags, config, 'myapi container delete <id> [--org <id>]');
|
|
165
|
+
const orgId = requireOrg(flags, config, 'myapi container delete <id> [--yes] [--org <id>]');
|
|
166
166
|
if (!id)
|
|
167
167
|
error('Missing id.\nUsage: myapi container delete <id>');
|
|
168
|
+
await confirmDestructive(flags, `delete container ${id} in org ${orgId}`, 'myapi container delete <id> --yes [--org <id>]');
|
|
168
169
|
await sdkContainer.deleteContainer(config.api_key, orgId, id);
|
|
169
|
-
success(`Deleted container ${id}`);
|
|
170
|
+
success(`Deleted container ${id} (org ${orgId})`);
|
|
170
171
|
}
|
|
171
172
|
// _isTarball returns true if the path looks like an already-built tar archive
|
|
172
173
|
// (so we upload it as-is instead of tarring a directory).
|
|
@@ -7,7 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import { database as sdk, MyApiError } from '@myapihq/sdk';
|
|
8
8
|
import { requireConfig } from '../config.js';
|
|
9
9
|
import { success, error, info, printTable, printJson } from '../output.js';
|
|
10
|
-
import { requireOrg, requireArg } from '../helpers.js';
|
|
10
|
+
import { requireOrg, requireArg, confirmDestructive } from '../helpers.js';
|
|
11
11
|
export const EXPOSES = [
|
|
12
12
|
'GET /database/orgs/{org_id}/namespaces',
|
|
13
13
|
'POST /database/orgs/{org_id}/namespaces',
|
|
@@ -97,10 +97,11 @@ async function nsCreate(name, flags) {
|
|
|
97
97
|
}
|
|
98
98
|
async function nsDelete(name, flags) {
|
|
99
99
|
const config = requireConfig();
|
|
100
|
-
const orgId = requireOrg(flags, config, 'myapi database delete-namespace <name> [--org <id>]');
|
|
100
|
+
const orgId = requireOrg(flags, config, 'myapi database delete-namespace <name> [--yes] [--org <id>]');
|
|
101
101
|
requireArg(name, 'name', 'myapi database delete-namespace <name>');
|
|
102
|
+
await confirmDestructive(flags, `delete namespace "${name}" and ALL its keys in org ${orgId}`, 'myapi database delete-namespace <name> --yes [--org <id>]');
|
|
102
103
|
await sdk.deleteNamespace(config.api_key, orgId, name);
|
|
103
|
-
success(`Namespace "${name}" and all its keys deleted.`);
|
|
104
|
+
success(`Namespace "${name}" and all its keys deleted (org ${orgId}).`);
|
|
104
105
|
}
|
|
105
106
|
// ── Keys ─────────────────────────────────────────────────────────────────
|
|
106
107
|
async function keysList(flags) {
|
package/dist/commands/domain.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { domain as sdkDomain, email as sdkEmail } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../config.js';
|
|
3
3
|
import { success, error, info, printTable, printJson } from '../output.js';
|
|
4
|
-
import { formatDate } from '../utils.js';
|
|
4
|
+
import { formatDate, retryFunds } from '../utils.js';
|
|
5
5
|
import { resolveRegistrantForRegister } from '../registrant.js';
|
|
6
|
+
import { confirm, isNonInteractive } from '../prompt.js';
|
|
6
7
|
import { requireOrg, requireDomain } from '../helpers.js';
|
|
7
8
|
export const EXPOSES = [
|
|
8
9
|
'GET /domain/orgs/{org_id}/list',
|
|
@@ -63,6 +64,8 @@ export const SCHEMA = {
|
|
|
63
64
|
subdomain: 'string',
|
|
64
65
|
// Assign: opt out of binding the www Worker route alongside apex.
|
|
65
66
|
'no-www': 'boolean',
|
|
67
|
+
// Assign: override the reassignment pre-flight guard.
|
|
68
|
+
force: 'boolean',
|
|
66
69
|
};
|
|
67
70
|
export async function check(domainArg, flags) {
|
|
68
71
|
const config = requireConfig();
|
|
@@ -97,7 +100,7 @@ export async function register(domainArg, flags) {
|
|
|
97
100
|
// Required by ICANN; resolver throws a useful error if nothing's set up
|
|
98
101
|
// in non-interactive contexts, or runs an interactive prompt for humans.
|
|
99
102
|
const registrant = await resolveRegistrantForRegister(flags, config);
|
|
100
|
-
await sdkDomain.registerDomain(config.api_key, orgId, domainArg, { years, registrant });
|
|
103
|
+
await retryFunds(() => sdkDomain.registerDomain(config.api_key, orgId, domainArg, { years, registrant }));
|
|
101
104
|
success(`Registered ${domainArg}!`);
|
|
102
105
|
info(`Assign it to your org now:\n myapi domain assign ${domainArg} --org ${orgId}`);
|
|
103
106
|
info(`DNS propagation can take a few minutes — track it with: myapi domain status ${domainArg}`);
|
|
@@ -166,7 +169,15 @@ export async function renew(domainArg, flags) {
|
|
|
166
169
|
const orgId = requireOrg(flags, config, 'myapi domain renew <domain> [--yes] [--org <id>]');
|
|
167
170
|
if (!domainArg)
|
|
168
171
|
error('Missing required arguments.\nUsage: myapi domain renew <domain> [--yes] [--org <id>]');
|
|
169
|
-
|
|
172
|
+
if (!flags.yes) {
|
|
173
|
+
if (isNonInteractive()) {
|
|
174
|
+
error(`Refusing to renew ${domainArg} without --yes — renewal charges org ${orgId}'s balance immediately.\nUsage: myapi domain renew <domain> --yes [--org <id>]`);
|
|
175
|
+
}
|
|
176
|
+
const ok = await confirm(`› Renew ${domainArg} now? This charges org ${orgId}'s balance. (y/N) `, false);
|
|
177
|
+
if (!ok)
|
|
178
|
+
error('Aborted.');
|
|
179
|
+
}
|
|
180
|
+
await retryFunds(() => sdkDomain.renewDomain(config.api_key, orgId, domainArg));
|
|
170
181
|
success(`Renewed ${domainArg}.`);
|
|
171
182
|
info(`Track expiry with: myapi domain status ${domainArg}`);
|
|
172
183
|
}
|
|
@@ -196,6 +207,23 @@ export async function assign(domainArg, flags) {
|
|
|
196
207
|
const orgId = requireOrg(flags, config, 'myapi domain assign <domain> [--no-www] [--org <id>]');
|
|
197
208
|
if (!domainArg)
|
|
198
209
|
error('Missing required arguments.\nUsage: myapi domain assign <domain> [--no-www] [--org <id>]');
|
|
210
|
+
// Assign is also the reassign path: if the domain is currently bound to a
|
|
211
|
+
// *different* org, a bare assign silently moves it (this has bitten us in
|
|
212
|
+
// production). Pre-flight the current binding and require --force to move.
|
|
213
|
+
// A failed pre-read aborts — never downgrade a safety check to a guess.
|
|
214
|
+
if (!flags.force) {
|
|
215
|
+
let current;
|
|
216
|
+
try {
|
|
217
|
+
const domains = await sdkDomain.listDomains(config.api_key, orgId, 'all');
|
|
218
|
+
current = domains.find(d => d.domain === domainArg);
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
error(`Could not verify ${domainArg}'s current assignment (${e instanceof Error ? e.message : e}).\nRefusing to assign blind — retry, or pass --force to skip this check.`);
|
|
222
|
+
}
|
|
223
|
+
if (current?.org_id && current.org_id !== orgId) {
|
|
224
|
+
error(`${domainArg} is currently assigned to org ${current.org_id}.\nAssigning it to org ${orgId} would MOVE it. Re-run with --force to reassign.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
199
227
|
const includeWww = flags['no-www'] ? false : undefined; // omitted → backend default (true).
|
|
200
228
|
const res = await sdkDomain.assignDomain(config.api_key, orgId, domainArg, { includeWww });
|
|
201
229
|
if (flags.json) {
|
|
@@ -398,10 +426,6 @@ async function recordsGet(domainArg, recordId, flags) {
|
|
|
398
426
|
if (!recordId)
|
|
399
427
|
error('Missing record id.\nUsage: myapi domain records get <domain> <record-id>');
|
|
400
428
|
const r = await sdkDomain.getDnsRecord(config.api_key, orgId, domain, recordId);
|
|
401
|
-
if (flags.json) {
|
|
402
|
-
printJson(r);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
429
|
printJson(r);
|
|
406
430
|
}
|
|
407
431
|
async function recordsCreate(domainArg, flags) {
|
|
@@ -564,14 +588,15 @@ After running:
|
|
|
564
588
|
2. At your current registrar, change the nameservers to the values returned.
|
|
565
589
|
3. Wait for propagation. Use "myapi domain status <domain> --watch" to track
|
|
566
590
|
activation — the backend live-checks Cloudflare each time you poll.`,
|
|
567
|
-
'renew': `myapi domain renew <domain> [--org <id>]
|
|
591
|
+
'renew': `myapi domain renew <domain> [--yes] [--org <id>]
|
|
568
592
|
|
|
569
593
|
Renews a registered domain for one more registration period (typically 1 year).
|
|
570
594
|
Charged against the org's billing balance — confirm with "myapi billing balance"
|
|
571
|
-
before running.
|
|
595
|
+
before running. Asks for confirmation; pass --yes to skip (required in
|
|
596
|
+
non-interactive runs).
|
|
572
597
|
|
|
573
598
|
Track expiry afterward with: myapi domain status <domain>`,
|
|
574
|
-
'assign': `myapi domain assign <domain> [--no-www] [--org <id>]
|
|
599
|
+
'assign': `myapi domain assign <domain> [--no-www] [--force] [--org <id>]
|
|
575
600
|
|
|
576
601
|
Assigns a registered or imported domain to an org. Backend binds Worker
|
|
577
602
|
routes for both <domain>/* and www.<domain>/* by default; www returns a 301
|
|
@@ -580,11 +605,11 @@ at https://<domain> after DNS propagation completes.
|
|
|
580
605
|
|
|
581
606
|
--no-www Skip the www route binding. The www form will not resolve at the
|
|
582
607
|
edge unless you create your own DNS + Worker setup for it.
|
|
608
|
+
--force Skip the reassignment pre-flight guard.
|
|
583
609
|
|
|
584
|
-
WARNING: This is also the reassign path. If
|
|
585
|
-
to a different org,
|
|
586
|
-
|
|
587
|
-
to confirm the current binding before reassigning.`,
|
|
610
|
+
WARNING: This is also the reassign path. If the domain is currently assigned
|
|
611
|
+
to a different org, assign refuses unless you pass --force. Check the current
|
|
612
|
+
binding with: myapi domain list --filter all`,
|
|
588
613
|
'unassign': 'myapi domain unassign <domain> [--org <id>]',
|
|
589
614
|
'status': `myapi domain status <domain> [--org <id>] [--watch] [--json]
|
|
590
615
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { email as sdkEmail } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../../config.js';
|
|
3
3
|
import { success, error, printTable, printJson, info } from '../../output.js';
|
|
4
|
+
import { retryFunds } from '../../utils.js';
|
|
4
5
|
export const EXPOSES = [
|
|
5
6
|
'POST /email/send',
|
|
6
7
|
'GET /email/status/{message_id}',
|
|
@@ -22,7 +23,9 @@ function summarizeMessage(m) {
|
|
|
22
23
|
}
|
|
23
24
|
async function send(flags) {
|
|
24
25
|
const config = requireConfig();
|
|
25
|
-
|
|
26
|
+
// typeof check (not truthiness): a bare `--to` whose value got swallowed by
|
|
27
|
+
// the next flag parses as `true`, which must fail here — not reach the API.
|
|
28
|
+
if (typeof flags.from !== 'string' || typeof flags.to !== 'string' || typeof flags.subject !== 'string') {
|
|
26
29
|
error('Missing required arguments.\nUsage: myapi email message send --from <email> --to <email> --subject <str> [--body <str> | --html <str> | --template-id <id> [--template-vars <json>]]');
|
|
27
30
|
}
|
|
28
31
|
if (!flags.body && !flags.html && !flags['template-id'])
|
|
@@ -38,7 +41,7 @@ async function send(flags) {
|
|
|
38
41
|
error('--template-vars must be valid JSON');
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
|
-
const res = await sdkEmail.sendEmail(config.api_key, {
|
|
44
|
+
const res = await retryFunds(() => sdkEmail.sendEmail(config.api_key, {
|
|
42
45
|
from: flags.from,
|
|
43
46
|
to: [flags.to],
|
|
44
47
|
subject: flags.subject,
|
|
@@ -46,7 +49,7 @@ async function send(flags) {
|
|
|
46
49
|
html: flags.html,
|
|
47
50
|
template_id: flags['template-id'],
|
|
48
51
|
template_vars: templateVars,
|
|
49
|
-
});
|
|
52
|
+
}));
|
|
50
53
|
success(`Email sent! Message ID: ${res.message_id}`);
|
|
51
54
|
}
|
|
52
55
|
async function status(messageId, _flags) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { email as sdkEmail } from '@myapihq/sdk';
|
|
2
2
|
import { requireConfig } from '../../config.js';
|
|
3
3
|
import { success, error, printTable, printJson, info } from '../../output.js';
|
|
4
|
-
import { pollJob } from '../../utils.js';
|
|
4
|
+
import { pollJob, retryFunds } from '../../utils.js';
|
|
5
5
|
import { requireOrg } from '../../helpers.js';
|
|
6
6
|
export const EXPOSES = [
|
|
7
7
|
'POST /email/orgs/{org_id}/templates/generate',
|
|
@@ -20,10 +20,10 @@ async function generate(nameArg, flags) {
|
|
|
20
20
|
if (!name || !flags.prompt) {
|
|
21
21
|
error('Missing required arguments.\nUsage: myapi email template generate <name> --prompt <str> [--org <id>]\n or: myapi email template generate --name <name> --prompt <str> [--org <id>]');
|
|
22
22
|
}
|
|
23
|
-
const job = await sdkEmail.generateTemplate(config.api_key, orgId, {
|
|
23
|
+
const job = await retryFunds(() => sdkEmail.generateTemplate(config.api_key, orgId, {
|
|
24
24
|
prompt: flags.prompt,
|
|
25
25
|
name,
|
|
26
|
-
});
|
|
26
|
+
}));
|
|
27
27
|
const status = await pollJob({
|
|
28
28
|
label: 'Generating template',
|
|
29
29
|
timeoutMs: 90_000,
|
package/dist/commands/fn.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fn as sdkFn } from '@myapihq/sdk';
|
|
|
3
3
|
import { requireConfig } from '../config.js';
|
|
4
4
|
import { success, error, printTable, info, printJson, banner } from '../output.js';
|
|
5
5
|
import { formatDate } from '../utils.js';
|
|
6
|
-
import { requireOrg } from '../helpers.js';
|
|
6
|
+
import { requireOrg, confirmDestructive } from '../helpers.js';
|
|
7
7
|
export const EXPOSES = [
|
|
8
8
|
'POST /function/orgs/{org_id}/functions',
|
|
9
9
|
'GET /function/orgs/{org_id}/functions',
|
|
@@ -126,11 +126,12 @@ export async function get(id, flags) {
|
|
|
126
126
|
}
|
|
127
127
|
export async function del(id, flags) {
|
|
128
128
|
const config = requireConfig();
|
|
129
|
-
const orgId = requireOrg(flags, config, 'myapi fn delete <id> [--org <id>]');
|
|
129
|
+
const orgId = requireOrg(flags, config, 'myapi fn delete <id> [--yes] [--org <id>]');
|
|
130
130
|
if (!id)
|
|
131
131
|
error('Missing id.\nUsage: myapi fn delete <id>');
|
|
132
|
+
await confirmDestructive(flags, `delete function ${id} in org ${orgId}`, 'myapi fn delete <id> --yes [--org <id>]');
|
|
132
133
|
await sdkFn.deleteFunction(config.api_key, orgId, id);
|
|
133
|
-
success(`Deleted function ${id}`);
|
|
134
|
+
success(`Deleted function ${id} (org ${orgId})`);
|
|
134
135
|
}
|
|
135
136
|
// deploy uploads a single-file JS bundle. The backend wraps it with the
|
|
136
137
|
// MYAPI shim and ships it to Cloudflare Workers. The scoped API key is
|
package/dist/commands/funnel.js
CHANGED
|
@@ -4,7 +4,7 @@ import { funnel as sdkFunnel, hq } from '@myapihq/sdk';
|
|
|
4
4
|
import { requireConfig } from '../config.js';
|
|
5
5
|
import { success, error, printTable, info, printJson } from '../output.js';
|
|
6
6
|
import { formatDate } from '../utils.js';
|
|
7
|
-
import { requireOrg } from '../helpers.js';
|
|
7
|
+
import { requireOrg, confirmDestructive } from '../helpers.js';
|
|
8
8
|
export const EXPOSES = [
|
|
9
9
|
'POST /funnel/orgs/{org_id}/funnels',
|
|
10
10
|
'GET /funnel/orgs/{org_id}/funnels',
|
|
@@ -77,7 +77,7 @@ export async function list(flags) {
|
|
|
77
77
|
}
|
|
78
78
|
const rows = funnels.map(f => ({
|
|
79
79
|
id: f.id,
|
|
80
|
-
|
|
80
|
+
subdomain_url: f.subdomain_url || '',
|
|
81
81
|
created_at: formatDate(f.created_at),
|
|
82
82
|
updated_at: formatDate(f.updated_at),
|
|
83
83
|
}));
|
|
@@ -128,11 +128,12 @@ export async function get(id, flags) {
|
|
|
128
128
|
}
|
|
129
129
|
export async function del(id, flags) {
|
|
130
130
|
const config = requireConfig();
|
|
131
|
-
const orgId = requireOrg(flags, config, 'myapi funnel delete <id> [--org <id>]');
|
|
131
|
+
const orgId = requireOrg(flags, config, 'myapi funnel delete <id> [--yes] [--org <id>]');
|
|
132
132
|
if (!id)
|
|
133
|
-
error('Missing required arguments.\nUsage: myapi funnel delete <id> [--org <id>]');
|
|
133
|
+
error('Missing required arguments.\nUsage: myapi funnel delete <id> [--yes] [--org <id>]');
|
|
134
|
+
await confirmDestructive(flags, `delete funnel ${id} (and all its live pages) in org ${orgId}`, 'myapi funnel delete <id> --yes [--org <id>]');
|
|
134
135
|
await sdkFunnel.deleteFunnel(config.api_key, orgId, id);
|
|
135
|
-
success(`Funnel ${id} deleted`);
|
|
136
|
+
success(`Funnel ${id} deleted (org ${orgId})`);
|
|
136
137
|
}
|
|
137
138
|
// List the pages currently published to a funnel. Resolves the funnel id
|
|
138
139
|
// from positional arg, --funnel flag, or the user's default funnel.
|
|
@@ -206,19 +207,34 @@ This funnel already serves a page at ${finalSlug}; pushing would replace it.
|
|
|
206
207
|
error("No content provided via stdin. Usage: echo '<h1>Hello</h1>' | myapi funnel push [slug]");
|
|
207
208
|
const result = await sdkFunnel.pushFunnelPage(config.api_key, orgId, funnelId, { slug: finalSlug, html });
|
|
208
209
|
const { orgLabel, funnelLabel } = await describeTarget(config.api_key, orgId, funnelId);
|
|
210
|
+
let liveUrl = result?.url;
|
|
211
|
+
if (!liveUrl) {
|
|
212
|
+
// Best-effort URL enrichment — the push already succeeded, so a failed
|
|
213
|
+
// org lookup must not turn a successful write into a non-zero exit.
|
|
214
|
+
const org = await hq.getOrg(config.api_key, orgId).catch(() => null);
|
|
215
|
+
if (org?.preview_subdomain) {
|
|
216
|
+
liveUrl = `https://${org.preview_subdomain}.makeautonomous.com${finalSlug}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Machine output: the live URL is carried as `subdomain_url` — the same field
|
|
220
|
+
// name `funnel get`/`list`/`create` use — so an agent that pushes has the URL
|
|
221
|
+
// in hand without a follow-up `funnel get` (and without guessing the field).
|
|
222
|
+
if (flags.json) {
|
|
223
|
+
printJson({
|
|
224
|
+
slug: finalSlug,
|
|
225
|
+
subdomain_url: liveUrl ?? null,
|
|
226
|
+
overwritten: Boolean(clash),
|
|
227
|
+
org_id: orgId,
|
|
228
|
+
funnel_id: funnelId,
|
|
229
|
+
});
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
209
232
|
success(`Pushed ${clash ? '(overwrote) ' : ''}page to ${finalSlug}`);
|
|
210
233
|
info(`Org: ${orgLabel}`);
|
|
211
234
|
info(`Funnel: ${funnelLabel}`);
|
|
212
235
|
if (resolvedFromOrg) {
|
|
213
236
|
info(`(Auto-picked the org's only funnel. Pin it explicitly with --funnel ${funnelId} or: myapi config set-funnel ${funnelId})`);
|
|
214
237
|
}
|
|
215
|
-
let liveUrl = result?.url;
|
|
216
|
-
if (!liveUrl) {
|
|
217
|
-
const org = await hq.getOrg(config.api_key, orgId);
|
|
218
|
-
if (org.preview_subdomain) {
|
|
219
|
-
liveUrl = `https://${org.preview_subdomain}.makeautonomous.com${finalSlug}`;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
238
|
if (liveUrl)
|
|
223
239
|
info(`Preview: ${liveUrl}`);
|
|
224
240
|
// If we're still on the preview subdomain, hint at how to serve on a
|
|
@@ -563,7 +579,7 @@ Examples:
|
|
|
563
579
|
myapi funnel publish ./dist # prod — refused if a site exists
|
|
564
580
|
myapi funnel publish ./dist --force # prod — replace existing site
|
|
565
581
|
myapi funnel publish ./site --api-fn <function_id>`,
|
|
566
|
-
'push': `myapi funnel push [slug] [--funnel <id>] [--slug <path>] [--force] [--org <id>] < page.html
|
|
582
|
+
'push': `myapi funnel push [slug] [--funnel <id>] [--slug <path>] [--force] [--org <id>] [--json] < page.html
|
|
567
583
|
|
|
568
584
|
Reads HTML from stdin and publishes to <slug> on your funnel. Funnel is resolved
|
|
569
585
|
from --funnel, the default funnel, or (only if the org has exactly one) auto-picked.
|
|
@@ -573,6 +589,10 @@ exists there, push refuses and names the org + funnel + slug at risk; re-run
|
|
|
573
589
|
with --force to replace it. On success it prints the resolved org + funnel so
|
|
574
590
|
you can confirm you wrote to the namespace you intended.
|
|
575
591
|
|
|
592
|
+
With --json, prints { slug, subdomain_url, overwritten, org_id, funnel_id } —
|
|
593
|
+
the live URL is carried as subdomain_url (same field name as funnel get/list),
|
|
594
|
+
so you don't need a follow-up "funnel get" to find where the page went.
|
|
595
|
+
|
|
576
596
|
By default, your funnel is served on a preview subdomain (*.makeautonomous.com).
|
|
577
597
|
To serve on your own domain, register and assign one with:
|
|
578
598
|
myapi domain register <domain>
|
package/dist/commands/git.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
1
3
|
import { git as sdkGit } from '@myapihq/sdk';
|
|
2
4
|
import { requireConfig } from '../config.js';
|
|
3
5
|
import { success, error, printTable, info, printJson } from '../output.js';
|
|
4
|
-
import { formatDate } from '../utils.js';
|
|
5
|
-
import { requireOrg, requireArg } from '../helpers.js';
|
|
6
|
+
import { formatDate, retryFunds } from '../utils.js';
|
|
7
|
+
import { requireOrg, requireArg, confirmDestructive } from '../helpers.js';
|
|
6
8
|
export const EXPOSES = [
|
|
7
9
|
'POST /git/orgs/{org_id}/repos',
|
|
8
10
|
'GET /git/orgs/{org_id}/repos',
|
|
@@ -35,11 +37,62 @@ export const SCHEMA = {
|
|
|
35
37
|
'author-name': 'string',
|
|
36
38
|
'author-email': 'string',
|
|
37
39
|
changes: 'string',
|
|
40
|
+
dir: 'string',
|
|
41
|
+
file: 'string',
|
|
42
|
+
as: 'string',
|
|
38
43
|
target: 'string',
|
|
39
44
|
source: 'string',
|
|
40
45
|
};
|
|
41
46
|
const shortSha = (s) => (s || '').slice(0, 8);
|
|
42
47
|
const firstLine = (s) => (s || '').split('\n')[0];
|
|
48
|
+
// Build a commit's file changes from local disk (--dir / --file), falling back
|
|
49
|
+
// to an explicit --changes JSON array. This is the ergonomic path an agent
|
|
50
|
+
// actually needs: point at a directory of generated code, not hand-author JSON.
|
|
51
|
+
function collectChanges(flags) {
|
|
52
|
+
const changes = [];
|
|
53
|
+
const toChange = (abs, repoPath) => {
|
|
54
|
+
const buf = fs.readFileSync(abs);
|
|
55
|
+
// Binary files (null byte present) go base64; everything else UTF-8 text.
|
|
56
|
+
return buf.includes(0)
|
|
57
|
+
? { path: repoPath, content_base64: buf.toString('base64') }
|
|
58
|
+
: { path: repoPath, content: buf.toString('utf-8') };
|
|
59
|
+
};
|
|
60
|
+
if (typeof flags.dir === 'string' && flags.dir) {
|
|
61
|
+
const root = flags.dir;
|
|
62
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory())
|
|
63
|
+
error(`--dir: not a directory: ${root}`);
|
|
64
|
+
const walk = (d) => {
|
|
65
|
+
for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
|
|
66
|
+
if (ent.name === '.git' || ent.name === 'node_modules')
|
|
67
|
+
continue;
|
|
68
|
+
const abs = path.join(d, ent.name);
|
|
69
|
+
if (ent.isDirectory())
|
|
70
|
+
walk(abs);
|
|
71
|
+
else if (ent.isFile())
|
|
72
|
+
changes.push(toChange(abs, path.relative(root, abs).split(path.sep).join('/')));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
walk(root);
|
|
76
|
+
}
|
|
77
|
+
if (typeof flags.file === 'string' && flags.file) {
|
|
78
|
+
if (!fs.existsSync(flags.file))
|
|
79
|
+
error(`--file: not found: ${flags.file}`);
|
|
80
|
+
const repoPath = (typeof flags.as === 'string' && flags.as) ? flags.as : path.basename(flags.file);
|
|
81
|
+
changes.push(toChange(flags.file, repoPath));
|
|
82
|
+
}
|
|
83
|
+
if (typeof flags.changes === 'string' && flags.changes) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(flags.changes);
|
|
86
|
+
if (!Array.isArray(parsed))
|
|
87
|
+
throw new Error('not an array');
|
|
88
|
+
changes.push(...parsed);
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
error(`--changes is not valid JSON: ${e?.message ?? e}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return changes;
|
|
95
|
+
}
|
|
43
96
|
// ── Repos ────────────────────────────────────────────────────────────────────
|
|
44
97
|
export async function create(name, flags) {
|
|
45
98
|
const config = requireConfig();
|
|
@@ -79,10 +132,11 @@ export async function get(repo, flags) {
|
|
|
79
132
|
}
|
|
80
133
|
export async function del(repo, flags) {
|
|
81
134
|
const config = requireConfig();
|
|
82
|
-
const orgId = requireOrg(flags, config, 'myapi git delete <repo> [--org <id>]');
|
|
135
|
+
const orgId = requireOrg(flags, config, 'myapi git delete <repo> [--yes] [--org <id>]');
|
|
83
136
|
requireArg(repo, 'repo', 'myapi git delete <repo>');
|
|
137
|
+
await confirmDestructive(flags, `delete repository "${repo}" in org ${orgId}`, 'myapi git delete <repo> --yes [--org <id>]');
|
|
84
138
|
await sdkGit.deleteRepo(config.api_key, orgId, repo);
|
|
85
|
-
success(`Deleted repository ${repo}`);
|
|
139
|
+
success(`Deleted repository ${repo} (org ${orgId})`);
|
|
86
140
|
}
|
|
87
141
|
// ── Refs / history ───────────────────────────────────────────────────────────
|
|
88
142
|
export async function refs(repo, flags) {
|
|
@@ -94,12 +148,15 @@ export async function refs(repo, flags) {
|
|
|
94
148
|
printJson(r);
|
|
95
149
|
return;
|
|
96
150
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
151
|
+
// branches/tags come back null (not []) on a repo with none — guard both.
|
|
152
|
+
const branches = r.branches ?? [];
|
|
153
|
+
const tags = r.tags ?? [];
|
|
154
|
+
info(`HEAD: ${r.head ?? '(none)'}`);
|
|
155
|
+
info(`Branches (${branches.length}):`);
|
|
156
|
+
for (const b of branches)
|
|
100
157
|
info(` ${b.name} ${shortSha(b.sha)}`);
|
|
101
|
-
info(`Tags (${
|
|
102
|
-
for (const t of
|
|
158
|
+
info(`Tags (${tags.length}):`);
|
|
159
|
+
for (const t of tags)
|
|
103
160
|
info(` ${t.name} ${shortSha(t.sha)}`);
|
|
104
161
|
}
|
|
105
162
|
export async function log(repo, flags) {
|
|
@@ -188,37 +245,45 @@ export async function diff(repo, flags) {
|
|
|
188
245
|
// ── Writes ───────────────────────────────────────────────────────────────────
|
|
189
246
|
export async function commit(repo, flags) {
|
|
190
247
|
const config = requireConfig();
|
|
191
|
-
const
|
|
192
|
-
|
|
248
|
+
const usage = 'myapi git commit <repo> --branch <b> --message <m> (--dir <path> | --file <path> [--as <repo-path>] | --changes <json>)';
|
|
249
|
+
const orgId = requireOrg(flags, config, usage);
|
|
250
|
+
requireArg(repo, 'repo', usage);
|
|
193
251
|
const branch = flags.branch;
|
|
194
252
|
if (!branch)
|
|
195
|
-
error(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
let changes;
|
|
200
|
-
try {
|
|
201
|
-
changes = JSON.parse(flags.changes);
|
|
202
|
-
if (!Array.isArray(changes))
|
|
203
|
-
throw new Error('not an array');
|
|
204
|
-
}
|
|
205
|
-
catch (e) {
|
|
206
|
-
error(`--changes is not valid JSON: ${e?.message ?? e}`);
|
|
253
|
+
error(`Missing --branch.\nUsage: ${usage}`);
|
|
254
|
+
const changes = collectChanges(flags);
|
|
255
|
+
if (changes.length === 0) {
|
|
256
|
+
error(`Nothing to commit. Provide one of:\n --dir <path> commit every file under a local directory\n --file <path> commit a single local file (--as <repo-path> to rename)\n --changes <json> explicit edits: [{"path":"README.md","content":"# Hello"}]`);
|
|
207
257
|
}
|
|
208
258
|
const payload = {
|
|
209
|
-
branch,
|
|
259
|
+
branch: branch,
|
|
210
260
|
message: flags.message || '',
|
|
211
|
-
changes
|
|
261
|
+
changes,
|
|
212
262
|
};
|
|
213
|
-
|
|
263
|
+
// Base resolution (optimistic concurrency). Explicit --base wins. Otherwise
|
|
264
|
+
// auto-resolve so the command "just works": if the branch exists, target its
|
|
265
|
+
// current tip; if it doesn't, send "" (create). Without this an agent would
|
|
266
|
+
// have to fetch the tip SHA itself, and a second commit fails REF_CONFLICT.
|
|
267
|
+
if (typeof flags.base === 'string') {
|
|
214
268
|
payload.base = flags.base;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
// A failed ref read must abort — swallowing it would be indistinguishable
|
|
272
|
+
// from "branch doesn't exist" and silently switch the commit to
|
|
273
|
+
// create-semantics (base "") against an existing branch.
|
|
274
|
+
const refs = await sdkGit.listRefs(config.api_key, orgId, repo).catch((e) => {
|
|
275
|
+
error(`Could not read refs for "${repo}" (${e instanceof Error ? e.message : String(e)}).\nRefusing to guess the commit base — retry, or pass --base <sha> explicitly.`);
|
|
276
|
+
});
|
|
277
|
+
const tip = refs.branches?.find(b => b.name === branch)?.sha;
|
|
278
|
+
payload.base = tip ?? '';
|
|
279
|
+
}
|
|
215
280
|
if (flags['author-name'] || flags['author-email']) {
|
|
216
281
|
payload.author = {
|
|
217
282
|
name: flags['author-name'] || '',
|
|
218
283
|
email: flags['author-email'] || '',
|
|
219
284
|
};
|
|
220
285
|
}
|
|
221
|
-
const res = await sdkGit.commit(config.api_key, orgId, repo, payload);
|
|
286
|
+
const res = await retryFunds(() => sdkGit.commit(config.api_key, orgId, repo, payload));
|
|
222
287
|
if (flags.json) {
|
|
223
288
|
printJson(res);
|
|
224
289
|
return;
|
|
@@ -291,15 +356,23 @@ const SUBCOMMAND_USAGE = {
|
|
|
291
356
|
'tree': 'myapi git tree <repo> <ref> [--path <p>] [--org <id>] [--json]',
|
|
292
357
|
'blob': 'myapi git blob <repo> <ref> <path> [--org <id>]\n\nPrints the file content (decoded). --json gives {path, size, content_base64}.',
|
|
293
358
|
'diff': 'myapi git diff <repo> --base <ref> --head <ref> [--org <id>]',
|
|
294
|
-
'commit': `myapi git commit <repo> --branch <b> --message <m> --changes <json> [--base <ref>] [--author-name <n>] [--author-email <e>] [--org <id>]
|
|
359
|
+
'commit': `myapi git commit <repo> --branch <b> --message <m> (--dir <path> | --file <path> [--as <repo-path>] | --changes <json>) [--base <ref>] [--author-name <n>] [--author-email <e>] [--org <id>]
|
|
360
|
+
|
|
361
|
+
Source the files to commit from local disk or explicit JSON (combinable):
|
|
362
|
+
--dir <path> commit every file under a local directory (recursive;
|
|
363
|
+
skips .git/ and node_modules/; binary files auto-base64)
|
|
364
|
+
--file <path> commit a single local file (--as <repo-path> to rename it
|
|
365
|
+
in the repo; defaults to the file's basename)
|
|
366
|
+
--changes <json> explicit edits — array of
|
|
367
|
+
{path, content | content_base64 | delete:true, mode?}
|
|
295
368
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
myapi git commit
|
|
299
|
-
|
|
369
|
+
Examples:
|
|
370
|
+
myapi git commit site --branch main --message "init" --dir ./public
|
|
371
|
+
myapi git commit site --branch main --message "add page" --file ./out.html --as index.html
|
|
372
|
+
myapi git commit site --branch main --message "rm" --changes '[{"path":"old.txt","delete":true}]'
|
|
300
373
|
|
|
301
|
-
--base is the expected current branch tip (optimistic concurrency);
|
|
302
|
-
|
|
374
|
+
--base is the expected current branch tip (optimistic concurrency); omit it to
|
|
375
|
+
create the branch or write onto its current tip, or pass it to guard a stale write.`,
|
|
303
376
|
'create-branch': 'myapi git create-branch <repo> <name> --from <ref> [--org <id>]',
|
|
304
377
|
'delete-branch': 'myapi git delete-branch <repo> <branch> [--org <id>]',
|
|
305
378
|
'tag': 'myapi git tag <repo> <name> --ref <ref> [--org <id>]',
|
|
@@ -327,7 +400,7 @@ History & content:
|
|
|
327
400
|
refs <repo> List branches and tags
|
|
328
401
|
|
|
329
402
|
Writes:
|
|
330
|
-
commit <repo> Commit
|
|
403
|
+
commit <repo> Commit files (--branch, --message, --dir | --file | --changes)
|
|
331
404
|
create-branch <repo> <name> Create a branch (--from <ref>)
|
|
332
405
|
delete-branch <repo> <branch> Delete a branch
|
|
333
406
|
tag <repo> <name> Create a tag (--ref <ref>)
|