@myapihq/cli 1.2.6 → 1.2.8
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/billing.d.ts +1 -0
- package/dist/commands/billing.js +33 -0
- package/dist/commands/container-validation.test.d.ts +1 -0
- package/dist/commands/container-validation.test.js +45 -0
- package/dist/commands/container.d.ts +16 -0
- package/dist/commands/container.js +270 -0
- package/dist/commands/email/campaign.js +16 -1
- package/dist/commands/email/message.js +26 -3
- package/dist/commands/email/template.js +8 -1
- package/dist/commands/pixel.js +15 -2
- package/dist/commands/update.d.ts +1 -1
- package/dist/commands/update.js +68 -45
- package/dist/commands/webhook.js +10 -1
- package/dist/completion.d.ts +3 -1
- package/dist/completion.js +160 -55
- package/dist/exposes.test.js +1 -0
- package/dist/index.js +26 -12
- package/dist/sdk-billing-usage.test.d.ts +1 -0
- package/dist/sdk-billing-usage.test.js +74 -0
- package/dist/sdk-container.test.d.ts +1 -0
- package/dist/sdk-container.test.js +139 -0
- package/package.json +2 -4
package/dist/commands/update.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { loadConfig, CONFIG_DIR } from '../config.js';
|
|
4
5
|
import { info, success, banner } from '../output.js';
|
|
5
6
|
import { installSkills } from './setup.js';
|
|
6
7
|
// `myapi update` only talks to the npm registry to check for new CLI versions
|
|
@@ -8,6 +9,33 @@ import { installSkills } from './setup.js';
|
|
|
8
9
|
export const EXPOSES = [];
|
|
9
10
|
const REGISTRY_URL = 'https://registry.npmjs.org/@myapihq/cli/latest';
|
|
10
11
|
const PACKUMENT_URL = 'https://registry.npmjs.org/@myapihq/cli';
|
|
12
|
+
// The update check hits the npm registry. Caching its result means at most
|
|
13
|
+
// one network round-trip per TTL window instead of one on every command —
|
|
14
|
+
// the dominant cost in CLI startup latency.
|
|
15
|
+
const UPDATE_CACHE_FILE = join(CONFIG_DIR, 'update-check.json');
|
|
16
|
+
const CHECK_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
17
|
+
function readUpdateCache() {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(UPDATE_CACHE_FILE, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function writeUpdateCache(cache) {
|
|
26
|
+
try {
|
|
27
|
+
if (!existsSync(CONFIG_DIR))
|
|
28
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
writeFileSync(UPDATE_CACHE_FILE, JSON.stringify(cache));
|
|
30
|
+
}
|
|
31
|
+
catch { /* best-effort — a write failure just means we re-check sooner */ }
|
|
32
|
+
}
|
|
33
|
+
// The last-known published version, read from cache with no network call.
|
|
34
|
+
// `myapi --version` uses this so it can flag an available update without the
|
|
35
|
+
// ~1s registry round-trip blocking the output.
|
|
36
|
+
export function cachedLatestVersion() {
|
|
37
|
+
return readUpdateCache()?.latest ?? null;
|
|
38
|
+
}
|
|
11
39
|
// Verify the version is listed in the full packument — the same data source
|
|
12
40
|
// `npm install` consults. The /latest endpoint and the tarball URL are on
|
|
13
41
|
// different CDN caches and can update before the packument does, causing
|
|
@@ -48,42 +76,48 @@ function npmInstallArgs(version) {
|
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
// checkForUpdate runs silently in the background on every command.
|
|
51
|
-
// Auto-installs if a newer version is available.
|
|
79
|
+
// Auto-installs if a newer version is available. The registry is consulted
|
|
80
|
+
// at most once per CHECK_TTL_MS; runs within that window reuse the cache.
|
|
52
81
|
// Suppress entirely with MYAPI_NO_UPDATE=1 or when stdout is not a TTY.
|
|
53
82
|
export async function checkForUpdate(currentVersion) {
|
|
54
83
|
if (process.env.MYAPI_NO_UPDATE === '1' || !process.stdout.isTTY)
|
|
55
84
|
return;
|
|
85
|
+
const cache = readUpdateCache();
|
|
86
|
+
let latest = cache?.latest;
|
|
87
|
+
if (!cache || Date.now() - cache.checkedAt >= CHECK_TTL_MS) {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(3000) });
|
|
90
|
+
if (res.ok)
|
|
91
|
+
latest = (await res.json()).version ?? latest;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Network errors are silently ignored — keep the last known `latest`.
|
|
95
|
+
}
|
|
96
|
+
// Record the attempt regardless of outcome so a slow or unreachable
|
|
97
|
+
// registry isn't re-hit on the very next command.
|
|
98
|
+
writeUpdateCache({ checkedAt: Date.now(), latest });
|
|
99
|
+
}
|
|
100
|
+
if (!latest || !isNewer(latest, currentVersion))
|
|
101
|
+
return;
|
|
102
|
+
// Verify the version is actually installable before attempting install —
|
|
103
|
+
// the /latest endpoint can report a new version before the packument
|
|
104
|
+
// (which `npm install` consults) has propagated through the CDN.
|
|
105
|
+
if (!(await isVersionInstallable(latest)))
|
|
106
|
+
return;
|
|
107
|
+
banner(`\n› New version available (${currentVersion} → ${latest}) — installing…`);
|
|
56
108
|
try {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (latest && isNewer(latest, currentVersion)) {
|
|
63
|
-
// Verify the version is actually installable before attempting install —
|
|
64
|
-
// the /latest endpoint can report a new version before the packument
|
|
65
|
-
// (which `npm install` consults) has propagated through the CDN.
|
|
66
|
-
if (!(await isVersionInstallable(latest)))
|
|
67
|
-
return;
|
|
68
|
-
banner(`\n› New version available (${currentVersion} → ${latest}) — installing…`);
|
|
69
|
-
try {
|
|
70
|
-
const { bin, args } = npmInstallArgs(latest);
|
|
71
|
-
execSync(`"${bin}" ${args.map(a => `"${a}"`).join(' ')}`, { stdio: 'pipe' });
|
|
72
|
-
const config = loadConfig();
|
|
73
|
-
if (config?.skills_installed) {
|
|
74
|
-
await installSkills();
|
|
75
|
-
}
|
|
76
|
-
banner(`› Updated to ${latest} — active after this command completes.\n`);
|
|
77
|
-
}
|
|
78
|
-
catch (installErr) {
|
|
79
|
-
const msg = installErr?.stderr?.toString?.() || installErr?.message || String(installErr);
|
|
80
|
-
banner(`› Auto-update failed: ${msg.trim()}`);
|
|
81
|
-
banner(`› Run manually: npm install -g @myapihq/cli@latest`);
|
|
82
|
-
}
|
|
109
|
+
const { bin, args } = npmInstallArgs(latest);
|
|
110
|
+
execSync(`"${bin}" ${args.map(a => `"${a}"`).join(' ')}`, { stdio: 'pipe' });
|
|
111
|
+
const config = loadConfig();
|
|
112
|
+
if (config?.skills_installed) {
|
|
113
|
+
await installSkills();
|
|
83
114
|
}
|
|
115
|
+
banner(`› Updated to ${latest} — active after this command completes.\n`);
|
|
84
116
|
}
|
|
85
|
-
catch {
|
|
86
|
-
|
|
117
|
+
catch (installErr) {
|
|
118
|
+
const msg = installErr?.stderr?.toString?.() || installErr?.message || String(installErr);
|
|
119
|
+
banner(`› Auto-update failed: ${msg.trim()}`);
|
|
120
|
+
banner(`› Run manually: npm install -g @myapihq/cli@latest`);
|
|
87
121
|
}
|
|
88
122
|
}
|
|
89
123
|
// myapi update — explicit update, same logic as auto-update.
|
|
@@ -100,8 +134,10 @@ export async function update(flags = {}) {
|
|
|
100
134
|
throw new Error(`registry returned ${res.status}`);
|
|
101
135
|
const data = await res.json();
|
|
102
136
|
latest = data.version ?? '';
|
|
103
|
-
if (latest)
|
|
137
|
+
if (latest) {
|
|
138
|
+
writeUpdateCache({ checkedAt: Date.now(), latest });
|
|
104
139
|
info(`› Installing @myapihq/cli@${latest}…`);
|
|
140
|
+
}
|
|
105
141
|
}
|
|
106
142
|
catch { /* proceed anyway */ }
|
|
107
143
|
try {
|
|
@@ -116,19 +152,6 @@ export async function update(flags = {}) {
|
|
|
116
152
|
await installSkills();
|
|
117
153
|
success('Up to date.');
|
|
118
154
|
}
|
|
119
|
-
// latestVersion fetches the current published version for --version display.
|
|
120
|
-
export async function latestVersion() {
|
|
121
|
-
try {
|
|
122
|
-
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(3000) });
|
|
123
|
-
if (!res.ok)
|
|
124
|
-
return null;
|
|
125
|
-
const data = await res.json();
|
|
126
|
-
return data.version ?? null;
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
155
|
// Stable-only semver comparison (1.2.3). Prerelease versions (1.2.3-wip.0)
|
|
133
156
|
// produce NaN in the patch slot, and `n > NaN` is always false — which is
|
|
134
157
|
// what we want: a prerelease user should not be auto-rolled back to stable.
|
package/dist/commands/webhook.js
CHANGED
|
@@ -25,7 +25,16 @@ export async function list(flags) {
|
|
|
25
25
|
printJson(endpoints);
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
// Curated columns: org_id is the query scope (identical every row), and
|
|
29
|
+
// url is mechanically derivable from slug — both omitted here to keep the
|
|
30
|
+
// table token-dense. `myapi webhook get <id>` shows the full record.
|
|
31
|
+
printTable(endpoints.map(e => ({
|
|
32
|
+
id: e.id,
|
|
33
|
+
name: e.name,
|
|
34
|
+
slug: e.slug,
|
|
35
|
+
crm_email_path: e.crm_email_path ?? '',
|
|
36
|
+
created_at: e.created_at,
|
|
37
|
+
})), {
|
|
29
38
|
flags,
|
|
30
39
|
empty: 'No webhook endpoints yet. Create one with: myapi webhook create --name <name>',
|
|
31
40
|
});
|
package/dist/completion.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare const COMMANDS: string[];
|
|
2
|
+
export declare const SUBCOMMANDS: Record<string, string[]>;
|
|
3
|
+
export declare function handleCompletionRequest(): void;
|
|
2
4
|
export declare function installCompletion(): void;
|
|
3
5
|
export declare function uninstallCompletion(): void;
|
package/dist/completion.js
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
|
-
// Shell
|
|
2
|
-
// the shell invokes us with the magic completion env vars, omelette's
|
|
3
|
-
// init() handles the request and exits before we hit normal command
|
|
4
|
-
// dispatch. On any other run, init() returns immediately.
|
|
1
|
+
// Shell tab-completion — hand-rolled, zero third-party dependencies.
|
|
5
2
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// This previously used the `omelette` package. It was removed for
|
|
4
|
+
// supply-chain safety: `myapi` is installed globally and reads the user's
|
|
5
|
+
// live API key from ~/.myapi/config.json on every run, so an unmaintained
|
|
6
|
+
// dependency on this path is an unacceptable compromise vector — one bad
|
|
7
|
+
// release would exfiltrate every user's key. This module reproduces only
|
|
8
|
+
// what we used (completion-request handling + shell init-file wiring) as
|
|
9
|
+
// audited first-party code.
|
|
9
10
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
// Protocol: the shell init snippet generated below calls back
|
|
12
|
+
// myapi --comp{bash,zsh,fish} --compgen <cword> <prev> <line>
|
|
13
|
+
// We parse <line>, compute the candidate list, print it newline-separated,
|
|
14
|
+
// and exit. The shell itself narrows the list to the partial word.
|
|
15
|
+
//
|
|
16
|
+
// This module is imported lazily (see index.ts) — only when argv carries a
|
|
17
|
+
// --comp* marker — so it never loads on a normal command run.
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
const PROGRAM = 'myapi';
|
|
22
|
+
const COMPLETION_DIR = path.join(os.homedir(), '.myapi');
|
|
23
|
+
const BLOCK_BEGIN = `# begin ${PROGRAM} completion`;
|
|
24
|
+
const BLOCK_END = `# end ${PROGRAM} completion`;
|
|
25
|
+
// Top-level commands — what `myapi <TAB>` offers. Kept honest by
|
|
26
|
+
// test/smoke/completion.test.ts, which fails if a command in `myapi --help`
|
|
27
|
+
// is missing here.
|
|
28
|
+
export const COMMANDS = [
|
|
29
|
+
'audience', 'auth', 'billing', 'company', 'completion', 'config', 'container',
|
|
30
|
+
'crm', 'database', 'domain', 'email', 'fn', 'funnel', 'help', 'image',
|
|
31
|
+
'install-skills', 'keys', 'llm', 'org', 'payments', 'people', 'pixel',
|
|
32
|
+
'setup', 'status', 'storage', 'update', 'url', 'webhook', 'whoami',
|
|
33
|
+
'workflow',
|
|
34
|
+
];
|
|
35
|
+
// command → subcommands, for `myapi <command> <TAB>`. Mirrors each
|
|
36
|
+
// command's dispatcher; commands absent here take no subcommand.
|
|
37
|
+
export const SUBCOMMANDS = {
|
|
38
|
+
auth: ['setup', 'import-key', 'whoami', 'link', 'switch', 'install-skills', 'config', 'registrant', 'api-keys'],
|
|
17
39
|
org: ['create', 'delete', 'get', 'import', 'list', 'sync-brand', 'update'],
|
|
18
|
-
billing: ['balance', 'history', 'setup', 'topup'],
|
|
19
|
-
domain: ['assign', 'check', '
|
|
20
|
-
funnel: ['create', 'delete', 'get', 'list', 'pages', 'push', 'verify'],
|
|
21
|
-
webhook: ['create', 'delete', 'delivery', 'list'],
|
|
40
|
+
billing: ['balance', 'history', 'usage', 'setup', 'topup', 'spend-cap'],
|
|
41
|
+
domain: ['assign', 'check', 'email-setup', 'import', 'list', 'records', 'register', 'renew', 'retry-provisioning', 'settings', 'status'],
|
|
42
|
+
funnel: ['create', 'delete', 'get', 'list', 'pages', 'publish', 'push', 'verify'],
|
|
43
|
+
webhook: ['create', 'delete', 'delivery', 'list', 'update'],
|
|
22
44
|
workflow: ['create', 'delete', 'disable', 'enable', 'get', 'get-run', 'list', 'runs', 'update'],
|
|
23
45
|
email: ['mailbox', 'message', 'warmup', 'template', 'campaign', 'verify'],
|
|
24
46
|
image: ['delete', 'generate', 'get', 'get-url', 'list', 'models'],
|
|
@@ -30,49 +52,132 @@ const TREE = {
|
|
|
30
52
|
llm: ['complete', 'embed', 'models'],
|
|
31
53
|
database: ['namespaces', 'create', 'delete-namespace', 'keys', 'get', 'set', 'del'],
|
|
32
54
|
crm: ['contacts', 'companies'],
|
|
33
|
-
contacts: ['list', 'search', 'create', 'get', 'update', 'delete', 'restore', 'promote', 'events'],
|
|
34
|
-
companies: ['list', 'search', 'create', 'get', 'update', 'delete', 'restore', 'promote'],
|
|
35
55
|
url: ['shorten'],
|
|
36
|
-
keys: ['create', 'list', 'revoke'],
|
|
37
|
-
'api-keys': ['create', 'list', 'revoke'],
|
|
56
|
+
keys: ['create', 'list', 'revoke', 'revoke-all'],
|
|
38
57
|
config: ['view', 'set-org', 'set-funnel', 'set-domain'],
|
|
39
|
-
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
status: [],
|
|
43
|
-
'install-skills': [],
|
|
44
|
-
update: [],
|
|
58
|
+
fn: ['create', 'deploy', 'env', 'runs', 'list', 'get', 'delete'],
|
|
59
|
+
payments: ['connect', 'status', 'charge', 'list', 'get', 'refund'],
|
|
60
|
+
container: ['create', 'deploy', 'list', 'get', 'logs', 'delete'],
|
|
45
61
|
completion: ['install', 'uninstall'],
|
|
46
|
-
help: [],
|
|
47
62
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
// ── Completion request handling ──────────────────────────────────────────────
|
|
64
|
+
// Entry point for a shell-initiated completion request. index.ts calls this
|
|
65
|
+
// (via a lazy import) whenever argv carries a --comp* marker. Always exits.
|
|
66
|
+
export function handleCompletionRequest() {
|
|
67
|
+
const argv = process.argv;
|
|
68
|
+
// `myapi --completion[-fish]` — emit the shell init script (zsh/fish
|
|
69
|
+
// source it live; bash sources the copy written by installCompletion).
|
|
70
|
+
if (argv.includes('--completion')) {
|
|
71
|
+
process.stdout.write(shellScript());
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
if (argv.includes('--completion-fish')) {
|
|
75
|
+
process.stdout.write(fishScript());
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
78
|
+
// `myapi --comp{bash,zsh} --compgen <cword> <prev> <line...>`
|
|
79
|
+
const gen = argv.indexOf('--compgen');
|
|
80
|
+
if (gen < 0)
|
|
81
|
+
process.exit(0);
|
|
82
|
+
// zsh's cursor index is one higher than bash's for the same position.
|
|
83
|
+
const isZsh = argv.includes('--compzsh');
|
|
84
|
+
const cword = (parseInt(argv[gen + 1], 10) || 0) - (isZsh ? 1 : 0);
|
|
85
|
+
const line = argv.slice(gen + 3).join(' ');
|
|
86
|
+
const words = line.trim().split(/\s+/);
|
|
87
|
+
let candidates = [];
|
|
88
|
+
if (cword <= 1)
|
|
89
|
+
candidates = COMMANDS;
|
|
90
|
+
else if (cword === 2)
|
|
91
|
+
candidates = SUBCOMMANDS[words[1]] ?? [];
|
|
92
|
+
// Print the full list; the shell narrows it to the partial word.
|
|
93
|
+
process.stdout.write(candidates.join(os.EOL) + os.EOL);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
// ── Shell init scripts ───────────────────────────────────────────────────────
|
|
97
|
+
// bash + zsh completion function. The `if compdef / elif complete` guard
|
|
98
|
+
// picks the right registration at source time, so one script serves both.
|
|
99
|
+
function shellScript() {
|
|
100
|
+
return `### ${PROGRAM} completion ###
|
|
101
|
+
if type compdef &>/dev/null; then
|
|
102
|
+
_${PROGRAM}_completion() {
|
|
103
|
+
compadd -- \`${PROGRAM} --compzsh --compgen "$CURRENT" "\${words[CURRENT-1]}" "$BUFFER"\`
|
|
104
|
+
}
|
|
105
|
+
compdef _${PROGRAM}_completion ${PROGRAM}
|
|
106
|
+
elif type complete &>/dev/null; then
|
|
107
|
+
_${PROGRAM}_completion() {
|
|
108
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
109
|
+
COMPREPLY=( $(compgen -W '$(${PROGRAM} --compbash --compgen "$COMP_CWORD" "$prev" "$COMP_LINE")' -- "$cur") )
|
|
110
|
+
}
|
|
111
|
+
complete -F _${PROGRAM}_completion ${PROGRAM}
|
|
112
|
+
fi
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
function fishScript() {
|
|
116
|
+
return `### ${PROGRAM} completion ###
|
|
117
|
+
function _${PROGRAM}_completion
|
|
118
|
+
${PROGRAM} --compfish --compgen (count (commandline -poc)) (commandline -pt) (commandline -pb)
|
|
119
|
+
end
|
|
120
|
+
complete -f -c ${PROGRAM} -a '(_${PROGRAM}_completion)'
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
// ── Install / uninstall ──────────────────────────────────────────────────────
|
|
124
|
+
function activeShell() {
|
|
125
|
+
const s = process.env.SHELL ?? '';
|
|
126
|
+
if (s.includes('zsh'))
|
|
127
|
+
return 'zsh';
|
|
128
|
+
if (s.includes('fish'))
|
|
129
|
+
return 'fish';
|
|
130
|
+
if (s.includes('bash'))
|
|
131
|
+
return 'bash';
|
|
132
|
+
throw new Error(`Could not detect a supported shell (SHELL="${s}"). Supported: bash, zsh, fish.`);
|
|
133
|
+
}
|
|
134
|
+
function initFile(shell) {
|
|
135
|
+
const home = os.homedir();
|
|
136
|
+
if (shell === 'zsh')
|
|
137
|
+
return path.join(home, '.zshrc');
|
|
138
|
+
if (shell === 'fish')
|
|
139
|
+
return path.join(home, '.config', 'fish', 'config.fish');
|
|
140
|
+
return path.join(home, process.platform === 'darwin' ? '.bash_profile' : '.bashrc');
|
|
141
|
+
}
|
|
142
|
+
// The source line appended to the shell's rc file.
|
|
143
|
+
function initBlock(shell) {
|
|
144
|
+
let cmd;
|
|
145
|
+
if (shell === 'bash')
|
|
146
|
+
cmd = `source "${path.join(COMPLETION_DIR, 'completion.sh')}"`;
|
|
147
|
+
else if (shell === 'zsh')
|
|
148
|
+
cmd = `. <(${PROGRAM} --completion)`;
|
|
149
|
+
else
|
|
150
|
+
cmd = `${PROGRAM} --completion-fish | source`;
|
|
151
|
+
return `\n${BLOCK_BEGIN}\n${cmd}\n${BLOCK_END}\n`;
|
|
152
|
+
}
|
|
153
|
+
function escapeRe(s) {
|
|
154
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
70
155
|
}
|
|
71
|
-
// Wire/unwire the shell rc snippet. Omelette writes a single eval line
|
|
72
|
-
// into ~/.bashrc / ~/.zshrc / ~/.config/fish/config.fish.
|
|
73
156
|
export function installCompletion() {
|
|
74
|
-
|
|
157
|
+
const shell = activeShell();
|
|
158
|
+
// bash can't reliably source a process substitution from .bashrc, so the
|
|
159
|
+
// script is written to a file the rc line sources.
|
|
160
|
+
if (shell === 'bash') {
|
|
161
|
+
fs.mkdirSync(COMPLETION_DIR, { recursive: true });
|
|
162
|
+
fs.writeFileSync(path.join(COMPLETION_DIR, 'completion.sh'), shellScript());
|
|
163
|
+
}
|
|
164
|
+
const file = initFile(shell);
|
|
165
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
166
|
+
if (existing.includes(BLOCK_BEGIN))
|
|
167
|
+
return; // idempotent — already installed
|
|
168
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
169
|
+
fs.appendFileSync(file, initBlock(shell));
|
|
75
170
|
}
|
|
76
171
|
export function uninstallCompletion() {
|
|
77
|
-
|
|
172
|
+
const shell = activeShell();
|
|
173
|
+
const file = initFile(shell);
|
|
174
|
+
if (fs.existsSync(file)) {
|
|
175
|
+
const cleaned = fs.readFileSync(file, 'utf8').replace(new RegExp(`\\n?${escapeRe(BLOCK_BEGIN)}[\\s\\S]*?${escapeRe(BLOCK_END)}\\n?`, 'g'), '');
|
|
176
|
+
fs.writeFileSync(file, cleaned);
|
|
177
|
+
}
|
|
178
|
+
if (shell === 'bash') {
|
|
179
|
+
const sh = path.join(COMPLETION_DIR, 'completion.sh');
|
|
180
|
+
if (fs.existsSync(sh))
|
|
181
|
+
fs.unlinkSync(sh);
|
|
182
|
+
}
|
|
78
183
|
}
|
package/dist/exposes.test.js
CHANGED
|
@@ -39,6 +39,7 @@ const COMMAND_MODULES = [
|
|
|
39
39
|
'./commands/workflow.js',
|
|
40
40
|
'./commands/fn.js',
|
|
41
41
|
'./commands/payments.js',
|
|
42
|
+
'./commands/container.js',
|
|
42
43
|
];
|
|
43
44
|
const ENDPOINT_PATTERN = /^(GET|POST|PATCH|PUT|DELETE) \/[A-Za-z0-9_\-./{}]*$/;
|
|
44
45
|
describe('every CLI command exports a typed EXPOSES array (S-101)', () => {
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import * as databaseCmd from './commands/database.js';
|
|
|
31
31
|
import * as crmCmd from './commands/crm/index.js';
|
|
32
32
|
import * as fnCmd from './commands/fn.js';
|
|
33
33
|
import * as paymentsCmd from './commands/payments.js';
|
|
34
|
-
import
|
|
34
|
+
import * as containerCmd from './commands/container.js';
|
|
35
35
|
// Each command file declares the value flags it understands. We union them
|
|
36
36
|
// into a single schema for the upfront parse, so adding a new value flag in
|
|
37
37
|
// one command means editing one file (its SCHEMA), not a global allowlist.
|
|
@@ -59,6 +59,7 @@ const COMBINED_SCHEMA = {
|
|
|
59
59
|
...workflowCmd.SCHEMA,
|
|
60
60
|
...fnCmd.SCHEMA,
|
|
61
61
|
...paymentsCmd.SCHEMA,
|
|
62
|
+
...containerCmd.SCHEMA,
|
|
62
63
|
// Top-level flags
|
|
63
64
|
version: 'boolean',
|
|
64
65
|
V: 'boolean',
|
|
@@ -114,12 +115,20 @@ function friendlyError(err) {
|
|
|
114
115
|
return base;
|
|
115
116
|
}
|
|
116
117
|
async function main() {
|
|
117
|
-
// Shell
|
|
118
|
-
//
|
|
119
|
-
|
|
118
|
+
// Shell tab-completion: when the shell invokes us for completion it passes
|
|
119
|
+
// a --comp* marker. Handle it via a lazily-imported module so completion
|
|
120
|
+
// code (and its file I/O) never loads on a normal command run.
|
|
121
|
+
if (process.argv.some(a => a === '--compgen' || a === '--completion' || a === '--completion-fish')) {
|
|
122
|
+
const { handleCompletionRequest } = await import('./completion.js');
|
|
123
|
+
handleCompletionRequest(); // computes candidates / emits the script, then exits
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
120
126
|
const { args, flags } = parseFlags(process.argv.slice(2), COMBINED_SCHEMA);
|
|
121
127
|
if (flags.version || flags.v || flags.V) {
|
|
122
|
-
|
|
128
|
+
// Read the last-known published version from cache — no network call, so
|
|
129
|
+
// `myapi --version` stays instant. The cache is refreshed by the
|
|
130
|
+
// background update check on regular commands.
|
|
131
|
+
const latest = updateCmd.cachedLatestVersion();
|
|
123
132
|
const updateNote = latest && updateCmd.isNewer(latest, pkg.version)
|
|
124
133
|
? ` (update available: ${latest})`
|
|
125
134
|
: '';
|
|
@@ -207,6 +216,9 @@ async function main() {
|
|
|
207
216
|
case 'payments':
|
|
208
217
|
await paymentsCmd.run(subcommand, restArgs, flags);
|
|
209
218
|
break;
|
|
219
|
+
case 'container':
|
|
220
|
+
await containerCmd.run(subcommand, restArgs, flags);
|
|
221
|
+
break;
|
|
210
222
|
// Convenience aliases
|
|
211
223
|
case 'setup':
|
|
212
224
|
await setupCmd.setup(flags);
|
|
@@ -231,20 +243,20 @@ async function main() {
|
|
|
231
243
|
await setupCmd.installSkills();
|
|
232
244
|
success('› Skills installed.');
|
|
233
245
|
break;
|
|
234
|
-
case 'completion':
|
|
235
|
-
|
|
236
|
-
// unconditionally, so anything we want to show has to print first.
|
|
246
|
+
case 'completion': {
|
|
247
|
+
const { installCompletion, uninstallCompletion } = await import('./completion.js');
|
|
237
248
|
if (subcommand === 'uninstall') {
|
|
238
|
-
success('› Removing completion from shell init file…');
|
|
239
|
-
info(' Restart your shell (or `source ~/.bashrc` / `~/.zshrc`) to take effect.');
|
|
240
249
|
uninstallCompletion();
|
|
250
|
+
success('› Completion removed from your shell init file.');
|
|
251
|
+
info(' Restart your shell (or re-source your rc file) for it to take effect.');
|
|
241
252
|
}
|
|
242
253
|
else {
|
|
243
|
-
success('› Installing tab completion…');
|
|
244
|
-
info(' Restart your shell (or `source ~/.bashrc` / `~/.zshrc`) to enable.');
|
|
245
254
|
installCompletion();
|
|
255
|
+
success('› Tab completion installed.');
|
|
256
|
+
info(' Restart your shell (or re-source your rc file) to enable it.');
|
|
246
257
|
}
|
|
247
258
|
break;
|
|
259
|
+
}
|
|
248
260
|
case 'help':
|
|
249
261
|
await dispatchHelp(subcommand);
|
|
250
262
|
break;
|
|
@@ -329,6 +341,7 @@ const HELP_TARGETS = {
|
|
|
329
341
|
crm: f => crmCmd.run(undefined, [], f),
|
|
330
342
|
fn: f => fnCmd.run(undefined, [], f),
|
|
331
343
|
payments: f => paymentsCmd.run(undefined, [], f),
|
|
344
|
+
container: f => containerCmd.run(undefined, [], f),
|
|
332
345
|
org: f => orgCmd.run(undefined, [], f),
|
|
333
346
|
billing: f => billingCmd.run(undefined, [], f),
|
|
334
347
|
keys: f => keysCmd.run(undefined, [], f),
|
|
@@ -371,6 +384,7 @@ Commands:
|
|
|
371
384
|
domain Manage domain configurations
|
|
372
385
|
funnel Manage websites (publish pages, custom domains, funnels)
|
|
373
386
|
fn Create and deploy functions on the edge runtime
|
|
387
|
+
container Run containers — services, workers, and scheduled jobs
|
|
374
388
|
payments Take payments with Stripe Checkout (connect, charge, refund)
|
|
375
389
|
webhook Manage inbound webhook endpoints and inspect deliveries
|
|
376
390
|
email Manage mailboxes, send/read email, templates, and campaigns
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SDK-level unit tests for hq.getBillingUsage — the spend-by-service
|
|
2
|
+
// rollup backed by the backend's detailed metering
|
|
3
|
+
// (GET /hq/billing/usage). Mocks global fetch — does NOT hit the network.
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
5
|
+
import { hq } from '@myapihq/sdk';
|
|
6
|
+
const API_KEY = 'myapi_test_abc';
|
|
7
|
+
let fetchMock;
|
|
8
|
+
function ok(data, status = 200) {
|
|
9
|
+
return new Response(JSON.stringify({ success: true, data, meta: {} }), {
|
|
10
|
+
status,
|
|
11
|
+
headers: { 'content-type': 'application/json' },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function fail(code, message, status = 400) {
|
|
15
|
+
return new Response(JSON.stringify({ success: false, error: { code, message }, meta: {} }), {
|
|
16
|
+
status,
|
|
17
|
+
headers: { 'content-type': 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const SAMPLE = {
|
|
21
|
+
period: 'month',
|
|
22
|
+
since: '2026-05-01T00:00:00Z',
|
|
23
|
+
services: [
|
|
24
|
+
{ service: 'llm', requests: 1240, cost_display: '$3.10' },
|
|
25
|
+
{ service: 'image', requests: 22, cost_display: '$0.44' },
|
|
26
|
+
],
|
|
27
|
+
total_display: '$3.54',
|
|
28
|
+
};
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
fetchMock = vi.fn();
|
|
31
|
+
globalThis.fetch = fetchMock;
|
|
32
|
+
});
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
describe('hq.getBillingUsage', () => {
|
|
37
|
+
it('GETs /hq/billing/usage with bearer auth and no query by default', async () => {
|
|
38
|
+
fetchMock.mockResolvedValueOnce(ok(SAMPLE));
|
|
39
|
+
const res = await hq.getBillingUsage(API_KEY);
|
|
40
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
41
|
+
expect(url).toMatch(/\/hq\/billing\/usage$/);
|
|
42
|
+
expect(init.method).toBe('GET');
|
|
43
|
+
expect(init.headers.Authorization).toBe(`Bearer ${API_KEY}`);
|
|
44
|
+
expect(res.total_display).toBe('$3.54');
|
|
45
|
+
expect(res.services).toHaveLength(2);
|
|
46
|
+
expect(res.services[0]).toEqual({ service: 'llm', requests: 1240, cost_display: '$3.10' });
|
|
47
|
+
});
|
|
48
|
+
it('appends ?period=30d when the trailing-30-days window is requested', async () => {
|
|
49
|
+
fetchMock.mockResolvedValueOnce(ok({ ...SAMPLE, period: '30d' }));
|
|
50
|
+
await hq.getBillingUsage(API_KEY, '30d');
|
|
51
|
+
expect(fetchMock.mock.calls[0][0]).toMatch(/\/hq\/billing\/usage\?period=30d$/);
|
|
52
|
+
});
|
|
53
|
+
it('sends period=month explicitly when asked', async () => {
|
|
54
|
+
fetchMock.mockResolvedValueOnce(ok(SAMPLE));
|
|
55
|
+
await hq.getBillingUsage(API_KEY, 'month');
|
|
56
|
+
expect(fetchMock.mock.calls[0][0]).toMatch(/\?period=month$/);
|
|
57
|
+
});
|
|
58
|
+
it('returns an empty services list cleanly (no spend in the window)', async () => {
|
|
59
|
+
fetchMock.mockResolvedValueOnce(ok({ period: 'month', since: '2026-05-01T00:00:00Z', services: [], total_display: '$0.00' }));
|
|
60
|
+
const res = await hq.getBillingUsage(API_KEY);
|
|
61
|
+
expect(res.services).toEqual([]);
|
|
62
|
+
expect(res.total_display).toBe('$0.00');
|
|
63
|
+
});
|
|
64
|
+
it('surfaces a bad period (400) as a typed MyApiError', async () => {
|
|
65
|
+
fetchMock.mockResolvedValueOnce(fail('bad_request', "period must be 'month' or '30d'", 400));
|
|
66
|
+
await expect(hq.getBillingUsage(API_KEY, 'month'))
|
|
67
|
+
.rejects.toMatchObject({ name: 'MyApiError', status: 400 });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('hq.EXPOSES', () => {
|
|
71
|
+
it('lists GET /hq/billing/usage', () => {
|
|
72
|
+
expect(hq.EXPOSES).toContain('GET /hq/billing/usage');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|