outrank-cli 0.1.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/LICENSE +21 -0
- package/README.md +152 -0
- package/SKILL.md +348 -0
- package/bin/outrank-cli.js +2 -0
- package/package.json +46 -0
- package/src/commands/articles.js +301 -0
- package/src/commands/auth.js +127 -0
- package/src/commands/billing.js +129 -0
- package/src/commands/keywords.js +311 -0
- package/src/commands/products.js +476 -0
- package/src/commands/subscription.js +39 -0
- package/src/commands/usage.js +39 -0
- package/src/core/api.js +82 -0
- package/src/core/args.js +144 -0
- package/src/core/config.js +80 -0
- package/src/core/errors.js +15 -0
- package/src/core/io.js +49 -0
- package/src/index.js +134 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { parseArgs, getRequiredPositional, readJsonInput, hasFlag, parseIntegerOption, getOption } from '../core/args.js';
|
|
2
|
+
import { requestAgentApi } from '../core/api.js';
|
|
3
|
+
import { cliSuccess } from '../core/errors.js';
|
|
4
|
+
|
|
5
|
+
export const PRODUCTS_HELP = `Usage:
|
|
6
|
+
outrank-cli products list [--status active|disabled] [--limit <n>] [--cursor <iso-timestamp>]
|
|
7
|
+
outrank-cli products get <product-id>
|
|
8
|
+
outrank-cli products create --json <object>
|
|
9
|
+
outrank-cli products update <product-id> --json <object>
|
|
10
|
+
outrank-cli products pause <product-id>
|
|
11
|
+
outrank-cli products resume <product-id>
|
|
12
|
+
outrank-cli products output-settings get <product-id>
|
|
13
|
+
outrank-cli products output-settings update <product-id> --json <object>
|
|
14
|
+
outrank-cli products target-audiences list <product-id>
|
|
15
|
+
outrank-cli products target-audiences replace <product-id> --json <object>
|
|
16
|
+
outrank-cli products competitors list <product-id>
|
|
17
|
+
outrank-cli products competitors replace <product-id> --json <object>
|
|
18
|
+
outrank-cli products gsc status <product-id>
|
|
19
|
+
outrank-cli products gsc connect <product-id>
|
|
20
|
+
outrank-cli products gsc poll <product-id> --attempt-id <attempt-id>
|
|
21
|
+
outrank-cli products gsc reconnect <product-id>
|
|
22
|
+
outrank-cli products gsc disconnect <product-id>
|
|
23
|
+
outrank-cli products gsc accounts <product-id>
|
|
24
|
+
outrank-cli products gsc properties <product-id> --email <email>
|
|
25
|
+
outrank-cli products gsc set-property <product-id> --email <email> --site-url <url>
|
|
26
|
+
outrank-cli products gsc performance <product-id> [--months <n>]
|
|
27
|
+
outrank-cli products gsc search-analytics <product-id> [--dimensions <csv>] [--limit <n>] [--months <n>] [--start-date <yyyy-mm-dd>] [--end-date <yyyy-mm-dd>] [--page <text>] [--query <text>] [--country <cc>] [--device <device>]
|
|
28
|
+
outrank-cli products gsc inspect-url <product-id> --url <url> [--language-code <code>]
|
|
29
|
+
outrank-cli products gsc cannibalization <product-id>
|
|
30
|
+
|
|
31
|
+
JSON Input:
|
|
32
|
+
Use one of: --json '<object>', --json-file payload.json, or --json-stdin
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
outrank-cli products list --limit 20
|
|
36
|
+
outrank-cli products create --json-file product.json
|
|
37
|
+
outrank-cli products update 00000000-0000-0000-0000-000000000000 --json '{"daily_article_count":2}'
|
|
38
|
+
outrank-cli products output-settings update 00000000-0000-0000-0000-000000000000 --json-file output-settings.json
|
|
39
|
+
outrank-cli products target-audiences replace 00000000-0000-0000-0000-000000000000 --json-file target-audiences.json
|
|
40
|
+
outrank-cli products competitors replace 00000000-0000-0000-0000-000000000000 --json-file competitors.json
|
|
41
|
+
outrank-cli products gsc connect 00000000-0000-0000-0000-000000000000
|
|
42
|
+
outrank-cli products gsc properties 00000000-0000-0000-0000-000000000000 --email owner@example.com
|
|
43
|
+
outrank-cli products gsc performance 00000000-0000-0000-0000-000000000000 --months 3
|
|
44
|
+
outrank-cli products gsc inspect-url 00000000-0000-0000-0000-000000000000 --url https://example.com/blog/post
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const buildQueryPath = (basePath, searchParams) => {
|
|
48
|
+
const url = new URL(`https://local${basePath}`);
|
|
49
|
+
for (const [key, value] of Object.entries(searchParams)) {
|
|
50
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
51
|
+
url.searchParams.set(key, String(value));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `${url.pathname}${url.search}`;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleProductsList = async (parsed) => {
|
|
58
|
+
const path = buildQueryPath('/products', {
|
|
59
|
+
status: getOption(parsed, 'status'),
|
|
60
|
+
limit: parseIntegerOption(parsed, 'limit', { min: 1, max: 100 }),
|
|
61
|
+
cursor: getOption(parsed, 'cursor'),
|
|
62
|
+
});
|
|
63
|
+
const response = await requestAgentApi(parsed, {
|
|
64
|
+
method: 'GET',
|
|
65
|
+
path,
|
|
66
|
+
});
|
|
67
|
+
return response.payload;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleProductsGet = async (parsed) => {
|
|
71
|
+
const productId = getRequiredPositional(parsed, 0, '<product-id>');
|
|
72
|
+
const response = await requestAgentApi(parsed, {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
path: `/products/${productId}`,
|
|
75
|
+
});
|
|
76
|
+
return response.payload;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleProductsCreate = async (parsed) => {
|
|
80
|
+
const body = await readJsonInput(parsed);
|
|
81
|
+
const response = await requestAgentApi(parsed, {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
path: '/products',
|
|
84
|
+
body,
|
|
85
|
+
});
|
|
86
|
+
return response.payload;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleProductsUpdate = async (parsed) => {
|
|
90
|
+
const productId = getRequiredPositional(parsed, 0, '<product-id>');
|
|
91
|
+
const body = await readJsonInput(parsed);
|
|
92
|
+
const response = await requestAgentApi(parsed, {
|
|
93
|
+
method: 'PATCH',
|
|
94
|
+
path: `/products/${productId}`,
|
|
95
|
+
body,
|
|
96
|
+
});
|
|
97
|
+
return response.payload;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleProductsPause = async (parsed) => {
|
|
101
|
+
const productId = getRequiredPositional(parsed, 0, '<product-id>');
|
|
102
|
+
const response = await requestAgentApi(parsed, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
path: `/products/${productId}/pause`,
|
|
105
|
+
});
|
|
106
|
+
return response.payload;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleProductsResume = async (parsed) => {
|
|
110
|
+
const productId = getRequiredPositional(parsed, 0, '<product-id>');
|
|
111
|
+
const response = await requestAgentApi(parsed, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
path: `/products/${productId}/resume`,
|
|
114
|
+
});
|
|
115
|
+
return response.payload;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleOutputSettings = async (argv) => {
|
|
119
|
+
const subcommand = argv[0];
|
|
120
|
+
const parsed = parseArgs(argv.slice(1));
|
|
121
|
+
const wantsHelp =
|
|
122
|
+
!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || hasFlag(parsed, 'help');
|
|
123
|
+
|
|
124
|
+
if (wantsHelp) {
|
|
125
|
+
return cliSuccess(
|
|
126
|
+
{
|
|
127
|
+
usage:
|
|
128
|
+
'Usage:\n outrank-cli products output-settings get <product-id>\n outrank-cli products output-settings update <product-id> --json <object>',
|
|
129
|
+
},
|
|
130
|
+
'Use `get` to inspect current article settings and `update` to patch them.',
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const productId = getRequiredPositional(parsed, 0, '<product-id>');
|
|
135
|
+
|
|
136
|
+
if (subcommand === 'get') {
|
|
137
|
+
const response = await requestAgentApi(parsed, {
|
|
138
|
+
method: 'GET',
|
|
139
|
+
path: `/products/${productId}/output-settings`,
|
|
140
|
+
});
|
|
141
|
+
return response.payload;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (subcommand === 'update') {
|
|
145
|
+
const body = await readJsonInput(parsed);
|
|
146
|
+
const response = await requestAgentApi(parsed, {
|
|
147
|
+
method: 'PATCH',
|
|
148
|
+
path: `/products/${productId}/output-settings`,
|
|
149
|
+
body,
|
|
150
|
+
});
|
|
151
|
+
return response.payload;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw Object.assign(new Error(`Unknown products output-settings subcommand: ${subcommand}.`), {
|
|
155
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
156
|
+
guidance:
|
|
157
|
+
'Run `outrank-cli products output-settings --help` to see the available output-settings commands.',
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const handleCompetitors = async (argv) => {
|
|
162
|
+
const subcommand = argv[0];
|
|
163
|
+
const parsed = parseArgs(argv.slice(1));
|
|
164
|
+
const wantsHelp =
|
|
165
|
+
!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || hasFlag(parsed, 'help');
|
|
166
|
+
|
|
167
|
+
if (wantsHelp) {
|
|
168
|
+
return cliSuccess(
|
|
169
|
+
{
|
|
170
|
+
usage:
|
|
171
|
+
'Usage:\n outrank-cli products competitors list <product-id>\n outrank-cli products competitors replace <product-id> --json <object>',
|
|
172
|
+
},
|
|
173
|
+
'Use `list` to inspect the competitor list and `replace` to submit the full replacement set.',
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const productId = getRequiredPositional(parsed, 0, '<product-id>');
|
|
178
|
+
|
|
179
|
+
if (subcommand === 'list') {
|
|
180
|
+
const response = await requestAgentApi(parsed, {
|
|
181
|
+
method: 'GET',
|
|
182
|
+
path: `/products/${productId}/competitors`,
|
|
183
|
+
});
|
|
184
|
+
return response.payload;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (subcommand === 'replace') {
|
|
188
|
+
const body = await readJsonInput(parsed);
|
|
189
|
+
const response = await requestAgentApi(parsed, {
|
|
190
|
+
method: 'PUT',
|
|
191
|
+
path: `/products/${productId}/competitors`,
|
|
192
|
+
body,
|
|
193
|
+
});
|
|
194
|
+
return response.payload;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw Object.assign(new Error(`Unknown products competitors subcommand: ${subcommand}.`), {
|
|
198
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
199
|
+
guidance:
|
|
200
|
+
'Run `outrank-cli products competitors --help` to see the available competitor commands.',
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const handleTargetAudiences = async (argv) => {
|
|
205
|
+
const subcommand = argv[0];
|
|
206
|
+
const parsed = parseArgs(argv.slice(1));
|
|
207
|
+
const wantsHelp =
|
|
208
|
+
!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || hasFlag(parsed, 'help');
|
|
209
|
+
|
|
210
|
+
if (wantsHelp) {
|
|
211
|
+
return cliSuccess(
|
|
212
|
+
{
|
|
213
|
+
usage:
|
|
214
|
+
'Usage:\n outrank-cli products target-audiences list <product-id>\n outrank-cli products target-audiences replace <product-id> --json <object>',
|
|
215
|
+
},
|
|
216
|
+
'Use `list` to inspect the target-audience list and `replace` to submit the full replacement set.',
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const productId = getRequiredPositional(parsed, 0, '<product-id>');
|
|
221
|
+
|
|
222
|
+
if (subcommand === 'list') {
|
|
223
|
+
const response = await requestAgentApi(parsed, {
|
|
224
|
+
method: 'GET',
|
|
225
|
+
path: `/products/${productId}/target-audiences`,
|
|
226
|
+
});
|
|
227
|
+
return response.payload;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (subcommand === 'replace') {
|
|
231
|
+
const body = await readJsonInput(parsed);
|
|
232
|
+
const response = await requestAgentApi(parsed, {
|
|
233
|
+
method: 'PUT',
|
|
234
|
+
path: `/products/${productId}/target-audiences`,
|
|
235
|
+
body,
|
|
236
|
+
});
|
|
237
|
+
return response.payload;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw Object.assign(new Error(`Unknown products target-audiences subcommand: ${subcommand}.`), {
|
|
241
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
242
|
+
guidance:
|
|
243
|
+
'Run `outrank-cli products target-audiences --help` to see the available target-audience commands.',
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const handleGoogleSearchConsole = async (argv) => {
|
|
248
|
+
const subcommand = argv[0];
|
|
249
|
+
const parsed = parseArgs(argv.slice(1));
|
|
250
|
+
const wantsHelp =
|
|
251
|
+
!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || hasFlag(parsed, 'help');
|
|
252
|
+
|
|
253
|
+
if (wantsHelp) {
|
|
254
|
+
return cliSuccess(
|
|
255
|
+
{
|
|
256
|
+
usage: `Usage:
|
|
257
|
+
outrank-cli products gsc status <product-id>
|
|
258
|
+
outrank-cli products gsc connect <product-id>
|
|
259
|
+
outrank-cli products gsc poll <product-id> --attempt-id <attempt-id>
|
|
260
|
+
outrank-cli products gsc reconnect <product-id>
|
|
261
|
+
outrank-cli products gsc disconnect <product-id>
|
|
262
|
+
outrank-cli products gsc accounts <product-id>
|
|
263
|
+
outrank-cli products gsc properties <product-id> --email <email>
|
|
264
|
+
outrank-cli products gsc set-property <product-id> --email <email> --site-url <url>
|
|
265
|
+
outrank-cli products gsc performance <product-id> [--months <n>]
|
|
266
|
+
outrank-cli products gsc search-analytics <product-id> [--dimensions <csv>] [--limit <n>] [--months <n>] [--start-date <yyyy-mm-dd>] [--end-date <yyyy-mm-dd>] [--page <text>] [--query <text>] [--country <cc>] [--device <device>]
|
|
267
|
+
outrank-cli products gsc inspect-url <product-id> --url <url> [--language-code <code>]
|
|
268
|
+
outrank-cli products gsc cannibalization <product-id>`,
|
|
269
|
+
},
|
|
270
|
+
'Use connect plus poll for the OAuth handoff, then inspect accounts, properties, performance, indexing, and keyword data from the same subtree.',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const productId = getRequiredPositional(parsed, 0, '<product-id>');
|
|
275
|
+
const basePath = `/products/${productId}/gsc`;
|
|
276
|
+
|
|
277
|
+
if (subcommand === 'status') {
|
|
278
|
+
const response = await requestAgentApi(parsed, {
|
|
279
|
+
method: 'GET',
|
|
280
|
+
path: basePath,
|
|
281
|
+
});
|
|
282
|
+
return response.payload;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (subcommand === 'connect') {
|
|
286
|
+
const response = await requestAgentApi(parsed, {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
path: `${basePath}/connect`,
|
|
289
|
+
});
|
|
290
|
+
return response.payload;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (subcommand === 'poll') {
|
|
294
|
+
const attemptId = getOption(parsed, 'attempt-id');
|
|
295
|
+
|
|
296
|
+
if (!attemptId) {
|
|
297
|
+
throw Object.assign(new Error('Missing required option: --attempt-id <attempt-id>.'), {
|
|
298
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
299
|
+
guidance:
|
|
300
|
+
'Run `outrank-cli products gsc poll <product-id> --attempt-id <attempt-id>` after connect starts.',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const response = await requestAgentApi(parsed, {
|
|
305
|
+
method: 'GET',
|
|
306
|
+
path: buildQueryPath(`${basePath}/connect`, {
|
|
307
|
+
attempt_id: attemptId,
|
|
308
|
+
}),
|
|
309
|
+
});
|
|
310
|
+
return response.payload;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (subcommand === 'reconnect') {
|
|
314
|
+
const response = await requestAgentApi(parsed, {
|
|
315
|
+
method: 'POST',
|
|
316
|
+
path: `${basePath}/reconnect`,
|
|
317
|
+
});
|
|
318
|
+
return response.payload;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (subcommand === 'disconnect') {
|
|
322
|
+
const response = await requestAgentApi(parsed, {
|
|
323
|
+
method: 'DELETE',
|
|
324
|
+
path: basePath,
|
|
325
|
+
});
|
|
326
|
+
return response.payload;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (subcommand === 'accounts') {
|
|
330
|
+
const response = await requestAgentApi(parsed, {
|
|
331
|
+
method: 'GET',
|
|
332
|
+
path: `${basePath}/accounts`,
|
|
333
|
+
});
|
|
334
|
+
return response.payload;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (subcommand === 'properties') {
|
|
338
|
+
const email = getOption(parsed, 'email');
|
|
339
|
+
|
|
340
|
+
if (!email) {
|
|
341
|
+
throw Object.assign(new Error('Missing required option: --email <email>.'), {
|
|
342
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
343
|
+
guidance:
|
|
344
|
+
'Run `outrank-cli products gsc properties <product-id> --email <email>` to list properties for a connected Google account.',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const response = await requestAgentApi(parsed, {
|
|
349
|
+
method: 'GET',
|
|
350
|
+
path: buildQueryPath(`${basePath}/properties`, {
|
|
351
|
+
email,
|
|
352
|
+
}),
|
|
353
|
+
});
|
|
354
|
+
return response.payload;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (subcommand === 'set-property') {
|
|
358
|
+
const email = getOption(parsed, 'email');
|
|
359
|
+
const siteUrl = getOption(parsed, 'site-url');
|
|
360
|
+
|
|
361
|
+
if (!email || !siteUrl) {
|
|
362
|
+
throw Object.assign(new Error('Missing required options: --email <email> and --site-url <url>.'), {
|
|
363
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
364
|
+
guidance:
|
|
365
|
+
'Run `outrank-cli products gsc set-property <product-id> --email <email> --site-url <url>` to save the verified property.',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const response = await requestAgentApi(parsed, {
|
|
370
|
+
method: 'PUT',
|
|
371
|
+
path: `${basePath}/property`,
|
|
372
|
+
body: {
|
|
373
|
+
email,
|
|
374
|
+
site_url: siteUrl,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
return response.payload;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (subcommand === 'performance') {
|
|
381
|
+
const path = buildQueryPath(`${basePath}/performance`, {
|
|
382
|
+
months: parseIntegerOption(parsed, 'months', { min: 1, max: 16 }),
|
|
383
|
+
});
|
|
384
|
+
const response = await requestAgentApi(parsed, {
|
|
385
|
+
method: 'GET',
|
|
386
|
+
path,
|
|
387
|
+
});
|
|
388
|
+
return response.payload;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (subcommand === 'search-analytics') {
|
|
392
|
+
const path = buildQueryPath(`${basePath}/search-analytics`, {
|
|
393
|
+
dimensions: getOption(parsed, 'dimensions'),
|
|
394
|
+
limit: parseIntegerOption(parsed, 'limit', { min: 1, max: 1000 }),
|
|
395
|
+
months: parseIntegerOption(parsed, 'months', { min: 1, max: 16 }),
|
|
396
|
+
start_date: getOption(parsed, 'start-date'),
|
|
397
|
+
end_date: getOption(parsed, 'end-date'),
|
|
398
|
+
page: getOption(parsed, 'page'),
|
|
399
|
+
query: getOption(parsed, 'query'),
|
|
400
|
+
country: getOption(parsed, 'country'),
|
|
401
|
+
device: getOption(parsed, 'device'),
|
|
402
|
+
});
|
|
403
|
+
const response = await requestAgentApi(parsed, {
|
|
404
|
+
method: 'GET',
|
|
405
|
+
path,
|
|
406
|
+
});
|
|
407
|
+
return response.payload;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (subcommand === 'inspect-url') {
|
|
411
|
+
const url = getOption(parsed, 'url');
|
|
412
|
+
|
|
413
|
+
if (!url) {
|
|
414
|
+
throw Object.assign(new Error('Missing required option: --url <url>.'), {
|
|
415
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
416
|
+
guidance:
|
|
417
|
+
'Run `outrank-cli products gsc inspect-url <product-id> --url <url>` to inspect indexing details for one page.',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const response = await requestAgentApi(parsed, {
|
|
422
|
+
method: 'POST',
|
|
423
|
+
path: `${basePath}/url-inspection`,
|
|
424
|
+
body: {
|
|
425
|
+
inspection_url: url,
|
|
426
|
+
...(getOption(parsed, 'language-code') ? { language_code: getOption(parsed, 'language-code') } : {}),
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
return response.payload;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (subcommand === 'cannibalization') {
|
|
433
|
+
const response = await requestAgentApi(parsed, {
|
|
434
|
+
method: 'GET',
|
|
435
|
+
path: `${basePath}/cannibalization`,
|
|
436
|
+
});
|
|
437
|
+
return response.payload;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
throw Object.assign(new Error(`Unknown products gsc subcommand: ${subcommand}.`), {
|
|
441
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
442
|
+
guidance:
|
|
443
|
+
'Run `outrank-cli products gsc --help` to see the available Google Search Console commands.',
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
export const handleProductsCommand = async (argv) => {
|
|
448
|
+
const subcommand = argv[0];
|
|
449
|
+
const parsed = parseArgs(argv.slice(1));
|
|
450
|
+
const wantsHelp = !subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help';
|
|
451
|
+
|
|
452
|
+
if (wantsHelp) {
|
|
453
|
+
return cliSuccess(
|
|
454
|
+
{
|
|
455
|
+
usage: PRODUCTS_HELP.trim(),
|
|
456
|
+
},
|
|
457
|
+
'Choose one of: list, get, create, update, pause, resume, output-settings, target-audiences, competitors, gsc.',
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (subcommand === 'list') return handleProductsList(parsed);
|
|
462
|
+
if (subcommand === 'get') return handleProductsGet(parsed);
|
|
463
|
+
if (subcommand === 'create') return handleProductsCreate(parsed);
|
|
464
|
+
if (subcommand === 'update') return handleProductsUpdate(parsed);
|
|
465
|
+
if (subcommand === 'pause') return handleProductsPause(parsed);
|
|
466
|
+
if (subcommand === 'resume') return handleProductsResume(parsed);
|
|
467
|
+
if (subcommand === 'output-settings') return handleOutputSettings(argv.slice(1));
|
|
468
|
+
if (subcommand === 'target-audiences') return handleTargetAudiences(argv.slice(1));
|
|
469
|
+
if (subcommand === 'competitors') return handleCompetitors(argv.slice(1));
|
|
470
|
+
if (subcommand === 'gsc') return handleGoogleSearchConsole(argv.slice(1));
|
|
471
|
+
|
|
472
|
+
throw Object.assign(new Error(`Unknown products subcommand: ${subcommand}.`), {
|
|
473
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
474
|
+
guidance: 'Run `outrank-cli products --help` to see available product commands.',
|
|
475
|
+
});
|
|
476
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { hasFlag, parseArgs } from '../core/args.js';
|
|
2
|
+
import { requestAgentApi } from '../core/api.js';
|
|
3
|
+
import { cliSuccess } from '../core/errors.js';
|
|
4
|
+
|
|
5
|
+
export const SUBSCRIPTION_HELP = `Usage:
|
|
6
|
+
outrank-cli subscription status
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
status Return the current subscription, plan, trial, quota, and add-on status.
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
export const handleSubscriptionCommand = async (argv) => {
|
|
13
|
+
const subcommand = argv[0];
|
|
14
|
+
const parsed = parseArgs(argv.slice(1));
|
|
15
|
+
const wantsHelp =
|
|
16
|
+
!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || hasFlag(parsed, 'help');
|
|
17
|
+
|
|
18
|
+
if (wantsHelp) {
|
|
19
|
+
return cliSuccess(
|
|
20
|
+
{
|
|
21
|
+
usage: SUBSCRIPTION_HELP.trim(),
|
|
22
|
+
},
|
|
23
|
+
'Use `status` to inspect the current plan, quota, trial, and add-on state.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (subcommand === 'status') {
|
|
28
|
+
const response = await requestAgentApi(parsed, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
path: '/subscription/status',
|
|
31
|
+
});
|
|
32
|
+
return response.payload;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw Object.assign(new Error(`Unknown subscription subcommand: ${subcommand}.`), {
|
|
36
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
37
|
+
guidance: 'Run `outrank-cli subscription --help` to see available subscription commands.',
|
|
38
|
+
});
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { hasFlag, parseArgs } from '../core/args.js';
|
|
2
|
+
import { requestAgentApi } from '../core/api.js';
|
|
3
|
+
import { cliSuccess } from '../core/errors.js';
|
|
4
|
+
|
|
5
|
+
export const USAGE_HELP = `Usage:
|
|
6
|
+
outrank-cli usage stats
|
|
7
|
+
|
|
8
|
+
Commands:
|
|
9
|
+
stats Return current usage and quota metrics for articles, backlinks, free tools, and product slots.
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
export const handleUsageCommand = async (argv) => {
|
|
13
|
+
const subcommand = argv[0];
|
|
14
|
+
const parsed = parseArgs(argv.slice(1));
|
|
15
|
+
const wantsHelp =
|
|
16
|
+
!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help' || hasFlag(parsed, 'help');
|
|
17
|
+
|
|
18
|
+
if (wantsHelp) {
|
|
19
|
+
return cliSuccess(
|
|
20
|
+
{
|
|
21
|
+
usage: USAGE_HELP.trim(),
|
|
22
|
+
},
|
|
23
|
+
'Use `stats` to inspect current quota consumption and activity.',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (subcommand === 'stats') {
|
|
28
|
+
const response = await requestAgentApi(parsed, {
|
|
29
|
+
method: 'GET',
|
|
30
|
+
path: '/usage/stats',
|
|
31
|
+
});
|
|
32
|
+
return response.payload;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw Object.assign(new Error(`Unknown usage subcommand: ${subcommand}.`), {
|
|
36
|
+
cliCode: 'CLI_INVALID_USAGE',
|
|
37
|
+
guidance: 'Run `outrank-cli usage --help` to see available usage commands.',
|
|
38
|
+
});
|
|
39
|
+
};
|
package/src/core/api.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { cliError } from './errors.js';
|
|
2
|
+
import { resolveRuntimeConfig } from './config.js';
|
|
3
|
+
|
|
4
|
+
const API_BASE_PATH = '/api/agent/v1';
|
|
5
|
+
|
|
6
|
+
export const resolveApiContext = async (parsed, { requireAuth = true } = {}) => {
|
|
7
|
+
const runtime = await resolveRuntimeConfig(parsed);
|
|
8
|
+
if (requireAuth && !runtime.apiKey) {
|
|
9
|
+
throw Object.assign(new Error('No Outrank API key is configured.'), {
|
|
10
|
+
cliCode: 'CLI_AUTH_REQUIRED',
|
|
11
|
+
guidance:
|
|
12
|
+
'Run `outrank-cli auth login <api-key>` to store a key locally, or set OUTRANK_API_KEY in the environment before retrying.',
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return runtime;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const buildHeaders = ({ apiKey, idempotencyKey, hasJsonBody }) => {
|
|
19
|
+
const headers = {
|
|
20
|
+
Accept: 'application/json',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (apiKey) {
|
|
24
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
25
|
+
}
|
|
26
|
+
if (hasJsonBody) {
|
|
27
|
+
headers['Content-Type'] = 'application/json';
|
|
28
|
+
}
|
|
29
|
+
if (idempotencyKey) {
|
|
30
|
+
headers['Idempotency-Key'] = idempotencyKey;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return headers;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const requestAgentApi = async (
|
|
37
|
+
parsed,
|
|
38
|
+
{ method = 'GET', path, body, idempotencyKey, requireAuth = true } = {},
|
|
39
|
+
) => {
|
|
40
|
+
const runtime = await resolveApiContext(parsed, { requireAuth });
|
|
41
|
+
const url = new URL(`${API_BASE_PATH}${path}`, runtime.baseUrl);
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
method,
|
|
44
|
+
headers: buildHeaders({
|
|
45
|
+
apiKey: runtime.apiKey,
|
|
46
|
+
idempotencyKey,
|
|
47
|
+
hasJsonBody: body !== undefined,
|
|
48
|
+
}),
|
|
49
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
50
|
+
}).catch((error) => {
|
|
51
|
+
throw Object.assign(new Error(`Request failed: ${error.message}`), {
|
|
52
|
+
cliCode: 'CLI_NETWORK_ERROR',
|
|
53
|
+
guidance:
|
|
54
|
+
`The CLI could not reach ${runtime.baseUrl}. Confirm OUTRANK_BASE_URL, the user's network, and the Outrank deployment, then retry.`,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
59
|
+
let payload;
|
|
60
|
+
if (contentType.includes('application/json')) {
|
|
61
|
+
payload = await response.json();
|
|
62
|
+
} else {
|
|
63
|
+
const guidance =
|
|
64
|
+
response.status === 404
|
|
65
|
+
? `The host ${runtime.baseUrl} responded, but ${url.pathname} is not deployed there. Point OUTRANK_BASE_URL or --base-url at the Outrank app deployment that serves the agent API, or deploy the /api/agent/v1 routes to this host.`
|
|
66
|
+
: 'The CLI expects the agent API to return JSON. Check the base URL and server deployment.';
|
|
67
|
+
payload = cliError(
|
|
68
|
+
'CLI_NON_JSON_RESPONSE',
|
|
69
|
+
`Outrank returned HTTP ${response.status} without a JSON body.`,
|
|
70
|
+
guidance,
|
|
71
|
+
{ status: response.status, url: url.toString() },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
ok: response.ok,
|
|
77
|
+
status: response.status,
|
|
78
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
79
|
+
payload,
|
|
80
|
+
runtime,
|
|
81
|
+
};
|
|
82
|
+
};
|