outrank-cli 0.5.0 → 0.6.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/README.md +317 -20
- package/SKILL.md +20 -2
- package/bin/outrank-mcp.js +3 -0
- package/package.json +21 -5
- package/src/adapters/cli/jsonInput.js +42 -0
- package/src/adapters/cli/parse.js +124 -0
- package/src/adapters/cli/run.js +139 -0
- package/src/adapters/mcp/constants.js +49 -0
- package/src/adapters/mcp/descriptions.js +87 -0
- package/src/adapters/mcp/help.js +34 -0
- package/src/adapters/mcp/input.js +195 -0
- package/src/adapters/mcp/instructions.js +139 -0
- package/src/adapters/mcp/logger.js +45 -0
- package/src/adapters/mcp/main.js +36 -0
- package/src/adapters/mcp/options.js +118 -0
- package/src/adapters/mcp/result.js +33 -0
- package/src/adapters/mcp/server.js +174 -0
- package/src/adapters/mcp/session.js +274 -0
- package/src/adapters/mcp/toolNotes.js +426 -0
- package/src/adapters/mcp/toolsets.js +106 -0
- package/src/commands/articles.js +285 -301
- package/src/commands/auth.js +204 -86
- package/src/commands/billing.js +102 -104
- package/src/commands/improvements.js +319 -211
- package/src/commands/index.js +22 -0
- package/src/commands/keywords.js +266 -249
- package/src/commands/marketplace/buyer.js +386 -0
- package/src/commands/marketplace/index.js +83 -0
- package/src/commands/marketplace/notifications.js +60 -0
- package/src/commands/marketplace/seller.js +595 -0
- package/src/commands/marketplace/shared.js +27 -0
- package/src/commands/products/backlinks.js +181 -0
- package/src/commands/products/core.js +149 -0
- package/src/commands/products/gsc.js +250 -0
- package/src/commands/products/index.js +174 -0
- package/src/commands/products/integrations.js +343 -0
- package/src/commands/products/linking.js +192 -0
- package/src/commands/products/shared.js +8 -0
- package/src/commands/registry.js +272 -0
- package/src/commands/subscription.js +20 -30
- package/src/commands/usage.js +17 -30
- package/src/core/config.js +243 -62
- package/src/core/constants.js +101 -0
- package/src/core/context.js +311 -0
- package/src/core/errors.js +39 -0
- package/src/core/execute.js +71 -0
- package/src/core/files.js +186 -0
- package/src/core/http.js +393 -0
- package/src/core/profileSelection.js +7 -0
- package/src/core/query.js +11 -0
- package/src/core/redact.js +27 -0
- package/src/core/schema.js +222 -0
- package/src/core/version.js +12 -0
- package/src/index.js +30 -58
- package/src/commands/marketplace.js +0 -740
- package/src/commands/products.js +0 -1185
- package/src/core/api.js +0 -82
- package/src/core/args.js +0 -144
package/src/commands/auth.js
CHANGED
|
@@ -1,127 +1,245 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { requestAgentApi } from '../core/api.js';
|
|
1
|
+
import { PROFILE_ENV } from '../core/constants.js';
|
|
3
2
|
import {
|
|
4
|
-
|
|
3
|
+
API_KEY_FIELD,
|
|
4
|
+
BASE_URL_FIELD,
|
|
5
|
+
clearProfileAuth,
|
|
5
6
|
getConfigPath,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
loadProfiles,
|
|
8
|
+
redactApiKey,
|
|
9
|
+
storeProfileAuth,
|
|
9
10
|
} from '../core/config.js';
|
|
10
|
-
import {
|
|
11
|
+
import { ContextSource, Surface, normalizeBaseUrl } from '../core/context.js';
|
|
12
|
+
import { cliSuccess, usageError } from '../core/errors.js';
|
|
13
|
+
import { assertHeaderSafeApiKey, requestAgentApi } from '../core/http.js';
|
|
14
|
+
import { profileSelectedByEnvHint } from '../core/profileSelection.js';
|
|
15
|
+
import { bool, string } from '../core/schema.js';
|
|
16
|
+
import { CommandKind, Toolset, defineCommand, defineGroup } from './registry.js';
|
|
11
17
|
|
|
12
18
|
export const AUTH_HELP = `Usage:
|
|
13
|
-
outrank-cli auth login <api-key> [--base-url <url>] [--no-verify]
|
|
19
|
+
outrank-cli auth login <api-key> [--profile <name>] [--default] [--base-url <url>] [--no-verify]
|
|
14
20
|
outrank-cli auth whoami
|
|
15
21
|
outrank-cli auth status
|
|
16
|
-
outrank-cli auth
|
|
22
|
+
outrank-cli auth profiles
|
|
23
|
+
outrank-cli auth logout [--profile <name>]
|
|
17
24
|
|
|
18
25
|
Commands:
|
|
19
|
-
login Store an API key
|
|
26
|
+
login Store an API key in a profile (the default profile unless --profile is given) and optionally verify it.
|
|
20
27
|
whoami Verify auth and return the current user and organization details.
|
|
21
|
-
status Show the
|
|
22
|
-
|
|
28
|
+
status Show the resolved profile, where its key and base URL came from, and the config path.
|
|
29
|
+
profiles List stored profiles with redacted keys and mark the default one.
|
|
30
|
+
logout Remove the stored API key from a profile.
|
|
23
31
|
|
|
24
32
|
Notes:
|
|
25
|
-
|
|
33
|
+
Precedence: --api-key and --base-url win; then a profile selected with --profile or OUTRANK_PROFILE
|
|
34
|
+
(a selected profile ignores OUTRANK_API_KEY and OUTRANK_BASE_URL); then OUTRANK_API_KEY and
|
|
35
|
+
OUTRANK_BASE_URL; then the default profile from local config.
|
|
36
|
+
login verifies against and stores the resolved base URL: --base-url, else OUTRANK_BASE_URL (ignored when
|
|
37
|
+
a profile is selected), else the profile's stored value, else the default.
|
|
26
38
|
Optional config-dir override: OUTRANK_CONFIG_DIR
|
|
27
39
|
Local config path: ~/.outrank/config.json
|
|
28
40
|
`;
|
|
29
41
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
const WHOAMI_PATH = '/auth/whoami';
|
|
43
|
+
const CONFIG_PATH_FIELD = 'config_path';
|
|
44
|
+
const ENV_IGNORED_SUFFIX = ' (env ignored)';
|
|
45
|
+
const LOGIN_MISSING_KEY_GUIDANCE = 'Run `outrank-cli auth login <api-key>` or pass `--api-key <api-key>`, then retry.';
|
|
46
|
+
const LOGIN_SAVED_GUIDANCE =
|
|
47
|
+
'Credentials saved. Run `outrank-cli auth whoami` to confirm the active workspace at any time.';
|
|
48
|
+
const loginSavedToProfileGuidance = (profile) =>
|
|
49
|
+
`Credentials saved to profile ${profile}, which is not the default. Pass --profile ${profile} or set ` +
|
|
50
|
+
`${PROFILE_ENV}=${profile} to use it, or re-run login with --default to make it the default.`;
|
|
51
|
+
const STATUS_GUIDANCE =
|
|
52
|
+
'Confirm this target before running live commands. If you use a non-default Outrank deployment, set ' +
|
|
53
|
+
'`OUTRANK_BASE_URL` first.';
|
|
54
|
+
const STATUS_UNKNOWN_PROFILE_GUIDANCE =
|
|
55
|
+
'The selected profile is not stored. Run `outrank-cli auth profiles` to see the stored profiles, or ' +
|
|
56
|
+
'`outrank-cli auth login <api-key> --profile <name>` to create it.';
|
|
57
|
+
const STATUS_UNKNOWN_PROFILE_WITH_KEY_GUIDANCE =
|
|
58
|
+
'The selected profile is not stored; the key in use comes from --api-key, not from a profile. Run ' +
|
|
59
|
+
'`outrank-cli auth login <api-key> --profile <name>` to store it.';
|
|
60
|
+
const statusInvalidStoredBaseUrlGuidance = (profile) =>
|
|
61
|
+
`The base URL stored in profile ${profile} is not a valid URL, so authenticated commands will fail. Edit it in ` +
|
|
62
|
+
`the config file, or run \`outrank-cli auth login <api-key> --profile ${profile} --base-url <url>\` to replace it.`;
|
|
63
|
+
const LOGIN_BASE_URL_FROM_ENV_NOTE =
|
|
64
|
+
' The base URL came from OUTRANK_BASE_URL and is now stored in the profile; unset the variable if that was not ' +
|
|
65
|
+
'intended and run login again with --base-url.';
|
|
35
66
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
);
|
|
67
|
+
const statusGuidance = (ctx) => {
|
|
68
|
+
if (ctx.profile.explicit && !ctx.profile.known) {
|
|
69
|
+
const base = ctx.apiKey ? STATUS_UNKNOWN_PROFILE_WITH_KEY_GUIDANCE : STATUS_UNKNOWN_PROFILE_GUIDANCE;
|
|
70
|
+
return base + profileSelectedByEnvHint(ctx.profile.source);
|
|
71
|
+
}
|
|
72
|
+
if (ctx.baseUrl === null) {
|
|
73
|
+
return statusInvalidStoredBaseUrlGuidance(ctx.profile.name) + profileSelectedByEnvHint(ctx.profile.source);
|
|
74
|
+
}
|
|
75
|
+
return STATUS_GUIDANCE;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const describeStoredBaseUrl = (value) => {
|
|
79
|
+
if (value === undefined) return { base_url: null, base_url_valid: true };
|
|
80
|
+
try {
|
|
81
|
+
return { base_url: normalizeBaseUrl(value), base_url_valid: true };
|
|
82
|
+
} catch {
|
|
83
|
+
return { base_url: null, base_url_valid: false };
|
|
43
84
|
}
|
|
85
|
+
};
|
|
86
|
+
const LOGOUT_GUIDANCE =
|
|
87
|
+
'Stored API key removed. Any future authenticated call now requires OUTRANK_API_KEY or a fresh `auth login`.';
|
|
88
|
+
const logoutProfileGuidance = (profile) =>
|
|
89
|
+
`Stored API key removed from profile ${profile}. Other profiles are untouched; run ` +
|
|
90
|
+
`\`outrank-cli auth login <api-key> --profile ${profile}\` to store a new key.`;
|
|
91
|
+
const logoutNothingStoredGuidance = (profile) =>
|
|
92
|
+
`Profile ${profile} had no stored API key, so nothing changed. Run \`outrank-cli auth profiles\` to see ` +
|
|
93
|
+
'which profiles hold keys.';
|
|
94
|
+
const PROFILES_EMPTY_GUIDANCE =
|
|
95
|
+
'No profiles are stored. Run `outrank-cli auth login <api-key>` (optionally with --profile <name>) to create one.';
|
|
96
|
+
const PROFILES_GUIDANCE =
|
|
97
|
+
'Select a profile per call with --profile <name> or per shell with OUTRANK_PROFILE. A selected profile ignores ' +
|
|
98
|
+
'OUTRANK_API_KEY and OUTRANK_BASE_URL; --api-key and --base-url still win.';
|
|
44
99
|
|
|
45
|
-
|
|
46
|
-
const apiKey = parsed.positionals[0] ?? getOption(parsed, 'api-key');
|
|
47
|
-
if (!apiKey) {
|
|
48
|
-
throw Object.assign(new Error('Missing API key.'), {
|
|
49
|
-
cliCode: 'CLI_INVALID_USAGE',
|
|
50
|
-
guidance:
|
|
51
|
-
'Run `outrank-cli auth login <api-key>` or pass `--api-key <api-key>`, then retry.',
|
|
52
|
-
});
|
|
53
|
-
}
|
|
100
|
+
const configPathField = (ctx) => (ctx.surface === Surface.MCP ? {} : { [CONFIG_PATH_FIELD]: getConfigPath() });
|
|
54
101
|
|
|
55
|
-
|
|
56
|
-
if (!hasFlag(parsed, 'no-verify')) {
|
|
57
|
-
const verifyParsed = parseArgs(['--api-key', apiKey, '--base-url', baseUrl]);
|
|
58
|
-
const response = await requestAgentApi(verifyParsed, {
|
|
59
|
-
method: 'GET',
|
|
60
|
-
path: '/auth/whoami',
|
|
61
|
-
});
|
|
62
|
-
if (!response.ok) {
|
|
63
|
-
return response.payload;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
102
|
+
const renderSource = (source, envIgnored) => (envIgnored ? `${source}${ENV_IGNORED_SUFFIX}` : source);
|
|
66
103
|
|
|
67
|
-
|
|
104
|
+
const login = defineCommand({
|
|
105
|
+
id: 'auth.login',
|
|
106
|
+
summary: 'Store an API key in a profile and optionally verify it immediately.',
|
|
107
|
+
kind: CommandKind.LOCAL,
|
|
108
|
+
toolsets: [Toolset.CORE],
|
|
109
|
+
mcp: false,
|
|
110
|
+
allowUnknownProfile: true,
|
|
111
|
+
input: { apiKey: string(), noVerify: bool(), makeDefault: bool() },
|
|
112
|
+
cli: {
|
|
113
|
+
positionals: ['apiKey'],
|
|
114
|
+
placeholders: { apiKey: '<api-key>' },
|
|
115
|
+
options: { 'no-verify': 'noVerify', default: 'makeDefault' },
|
|
116
|
+
},
|
|
117
|
+
run: async (input, ctx) => {
|
|
118
|
+
const apiKey = input.apiKey ?? (ctx.source.apiKey === ContextSource.FLAG ? ctx.apiKey : undefined);
|
|
119
|
+
if (!apiKey) throw usageError('Missing API key.', LOGIN_MISSING_KEY_GUIDANCE);
|
|
120
|
+
assertHeaderSafeApiKey(apiKey);
|
|
121
|
+
if (!input.noVerify) {
|
|
122
|
+
const { result } = await requestAgentApi({ ...ctx, apiKey }, { method: 'GET', path: WHOAMI_PATH });
|
|
123
|
+
if (!result.ok) return result;
|
|
124
|
+
}
|
|
125
|
+
const saved = await storeProfileAuth({
|
|
126
|
+
profile: ctx.profile.name,
|
|
127
|
+
apiKey,
|
|
128
|
+
baseUrl: ctx.baseUrl,
|
|
129
|
+
makeDefault: input.makeDefault,
|
|
130
|
+
});
|
|
131
|
+
const envNote = ctx.source.baseUrl === ContextSource.ENV ? LOGIN_BASE_URL_FROM_ENV_NOTE : '';
|
|
68
132
|
return cliSuccess(
|
|
69
133
|
{
|
|
70
134
|
stored: {
|
|
135
|
+
profile: saved.profile,
|
|
136
|
+
is_default: saved.isDefault,
|
|
71
137
|
api_key: true,
|
|
72
|
-
base_url: saved.
|
|
73
|
-
|
|
138
|
+
base_url: saved.stored[BASE_URL_FIELD],
|
|
139
|
+
base_url_source: ctx.source.baseUrl,
|
|
140
|
+
[CONFIG_PATH_FIELD]: getConfigPath(),
|
|
74
141
|
},
|
|
75
142
|
},
|
|
76
|
-
|
|
143
|
+
(saved.isDefault ? LOGIN_SAVED_GUIDANCE : loginSavedToProfileGuidance(saved.profile)) + envNote,
|
|
77
144
|
);
|
|
78
|
-
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
79
147
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
148
|
+
const whoami = defineCommand({
|
|
149
|
+
id: 'auth.whoami',
|
|
150
|
+
summary: 'Verify auth and return the current user and organization details.',
|
|
151
|
+
kind: CommandKind.READ,
|
|
152
|
+
toolsets: [Toolset.CORE],
|
|
153
|
+
run: (_input, ctx) => ctx.http({ method: 'GET', path: WHOAMI_PATH }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const status = defineCommand({
|
|
157
|
+
id: 'auth.status',
|
|
158
|
+
summary: 'Show the resolved profile, auth source and base-url configuration (the key itself is never shown).',
|
|
159
|
+
kind: CommandKind.LOCAL,
|
|
160
|
+
toolsets: [Toolset.CORE],
|
|
161
|
+
mcp: { redact: [CONFIG_PATH_FIELD] },
|
|
162
|
+
offline: true,
|
|
163
|
+
allowUnknownProfile: true,
|
|
164
|
+
run: (_input, ctx) =>
|
|
165
|
+
cliSuccess(
|
|
166
|
+
{
|
|
167
|
+
profile: ctx.profile.name,
|
|
168
|
+
profile_source: ctx.profile.source,
|
|
169
|
+
profile_stored: ctx.profile.known,
|
|
170
|
+
base_url: ctx.baseUrl,
|
|
171
|
+
base_url_valid: ctx.baseUrl !== null,
|
|
172
|
+
api_key_present: Boolean(ctx.apiKey),
|
|
173
|
+
api_key_source: renderSource(ctx.source.apiKey, ctx.source.envIgnored),
|
|
174
|
+
base_url_source: renderSource(ctx.source.baseUrl, ctx.source.baseUrlEnvIgnored),
|
|
175
|
+
...configPathField(ctx),
|
|
176
|
+
},
|
|
177
|
+
statusGuidance(ctx),
|
|
178
|
+
),
|
|
179
|
+
});
|
|
87
180
|
|
|
88
|
-
|
|
89
|
-
|
|
181
|
+
const profiles = defineCommand({
|
|
182
|
+
id: 'auth.profiles',
|
|
183
|
+
summary: 'List stored profiles with redacted keys and mark the default one.',
|
|
184
|
+
kind: CommandKind.LOCAL,
|
|
185
|
+
toolsets: [Toolset.CORE],
|
|
186
|
+
mcp: false,
|
|
187
|
+
offline: true,
|
|
188
|
+
allowUnknownProfile: true,
|
|
189
|
+
run: async (_input, ctx) => {
|
|
190
|
+
const state = await loadProfiles();
|
|
191
|
+
const names = Object.keys(state.profiles).sort();
|
|
90
192
|
return cliSuccess(
|
|
91
193
|
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
: 'default',
|
|
104
|
-
config_path: getConfigPath(),
|
|
194
|
+
default_profile: state.defaultProfile,
|
|
195
|
+
active_profile: ctx.profile.name,
|
|
196
|
+
active_profile_stored: ctx.profile.known,
|
|
197
|
+
profiles: names.map((name) => ({
|
|
198
|
+
name,
|
|
199
|
+
is_default: name === state.defaultProfile,
|
|
200
|
+
api_key_present: Boolean(state.profiles[name][API_KEY_FIELD]),
|
|
201
|
+
api_key_suffix: redactApiKey(state.profiles[name][API_KEY_FIELD]),
|
|
202
|
+
...describeStoredBaseUrl(state.profiles[name][BASE_URL_FIELD]),
|
|
203
|
+
})),
|
|
204
|
+
[CONFIG_PATH_FIELD]: getConfigPath(),
|
|
105
205
|
},
|
|
106
|
-
|
|
206
|
+
names.length === 0 ? PROFILES_EMPTY_GUIDANCE : PROFILES_GUIDANCE,
|
|
107
207
|
);
|
|
108
|
-
}
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const logoutGuidance = (ctx, removed) => {
|
|
212
|
+
if (!removed) return logoutNothingStoredGuidance(ctx.profile.name);
|
|
213
|
+
return ctx.profile.explicit ? logoutProfileGuidance(ctx.profile.name) : LOGOUT_GUIDANCE;
|
|
214
|
+
};
|
|
109
215
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
216
|
+
const logout = defineCommand({
|
|
217
|
+
id: 'auth.logout',
|
|
218
|
+
summary: 'Remove the stored API key from a profile.',
|
|
219
|
+
kind: CommandKind.LOCAL,
|
|
220
|
+
toolsets: [Toolset.CORE],
|
|
221
|
+
mcp: false,
|
|
222
|
+
offline: true,
|
|
223
|
+
run: async (_input, ctx) => {
|
|
224
|
+
const { removed } = await clearProfileAuth({ profile: ctx.profile.name });
|
|
113
225
|
return cliSuccess(
|
|
114
226
|
{
|
|
115
|
-
cleared: [
|
|
116
|
-
|
|
117
|
-
|
|
227
|
+
cleared: removed ? [API_KEY_FIELD] : [],
|
|
228
|
+
profile: ctx.profile.name,
|
|
229
|
+
[CONFIG_PATH_FIELD]: getConfigPath(),
|
|
230
|
+
base_url_still_configured: Boolean(ctx.storedBaseUrl),
|
|
118
231
|
},
|
|
119
|
-
|
|
232
|
+
logoutGuidance(ctx, removed),
|
|
120
233
|
);
|
|
121
|
-
}
|
|
234
|
+
},
|
|
235
|
+
});
|
|
122
236
|
|
|
123
|
-
|
|
124
|
-
|
|
237
|
+
export const authGroup = defineGroup({
|
|
238
|
+
id: 'auth',
|
|
239
|
+
help: { usage: AUTH_HELP, guidance: 'Choose one of: login, whoami, status, profiles, logout.' },
|
|
240
|
+
unknown: (word) => ({
|
|
241
|
+
message: `Unknown auth subcommand: ${word}.`,
|
|
125
242
|
guidance: 'Run `outrank-cli auth --help` to see available auth commands.',
|
|
126
|
-
})
|
|
127
|
-
|
|
243
|
+
}),
|
|
244
|
+
children: [login, whoami, status, profiles, logout],
|
|
245
|
+
});
|
package/src/commands/billing.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
BILLING_MAX_ADDITIONAL_PRODUCTS,
|
|
5
|
+
BILLING_MIN_ADDITIONAL_PRODUCTS,
|
|
6
|
+
BILLING_MIN_CONFIRM_CHARGE_CENTS,
|
|
7
|
+
HTTP_STATUS_CONFLICT,
|
|
8
|
+
} from '../core/constants.js';
|
|
5
9
|
import { cliSuccess } from '../core/errors.js';
|
|
10
|
+
import { int, string } from '../core/schema.js';
|
|
11
|
+
import { CommandKind, Toolset, defineCommand, defineGroup } from './registry.js';
|
|
6
12
|
|
|
7
13
|
export const BILLING_HELP = `Usage:
|
|
8
14
|
outrank-cli billing portal-url
|
|
@@ -16,114 +22,106 @@ Notes:
|
|
|
16
22
|
buy requires the server-quoted recurring total in cents and an explicit idempotency key.
|
|
17
23
|
`;
|
|
18
24
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
25
|
+
const PURCHASE_PRODUCTS_HELP =
|
|
26
|
+
'Usage:\n' +
|
|
27
|
+
' outrank-cli billing purchase-products quote --additional-products <n> [--idempotency-key <key>]\n' +
|
|
28
|
+
' outrank-cli billing purchase-products buy --additional-products <n> --confirm-charge-cents <n> ' +
|
|
29
|
+
'--idempotency-key <key>';
|
|
30
|
+
|
|
31
|
+
const PURCHASE_PRODUCTS_PATH = '/billing/purchase-products';
|
|
32
|
+
const QUOTE_GUIDANCE =
|
|
33
|
+
'Quote received. Review the recurring total and, if the human approves, run the matching `buy` command with ' +
|
|
34
|
+
'--confirm-charge-cents set to the quoted amount.';
|
|
35
|
+
const buyKeyGuidance = () =>
|
|
36
|
+
`Generate a stable unique key for this purchase, for example ${randomUUID()}, then retry with ` +
|
|
37
|
+
'--idempotency-key <value>.';
|
|
38
|
+
|
|
39
|
+
const additionalProducts = () =>
|
|
40
|
+
int({ min: BILLING_MIN_ADDITIONAL_PRODUCTS, max: BILLING_MAX_ADDITIONAL_PRODUCTS }).required();
|
|
41
|
+
|
|
42
|
+
const isQuote = (status, result) =>
|
|
43
|
+
status === HTTP_STATUS_CONFLICT &&
|
|
44
|
+
result.ok === false &&
|
|
45
|
+
Number.isInteger(result.error?.details?.expected_recurring_total_cents);
|
|
46
|
+
|
|
47
|
+
const portalUrl = defineCommand({
|
|
48
|
+
id: 'billing.portal-url',
|
|
49
|
+
summary: 'Return a Stripe customer-portal link for the human to open in a browser.',
|
|
50
|
+
kind: CommandKind.READ,
|
|
51
|
+
toolsets: [Toolset.BILLING],
|
|
52
|
+
entryPoint: true,
|
|
53
|
+
run: (_input, ctx) => ctx.http({ method: 'GET', path: '/billing/portal-url' }),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const quote = defineCommand({
|
|
57
|
+
id: 'billing.purchase-products.quote',
|
|
58
|
+
summary: 'Request pricing for additional product slots without authorizing the charge.',
|
|
59
|
+
kind: CommandKind.READ,
|
|
60
|
+
toolsets: [Toolset.BILLING],
|
|
61
|
+
entryPoint: true,
|
|
62
|
+
input: { additionalProducts: additionalProducts(), idempotencyKey: string() },
|
|
63
|
+
run: async (input, ctx) => {
|
|
64
|
+
const idempotencyKey = input.idempotencyKey ?? randomUUID();
|
|
65
|
+
const { result, status } = await ctx.request({
|
|
44
66
|
method: 'POST',
|
|
45
|
-
path:
|
|
46
|
-
body: {
|
|
47
|
-
additional_products: additionalProducts,
|
|
48
|
-
},
|
|
67
|
+
path: PURCHASE_PRODUCTS_PATH,
|
|
68
|
+
body: { additional_products: input.additionalProducts },
|
|
49
69
|
idempotencyKey,
|
|
50
70
|
});
|
|
51
|
-
|
|
52
|
-
if (!response.ok && response.status === 409) {
|
|
71
|
+
if (isQuote(status, result)) {
|
|
53
72
|
return cliSuccess(
|
|
54
|
-
{
|
|
55
|
-
|
|
56
|
-
api_response: response.payload,
|
|
57
|
-
idempotency_key: idempotencyKey,
|
|
58
|
-
},
|
|
59
|
-
'Quote received. Review the recurring total and, if the human approves, run the matching `buy` command with --confirm-charge-cents set to the quoted amount.',
|
|
73
|
+
{ quote: result.error.details, api_response: result, idempotency_key: idempotencyKey },
|
|
74
|
+
QUOTE_GUIDANCE,
|
|
60
75
|
);
|
|
61
76
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (subcommand === 'buy') {
|
|
67
|
-
const confirmChargeCents = parseIntegerOption(parsed, 'confirm-charge-cents', {
|
|
68
|
-
required: true,
|
|
69
|
-
min: 0,
|
|
70
|
-
});
|
|
71
|
-
const idempotencyKey = getOption(parsed, 'idempotency-key');
|
|
72
|
-
if (!idempotencyKey) {
|
|
73
|
-
throw Object.assign(new Error('Missing required option: --idempotency-key.'), {
|
|
74
|
-
cliCode: 'CLI_INVALID_USAGE',
|
|
75
|
-
guidance:
|
|
76
|
-
`Generate a stable unique key for this purchase, for example ${randomUUID()}, then retry with --idempotency-key <value>.`,
|
|
77
|
-
});
|
|
77
|
+
if (result.ok && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
|
|
78
|
+
return { ...result, data: { ...result.data, idempotency_key: idempotencyKey } };
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
return result;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const buy = defineCommand({
|
|
85
|
+
id: 'billing.purchase-products.buy',
|
|
86
|
+
summary: 'Buy additional product slots at the quoted recurring total.',
|
|
87
|
+
kind: CommandKind.MONEY,
|
|
88
|
+
toolsets: [Toolset.BILLING],
|
|
89
|
+
input: {
|
|
90
|
+
additionalProducts: additionalProducts(),
|
|
91
|
+
confirmChargeCents: int({ min: BILLING_MIN_CONFIRM_CHARGE_CENTS }).required(),
|
|
92
|
+
idempotencyKey: string().required({ guidance: buyKeyGuidance() }),
|
|
93
|
+
},
|
|
94
|
+
run: (input, ctx) =>
|
|
95
|
+
ctx.http({
|
|
81
96
|
method: 'POST',
|
|
82
|
-
path:
|
|
83
|
-
body: {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
'Choose one of the currently available billing groups: `portal-url` or `purchase-products`.',
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (subcommand === 'portal-url') {
|
|
114
|
-
const response = await requestAgentApi(parsed, {
|
|
115
|
-
method: 'GET',
|
|
116
|
-
path: '/billing/portal-url',
|
|
117
|
-
});
|
|
118
|
-
return response.payload;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (subcommand === 'purchase-products') {
|
|
122
|
-
return handlePurchaseProducts(argv.slice(1));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
throw Object.assign(new Error(`Unknown billing subcommand: ${subcommand}.`), {
|
|
126
|
-
cliCode: 'CLI_INVALID_USAGE',
|
|
97
|
+
path: PURCHASE_PRODUCTS_PATH,
|
|
98
|
+
body: { additional_products: input.additionalProducts, confirm_charge_cents: input.confirmChargeCents },
|
|
99
|
+
idempotencyKey: input.idempotencyKey,
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const purchaseProducts = defineGroup({
|
|
104
|
+
id: 'billing.purchase-products',
|
|
105
|
+
help: {
|
|
106
|
+
usage: PURCHASE_PRODUCTS_HELP,
|
|
107
|
+
guidance: 'Use `quote` first, then `buy` with the quoted recurring total and a stable idempotency key.',
|
|
108
|
+
},
|
|
109
|
+
unknown: (word) => ({
|
|
110
|
+
message: `Unknown billing purchase-products subcommand: ${word}.`,
|
|
111
|
+
guidance: 'Run `outrank-cli billing purchase-products --help` to see the available purchase-products commands.',
|
|
112
|
+
}),
|
|
113
|
+
children: [quote, buy],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const billingGroup = defineGroup({
|
|
117
|
+
id: 'billing',
|
|
118
|
+
help: {
|
|
119
|
+
usage: BILLING_HELP,
|
|
120
|
+
guidance: 'Choose one of the currently available billing groups: `portal-url` or `purchase-products`.',
|
|
121
|
+
},
|
|
122
|
+
unknown: (word) => ({
|
|
123
|
+
message: `Unknown billing subcommand: ${word}.`,
|
|
127
124
|
guidance: 'Run `outrank-cli billing --help` to see available billing commands.',
|
|
128
|
-
})
|
|
129
|
-
|
|
125
|
+
}),
|
|
126
|
+
children: [portalUrl, purchaseProducts],
|
|
127
|
+
});
|