@nuxtseo/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.
Files changed (44) hide show
  1. package/README.md +218 -0
  2. package/dist/api.d.ts +14 -0
  3. package/dist/api.js +43 -0
  4. package/dist/browser.d.ts +7 -0
  5. package/dist/browser.js +24 -0
  6. package/dist/cli-entry.d.ts +2 -0
  7. package/dist/cli-entry.js +41 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.js +232 -0
  10. package/dist/command-contract.d.ts +33 -0
  11. package/dist/command-contract.js +131 -0
  12. package/dist/commands.d.ts +8 -0
  13. package/dist/commands.js +626 -0
  14. package/dist/failures.d.ts +36 -0
  15. package/dist/failures.js +132 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.js +3 -0
  18. package/dist/pairing.d.ts +13 -0
  19. package/dist/pairing.js +100 -0
  20. package/dist/parse.d.ts +23 -0
  21. package/dist/parse.js +126 -0
  22. package/dist/render.d.ts +17 -0
  23. package/dist/render.js +156 -0
  24. package/dist/runtime.d.ts +18 -0
  25. package/dist/runtime.js +12 -0
  26. package/dist/site.d.ts +5 -0
  27. package/dist/site.js +48 -0
  28. package/dist/state/auth.d.ts +45 -0
  29. package/dist/state/auth.js +232 -0
  30. package/dist/state/config.d.ts +39 -0
  31. package/dist/state/config.js +145 -0
  32. package/dist/state/files.d.ts +6 -0
  33. package/dist/state/files.js +125 -0
  34. package/dist/state/index.d.ts +4 -0
  35. package/dist/state/index.js +4 -0
  36. package/dist/state/paths.d.ts +10 -0
  37. package/dist/state/paths.js +16 -0
  38. package/dist/state/result.d.ts +67 -0
  39. package/dist/state/result.js +6 -0
  40. package/dist/version.d.ts +1 -0
  41. package/dist/version.js +2 -0
  42. package/package.json +62 -0
  43. package/skills/nuxtseo-cli/SKILL.md +162 -0
  44. package/skills/nuxtseo-cli/references/protocol.md +64 -0
@@ -0,0 +1,626 @@
1
+ import * as prompts from '@clack/prompts';
2
+ import { defineCommand } from 'citty';
3
+ import { createApiClient, fromStateError, loadApiContext } from './api.js';
4
+ import { openBrowser } from './browser.js';
5
+ import { EXIT_CODE, fail, fromSdkFailure, ok } from './failures.js';
6
+ import { awaitPairingApproval, createCliPairingClient, startPairing } from './pairing.js';
7
+ import { parseAbsolutePageUrl, parseInteger } from './parse.js';
8
+ import { renderAction, renderActionResolution, renderActions, renderMentions, renderPageInspection, renderPageScan, renderPerformance, renderRecoverableBacklinks, renderSearchStatus, renderSites, renderUsage, } from './render.js';
9
+ import { writeCliResponse, writeDiagnostic, writeOutput, writeProtocolResponse } from './runtime.js';
10
+ import { resolveSite } from './site.js';
11
+ import { clearCredential, getCredentialStatus, readConfig, resolveApiUrl, saveCredential, updateConfig, } from './state/index.js';
12
+ import { VERSION } from './version.js';
13
+ function present(runtime, globals, result, render) {
14
+ if (result._tag === 'Err')
15
+ return fromSdkFailure(result.error);
16
+ if (globals.json)
17
+ writeProtocolResponse(runtime, result.value);
18
+ else
19
+ writeOutput(runtime, render(result.value));
20
+ return ok(undefined);
21
+ }
22
+ async function withSpinner(runtime, message, task) {
23
+ const spinner = runtime.interactive
24
+ ? prompts.spinner({ output: runtime.error, signal: runtime.signal })
25
+ : null;
26
+ spinner?.start(message);
27
+ return task().then((result) => {
28
+ spinner?.stop();
29
+ return result;
30
+ }, (cause) => {
31
+ spinner?.error('Request failed');
32
+ return Promise.reject(cause);
33
+ });
34
+ }
35
+ async function apiAndSite(runtime, globals) {
36
+ const api = await loadApiContext(runtime, globals);
37
+ if (api._tag === 'Err')
38
+ return api;
39
+ const site = await resolveSite(api.value, runtime, globals);
40
+ return site._tag === 'Err'
41
+ ? site
42
+ : ok({ api: api.value, siteId: site.value });
43
+ }
44
+ async function confirmMutation(runtime, globals, message) {
45
+ if (globals.yes)
46
+ return ok(true);
47
+ if (!runtime.interactive) {
48
+ return fail(EXIT_CODE.invalidInput, 'confirmation_required: This mutation requires `--yes` outside an interactive terminal.', undefined, 'confirmation_required');
49
+ }
50
+ const confirmed = await prompts.confirm({
51
+ message,
52
+ input: runtime.input,
53
+ output: runtime.error,
54
+ signal: runtime.signal,
55
+ initialValue: false,
56
+ });
57
+ if (prompts.isCancel(confirmed))
58
+ return fail(EXIT_CODE.interrupted, 'interrupted: Mutation cancelled.');
59
+ return ok(confirmed);
60
+ }
61
+ async function readLoginToken(runtime) {
62
+ if (runtime.interactive) {
63
+ const token = await prompts.password({
64
+ message: 'Team API token',
65
+ mask: '*',
66
+ input: runtime.input,
67
+ output: runtime.error,
68
+ signal: runtime.signal,
69
+ validate: value => typeof value === 'string' && value.trim() ? undefined : 'Token is required.',
70
+ });
71
+ if (prompts.isCancel(token))
72
+ return fail(EXIT_CODE.interrupted, 'interrupted: Login cancelled.');
73
+ return ok(token.trim());
74
+ }
75
+ if (runtime.inputIsTTY) {
76
+ return fail(EXIT_CODE.invalidInput, 'token_input_required: Pipe the token to `nuxtseo login`; tokens are never accepted as command arguments.', undefined, 'token_input_required');
77
+ }
78
+ const token = (await runtime.readStdin()).trim();
79
+ return token
80
+ ? ok(token)
81
+ : fail(EXIT_CODE.invalidInput, 'token_input_required: No token was received on stdin.', undefined, 'token_input_required');
82
+ }
83
+ /**
84
+ * Browser pairing: start a request, show the code, open the browser, then wait.
85
+ *
86
+ * The user compares the code on screen with the code printed here, so both are
87
+ * on stderr where the human output lives. Nothing is granted until they approve.
88
+ */
89
+ async function pairInBrowser(runtime, apiUrl, options) {
90
+ const client = createCliPairingClient(apiUrl);
91
+ const pairing = await startPairing(client, runtime);
92
+ if (pairing._tag === 'Err')
93
+ return pairing;
94
+ const opened = options.openBrowser ? openBrowser(pairing.value.verificationUrl) : false;
95
+ writeDiagnostic(runtime, [
96
+ `Pairing code: ${pairing.value.pairingCode}`,
97
+ opened
98
+ ? `Approve this login in your browser: ${pairing.value.verificationUrl}`
99
+ : `Open this URL to approve the login: ${pairing.value.verificationUrl}`,
100
+ 'The code above must match the one shown in the browser.',
101
+ ].join('\n'));
102
+ const spinner = runtime.interactive
103
+ ? prompts.spinner({ output: runtime.error, signal: runtime.signal })
104
+ : null;
105
+ spinner?.start('Waiting for approval');
106
+ const approved = await awaitPairingApproval(client, runtime, pairing.value);
107
+ spinner?.stop(approved._tag === 'Ok' ? 'Approved' : 'Not approved');
108
+ return approved._tag === 'Err' ? approved : ok(approved.value.token);
109
+ }
110
+ async function login(runtime, globals, options) {
111
+ const apiUrl = await resolveApiUrl({ apiUrl: globals.apiUrl, env: runtime.env, paths: runtime.paths });
112
+ if (apiUrl._tag === 'Err')
113
+ return fromStateError(apiUrl.error);
114
+ // Pairing needs a browser and a person. Without an interactive terminal the
115
+ // old paste path is the only one that can complete, so CI keeps working
116
+ // unchanged.
117
+ const token = options.withToken || !runtime.interactive
118
+ ? await readLoginToken(runtime)
119
+ : await pairInBrowser(runtime, apiUrl.value.apiUrl, { openBrowser: !options.noBrowser });
120
+ if (token._tag === 'Err')
121
+ return token;
122
+ const client = createApiClient(apiUrl.value.apiUrl, token.value);
123
+ const validated = await withSpinner(runtime, 'Validating token', () => client.sites.list({}, { signal: runtime.requestSignal }));
124
+ if (validated._tag === 'Err')
125
+ return fromSdkFailure(validated.error);
126
+ const saved = await saveCredential(token.value, { paths: runtime.paths });
127
+ if (saved._tag === 'Err')
128
+ return fromStateError(saved.error);
129
+ if (saved.value.warning)
130
+ writeDiagnostic(runtime, `warning: ${saved.value.warning.message}`);
131
+ if (globals.apiUrl) {
132
+ const configured = await updateConfig({ apiUrl: apiUrl.value.apiUrl }, { paths: runtime.paths });
133
+ if (configured._tag === 'Err') {
134
+ writeDiagnostic(runtime, 'warning: The credential was stored, but the API URL could not be saved.');
135
+ return fromStateError(configured.error);
136
+ }
137
+ }
138
+ if (runtime.env.NUXTSEO_TOKEN !== undefined) {
139
+ writeDiagnostic(runtime, 'warning: NUXTSEO_TOKEN remains active and takes precedence over the stored credential.');
140
+ }
141
+ if (globals.json)
142
+ writeProtocolResponse(runtime, validated.value);
143
+ else
144
+ writeOutput(runtime, `Logged in. ${validated.value.data.sites.length} Sites accessible. Credential: ${saved.value.storage}.`);
145
+ return ok(undefined);
146
+ }
147
+ async function logout(runtime, globals) {
148
+ const cleared = await clearCredential({ paths: runtime.paths });
149
+ if (cleared._tag === 'Err')
150
+ return fromStateError(cleared.error);
151
+ if (globals.json) {
152
+ writeCliResponse(runtime, {
153
+ _tag: 'CliLogout',
154
+ schemaVersion: 1,
155
+ removed: cleared.value.removed,
156
+ });
157
+ }
158
+ else {
159
+ writeOutput(runtime, cleared.value.removed ? 'Local credential removed.' : 'No stored credential.');
160
+ }
161
+ writeDiagnostic(runtime, 'Token revocation remains in the NuxtSEO dashboard. NUXTSEO_TOKEN, when set, is unchanged.');
162
+ return ok(undefined);
163
+ }
164
+ async function whoami(runtime, globals) {
165
+ const api = await loadApiContext(runtime, globals);
166
+ if (api._tag === 'Err')
167
+ return api;
168
+ const listed = await withSpinner(runtime, 'Checking authentication', () => api.value.client.sites.list({}, { signal: runtime.requestSignal }));
169
+ if (listed._tag === 'Err')
170
+ return fromSdkFailure(listed.error);
171
+ if (globals.json) {
172
+ writeProtocolResponse(runtime, listed.value);
173
+ }
174
+ else {
175
+ writeOutput(runtime, [
176
+ 'Authenticated Team API token.',
177
+ `API: ${api.value.apiUrl}`,
178
+ `Credential: ${api.value.credentialSource}`,
179
+ `Accessible Sites: ${listed.value.data.sites.length}`,
180
+ `Selected Site: ${api.value.config.siteId ?? 'none'}`,
181
+ ].join('\n'));
182
+ }
183
+ return ok(undefined);
184
+ }
185
+ async function config(runtime, globals) {
186
+ const [stored, apiUrl, auth] = await Promise.all([
187
+ readConfig({ paths: runtime.paths }),
188
+ resolveApiUrl({ apiUrl: globals.apiUrl, env: runtime.env, paths: runtime.paths }),
189
+ getCredentialStatus({ env: runtime.env, paths: runtime.paths }),
190
+ ]);
191
+ if (stored._tag === 'Err')
192
+ return fromStateError(stored.error);
193
+ if (apiUrl._tag === 'Err')
194
+ return fromStateError(apiUrl.error);
195
+ if (auth._tag === 'Err')
196
+ return fromStateError(auth.error);
197
+ if (globals.json) {
198
+ writeCliResponse(runtime, {
199
+ _tag: 'CliConfig',
200
+ schemaVersion: 1,
201
+ path: runtime.paths.configFile,
202
+ api: { url: apiUrl.value.apiUrl, source: apiUrl.value.source },
203
+ siteId: stored.value.siteId ?? null,
204
+ authentication: auth.value,
205
+ });
206
+ return ok(undefined);
207
+ }
208
+ if (!runtime.interactive) {
209
+ writeOutput(runtime, [
210
+ `Config: ${runtime.paths.configFile}`,
211
+ `API: ${apiUrl.value.apiUrl} (${apiUrl.value.source})`,
212
+ `Selected Site: ${stored.value.siteId ?? 'none'}`,
213
+ `Authentication: ${auth.value.authenticated ? auth.value.source : 'not configured'}`,
214
+ ].join('\n'));
215
+ return ok(undefined);
216
+ }
217
+ const action = await prompts.select({
218
+ message: 'Configuration',
219
+ options: [
220
+ { value: 'api', label: 'Set API host', hint: apiUrl.value.apiUrl },
221
+ { value: 'production', label: 'Use production API host' },
222
+ { value: 'clear-site', label: 'Clear selected Site', hint: stored.value.siteId ?? 'none selected' },
223
+ { value: 'done', label: 'Done' },
224
+ ],
225
+ input: runtime.input,
226
+ output: runtime.error,
227
+ signal: runtime.signal,
228
+ });
229
+ if (prompts.isCancel(action) || action === 'done')
230
+ return ok(undefined);
231
+ if (action === 'api') {
232
+ const next = await prompts.text({
233
+ message: 'API host',
234
+ initialValue: stored.value.apiUrl ?? apiUrl.value.apiUrl,
235
+ input: runtime.input,
236
+ output: runtime.error,
237
+ signal: runtime.signal,
238
+ });
239
+ if (prompts.isCancel(next))
240
+ return fail(EXIT_CODE.interrupted, 'interrupted: Configuration cancelled.');
241
+ const written = await updateConfig({ apiUrl: next }, { paths: runtime.paths });
242
+ if (written._tag === 'Err')
243
+ return fromStateError(written.error);
244
+ writeOutput(runtime, `API host saved: ${written.value.apiUrl}`);
245
+ return ok(undefined);
246
+ }
247
+ const written = await updateConfig(action === 'production' ? { apiUrl: undefined } : { siteId: undefined }, { paths: runtime.paths });
248
+ if (written._tag === 'Err')
249
+ return fromStateError(written.error);
250
+ writeOutput(runtime, action === 'production' ? 'Production API host restored.' : 'Selected Site cleared.');
251
+ return ok(undefined);
252
+ }
253
+ async function sitesList(runtime, globals) {
254
+ const api = await loadApiContext(runtime, globals);
255
+ if (api._tag === 'Err')
256
+ return api;
257
+ const listed = await withSpinner(runtime, 'Loading Sites', () => api.value.client.sites.list({}, { signal: runtime.requestSignal }));
258
+ return present(runtime, globals, listed, response => renderSites(response, api.value.config.siteId));
259
+ }
260
+ async function sitesUse(runtime, globals, siteId) {
261
+ const api = await loadApiContext(runtime, globals);
262
+ if (api._tag === 'Err')
263
+ return api;
264
+ const listed = await withSpinner(runtime, 'Checking Site access', () => api.value.client.sites.list({}, { signal: runtime.requestSignal }));
265
+ if (listed._tag === 'Err')
266
+ return fromSdkFailure(listed.error);
267
+ const site = listed.value.data.sites.find(candidate => candidate.id === siteId);
268
+ if (!site) {
269
+ return fail(EXIT_CODE.notFound, `site_not_accessible: ${siteId} is not accessible to this token.`, undefined, 'site_not_accessible');
270
+ }
271
+ const saved = await updateConfig({ siteId }, { paths: runtime.paths });
272
+ if (saved._tag === 'Err')
273
+ return fromStateError(saved.error);
274
+ if (globals.json) {
275
+ writeCliResponse(runtime, {
276
+ _tag: 'CliSiteSelection',
277
+ schemaVersion: 1,
278
+ site: {
279
+ id: site.id,
280
+ name: site.name ?? null,
281
+ url: site.url ?? null,
282
+ },
283
+ });
284
+ }
285
+ else {
286
+ writeOutput(runtime, `Selected Site: ${site.name ?? site.url ?? site.id} (${site.id})`);
287
+ }
288
+ return ok(undefined);
289
+ }
290
+ async function usage(runtime, globals, group) {
291
+ const api = await loadApiContext(runtime, globals);
292
+ if (api._tag === 'Err')
293
+ return api;
294
+ const response = await withSpinner(runtime, 'Loading usage', () => api.value.client.account.usage({ query: { group } }, { signal: runtime.requestSignal }));
295
+ return present(runtime, globals, response, renderUsage);
296
+ }
297
+ async function actionsList(runtime, globals, limitInput, offsetInput) {
298
+ const limit = parseInteger(limitInput, { name: '--limit', minimum: 1, maximum: 25, defaultValue: 10 });
299
+ if (limit._tag === 'Err')
300
+ return limit;
301
+ const offset = parseInteger(offsetInput, { name: '--offset', minimum: 0, defaultValue: 0 });
302
+ if (offset._tag === 'Err')
303
+ return offset;
304
+ const resolved = await apiAndSite(runtime, globals);
305
+ if (resolved._tag === 'Err')
306
+ return resolved;
307
+ const response = await withSpinner(runtime, 'Loading next actions', () => resolved.value.api.client.actions.list({
308
+ params: { siteId: resolved.value.siteId },
309
+ query: { limit: limit.value, offset: offset.value },
310
+ }, { signal: runtime.requestSignal }));
311
+ return present(runtime, globals, response, value => renderActions(value.data));
312
+ }
313
+ async function actionsShow(runtime, globals, args) {
314
+ const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 100, defaultValue: 50 });
315
+ if (limit._tag === 'Err')
316
+ return limit;
317
+ const resolved = await apiAndSite(runtime, globals);
318
+ if (resolved._tag === 'Err')
319
+ return resolved;
320
+ const response = await withSpinner(runtime, 'Loading action', () => resolved.value.api.client.actions.show({
321
+ params: { siteId: resolved.value.siteId, actionId: args.actionId },
322
+ query: { groupId: args.groupId, cursor: args.cursor, limit: limit.value },
323
+ }, { signal: runtime.requestSignal }));
324
+ return present(runtime, globals, response, value => renderAction(value.data));
325
+ }
326
+ async function actionsResolve(runtime, globals, actionId) {
327
+ const resolved = await apiAndSite(runtime, globals);
328
+ if (resolved._tag === 'Err')
329
+ return resolved;
330
+ const shown = await withSpinner(runtime, 'Loading action evidence', () => resolved.value.api.client.actions.show({
331
+ params: { siteId: resolved.value.siteId, actionId },
332
+ query: { limit: 50 },
333
+ }, { signal: runtime.requestSignal }));
334
+ if (shown._tag === 'Err')
335
+ return fromSdkFailure(shown.error);
336
+ const confirmed = await confirmMutation(runtime, globals, `Resolve ${actionId}: ${shown.value.data.diagnosis}?`);
337
+ if (confirmed._tag === 'Err')
338
+ return confirmed;
339
+ if (!confirmed.value) {
340
+ writeDiagnostic(runtime, 'Mutation cancelled.');
341
+ return ok(undefined);
342
+ }
343
+ const response = await withSpinner(runtime, 'Resolving action', () => resolved.value.api.client.actions.resolve({
344
+ params: { siteId: resolved.value.siteId, actionId },
345
+ body: { artifactVersion: shown.value.data.artifactVersion },
346
+ }, { signal: runtime.requestSignal }));
347
+ return present(runtime, globals, response, value => renderActionResolution(value.data));
348
+ }
349
+ async function pageInspect(runtime, globals, args) {
350
+ const url = parseAbsolutePageUrl(args.url);
351
+ if (url._tag === 'Err')
352
+ return url;
353
+ const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 200, defaultValue: 100 });
354
+ if (limit._tag === 'Err')
355
+ return limit;
356
+ const offset = parseInteger(args.offset, { name: '--offset', minimum: 0, defaultValue: 0 });
357
+ if (offset._tag === 'Err')
358
+ return offset;
359
+ const resolved = await apiAndSite(runtime, globals);
360
+ if (resolved._tag === 'Err')
361
+ return resolved;
362
+ const response = await withSpinner(runtime, 'Inspecting Page', () => resolved.value.api.client.pages.inspect({
363
+ params: { siteId: resolved.value.siteId },
364
+ query: {
365
+ url: url.value,
366
+ includeResolved: args.includeResolved ? '1' : '0',
367
+ limit: limit.value,
368
+ offset: offset.value,
369
+ },
370
+ }, { signal: runtime.requestSignal }));
371
+ return present(runtime, globals, response, value => renderPageInspection(value.data));
372
+ }
373
+ async function pageScan(runtime, globals, rawUrl) {
374
+ const url = parseAbsolutePageUrl(rawUrl);
375
+ if (url._tag === 'Err')
376
+ return url;
377
+ const resolved = await apiAndSite(runtime, globals);
378
+ if (resolved._tag === 'Err')
379
+ return resolved;
380
+ const confirmed = await confirmMutation(runtime, globals, `Start mobile and desktop scans for ${url.value}?`);
381
+ if (confirmed._tag === 'Err')
382
+ return confirmed;
383
+ if (!confirmed.value) {
384
+ writeDiagnostic(runtime, 'Mutation cancelled.');
385
+ return ok(undefined);
386
+ }
387
+ const response = await withSpinner(runtime, 'Starting Page scans', () => resolved.value.api.client.pages.scan({
388
+ params: { siteId: resolved.value.siteId },
389
+ body: { url: url.value },
390
+ }, { signal: runtime.requestSignal }));
391
+ return present(runtime, globals, response, value => renderPageScan(value.data));
392
+ }
393
+ async function performance(runtime, globals) {
394
+ const resolved = await apiAndSite(runtime, globals);
395
+ if (resolved._tag === 'Err')
396
+ return resolved;
397
+ const response = await withSpinner(runtime, 'Loading performance', () => resolved.value.api.client.performance.overview({
398
+ params: { siteId: resolved.value.siteId },
399
+ }, { signal: runtime.requestSignal }));
400
+ return present(runtime, globals, response, value => renderPerformance(value.data));
401
+ }
402
+ async function searchStatus(runtime, globals) {
403
+ const resolved = await apiAndSite(runtime, globals);
404
+ if (resolved._tag === 'Err')
405
+ return resolved;
406
+ const response = await withSpinner(runtime, 'Loading Search Console status', () => resolved.value.api.client.search.readStatus({
407
+ params: { siteId: resolved.value.siteId },
408
+ }, { signal: runtime.requestSignal }));
409
+ return present(runtime, globals, response, value => renderSearchStatus(value.data));
410
+ }
411
+ async function backlinksRecoverable(runtime, globals, limitInput, offsetInput) {
412
+ const limit = parseInteger(limitInput, { name: '--limit', minimum: 1, maximum: 200, defaultValue: 100 });
413
+ if (limit._tag === 'Err')
414
+ return limit;
415
+ const offset = parseInteger(offsetInput, { name: '--offset', minimum: 0, defaultValue: 0 });
416
+ if (offset._tag === 'Err')
417
+ return offset;
418
+ const resolved = await apiAndSite(runtime, globals);
419
+ if (resolved._tag === 'Err')
420
+ return resolved;
421
+ const response = await withSpinner(runtime, 'Loading recoverable Backlinks', () => resolved.value.api.client.backlinks.recoverable({
422
+ params: { siteId: resolved.value.siteId },
423
+ query: { limit: limit.value, offset: offset.value },
424
+ }, { signal: runtime.requestSignal }));
425
+ return present(runtime, globals, response, value => renderRecoverableBacklinks(value.data));
426
+ }
427
+ async function mentionsList(runtime, globals, args) {
428
+ const limit = parseInteger(args.limit, { name: '--limit', minimum: 1, maximum: 200, defaultValue: 100 });
429
+ if (limit._tag === 'Err')
430
+ return limit;
431
+ const resolved = await apiAndSite(runtime, globals);
432
+ if (resolved._tag === 'Err')
433
+ return resolved;
434
+ const response = await withSpinner(runtime, 'Loading Mentions', () => resolved.value.api.client.mentions.list({
435
+ params: { siteId: resolved.value.siteId },
436
+ query: { limit: limit.value, includeFiltered: args.includeFiltered ? '1' : '0' },
437
+ }, { signal: runtime.requestSignal }));
438
+ return present(runtime, globals, response, value => renderMentions(value.data));
439
+ }
440
+ function capture(execution, task, positionalArguments, expectedPositionals) {
441
+ return async () => {
442
+ if (positionalArguments.length > expectedPositionals) {
443
+ execution.result = fail(EXIT_CODE.invalidInput, `invalid_cli_input: Unexpected argument ${JSON.stringify(positionalArguments[expectedPositionals])}.`);
444
+ return;
445
+ }
446
+ execution.result = await task();
447
+ };
448
+ }
449
+ function positional(name, description) {
450
+ return { type: 'positional', required: true, valueHint: name, description };
451
+ }
452
+ export function createRootCommand(runtime, globals, execution) {
453
+ const sites = defineCommand({
454
+ meta: { name: 'sites', description: 'List and select Sites' },
455
+ subCommands: {
456
+ list: defineCommand({
457
+ meta: { name: 'list', description: 'List accessible Sites' },
458
+ run: ({ args }) => capture(execution, () => sitesList(runtime, globals), args._, 0)(),
459
+ }),
460
+ use: defineCommand({
461
+ meta: { name: 'use', description: 'Select the Site used by Site scoped commands' },
462
+ args: { siteId: positional('site-id', 'Accessible Site ID') },
463
+ run: ({ args }) => capture(execution, () => sitesUse(runtime, globals, args.siteId), args._, 1)(),
464
+ }),
465
+ },
466
+ });
467
+ const actions = defineCommand({
468
+ meta: { name: 'actions', description: 'Read and resolve next actions' },
469
+ subCommands: {
470
+ list: defineCommand({
471
+ meta: { name: 'list', description: 'List server ranked next actions' },
472
+ args: {
473
+ limit: { type: 'string', description: 'Maximum actions, 1 to 25' },
474
+ offset: { type: 'string', description: 'Pagination offset' },
475
+ },
476
+ run: ({ args }) => capture(execution, () => actionsList(runtime, globals, args.limit, args.offset), args._, 0)(),
477
+ }),
478
+ show: defineCommand({
479
+ meta: { name: 'show', description: 'Show one action and its evidence' },
480
+ args: {
481
+ 'actionId': positional('action-id', 'Action ID'),
482
+ 'group-id': { type: 'string', description: 'Evidence group ID' },
483
+ 'cursor': { type: 'string', description: 'Evidence cursor' },
484
+ 'limit': { type: 'string', description: 'Maximum evidence items, 1 to 100' },
485
+ },
486
+ run: ({ args }) => capture(execution, () => actionsShow(runtime, globals, {
487
+ actionId: args.actionId,
488
+ groupId: args['group-id'],
489
+ cursor: args.cursor,
490
+ limit: args.limit,
491
+ }), args._, 1)(),
492
+ }),
493
+ resolve: defineCommand({
494
+ meta: { name: 'resolve', description: 'Claim an action and start server verification' },
495
+ args: { actionId: positional('action-id', 'Action ID') },
496
+ run: ({ args }) => capture(execution, () => actionsResolve(runtime, globals, args.actionId), args._, 1)(),
497
+ }),
498
+ },
499
+ });
500
+ const page = defineCommand({
501
+ meta: { name: 'page', description: 'Inspect and scan Pages' },
502
+ subCommands: {
503
+ inspect: defineCommand({
504
+ meta: { name: 'inspect', description: 'Read stored Page observations' },
505
+ args: {
506
+ 'url': positional('url', 'Absolute Page URL'),
507
+ 'include-resolved': { type: 'boolean', description: 'Include resolved observations' },
508
+ 'limit': { type: 'string', description: 'Maximum observations, 1 to 200' },
509
+ 'offset': { type: 'string', description: 'Pagination offset' },
510
+ },
511
+ run: ({ args }) => capture(execution, () => pageInspect(runtime, globals, {
512
+ url: args.url,
513
+ includeResolved: args['include-resolved'],
514
+ limit: args.limit,
515
+ offset: args.offset,
516
+ }), args._, 1)(),
517
+ }),
518
+ scan: defineCommand({
519
+ meta: { name: 'scan', description: 'Start mobile and desktop Page scans' },
520
+ args: { url: positional('url', 'Absolute Page URL') },
521
+ run: ({ args }) => capture(execution, () => pageScan(runtime, globals, args.url), args._, 1)(),
522
+ }),
523
+ },
524
+ });
525
+ const backlinks = defineCommand({
526
+ meta: { name: 'backlinks', description: 'Read stored inbound link evidence' },
527
+ subCommands: {
528
+ recoverable: defineCommand({
529
+ meta: { name: 'recoverable', description: 'List inbound links whose destination is broken' },
530
+ args: {
531
+ limit: { type: 'string', description: 'Maximum links, 1 to 200' },
532
+ offset: { type: 'string', description: 'Pagination offset' },
533
+ },
534
+ run: ({ args }) => capture(execution, () => backlinksRecoverable(runtime, globals, args.limit, args.offset), args._, 0)(),
535
+ }),
536
+ },
537
+ });
538
+ const mentions = defineCommand({
539
+ meta: { name: 'mentions', description: 'Read pages that name the Site without linking to it' },
540
+ subCommands: {
541
+ list: defineCommand({
542
+ meta: { name: 'list', description: 'List stored Mentions, newest first' },
543
+ args: {
544
+ 'limit': { type: 'string', description: 'Maximum Mentions, 1 to 200' },
545
+ 'include-filtered': { type: 'boolean', description: 'Keep Mentions AI triage marked a false positive' },
546
+ },
547
+ run: ({ args }) => capture(execution, () => mentionsList(runtime, globals, {
548
+ limit: args.limit,
549
+ includeFiltered: args['include-filtered'],
550
+ }), args._, 0)(),
551
+ }),
552
+ },
553
+ });
554
+ const search = defineCommand({
555
+ meta: { name: 'search', description: 'Read Search Console state' },
556
+ subCommands: {
557
+ status: defineCommand({
558
+ meta: { name: 'status', description: 'Read Search Console status' },
559
+ run: ({ args }) => capture(execution, () => searchStatus(runtime, globals), args._, 0)(),
560
+ }),
561
+ },
562
+ });
563
+ return defineCommand({
564
+ meta: {
565
+ name: 'nuxtseo',
566
+ version: VERSION,
567
+ description: 'Use NuxtSEO from a terminal or automation',
568
+ },
569
+ args: {
570
+ 'api-url': { type: 'string', description: 'Override the NuxtSEO API host' },
571
+ 'site': { type: 'string', description: 'Override the selected Site' },
572
+ 'json': { type: 'boolean', description: 'Write machine JSON; protocol envelopes remain unchanged' },
573
+ 'no-input': { type: 'boolean', description: 'Disable every interactive prompt' },
574
+ 'timeout-ms': { type: 'string', description: 'Request deadline in milliseconds, 1 to 300000' },
575
+ 'yes': { type: 'boolean', alias: 'y', description: 'Confirm a mutation without prompting' },
576
+ },
577
+ subCommands: {
578
+ login: defineCommand({
579
+ meta: { name: 'login', description: 'Approve this machine in a browser and store the Team API token it mints' },
580
+ args: {
581
+ 'with-token': { type: 'boolean', description: 'Paste an existing Team API token instead of pairing' },
582
+ // `--no-browser` negates this. The option contract derives a negated
583
+ // boolean from the `no-` prefix, so the definition is the positive.
584
+ 'browser': { type: 'boolean', default: true, description: 'Open a browser for approval; --no-browser prints the URL' },
585
+ },
586
+ run: ({ args }) => capture(execution, () => login(runtime, globals, {
587
+ withToken: args['with-token'],
588
+ noBrowser: args.browser === false,
589
+ }), args._, 0)(),
590
+ }),
591
+ logout: defineCommand({
592
+ meta: { name: 'logout', description: 'Remove the local credential' },
593
+ run: ({ args }) => capture(execution, () => logout(runtime, globals), args._, 0)(),
594
+ }),
595
+ whoami: defineCommand({
596
+ meta: { name: 'whoami', description: 'Validate and describe the active credential' },
597
+ run: ({ args }) => capture(execution, () => whoami(runtime, globals), args._, 0)(),
598
+ }),
599
+ config: defineCommand({
600
+ meta: { name: 'config', description: 'Show or edit local configuration' },
601
+ run: ({ args }) => capture(execution, () => config(runtime, globals), args._, 0)(),
602
+ }),
603
+ sites,
604
+ usage: defineCommand({
605
+ meta: { name: 'usage', description: 'Read account usage' },
606
+ args: {
607
+ group: {
608
+ type: 'enum',
609
+ options: ['integrations', 'compute', 'capacity'],
610
+ description: 'Filter meter group',
611
+ },
612
+ },
613
+ run: ({ args }) => capture(execution, () => usage(runtime, globals, args.group), args._, 0)(),
614
+ }),
615
+ actions,
616
+ backlinks,
617
+ mentions,
618
+ page,
619
+ performance: defineCommand({
620
+ meta: { name: 'performance', description: 'Read the stored Site performance overview' },
621
+ run: ({ args }) => capture(execution, () => performance(runtime, globals), args._, 0)(),
622
+ }),
623
+ search,
624
+ },
625
+ });
626
+ }
@@ -0,0 +1,36 @@
1
+ import type { SdkFailure } from '@nuxtseo/sdk';
2
+ export declare const EXIT_CODE: {
3
+ readonly success: 0;
4
+ readonly invalidInput: 2;
5
+ readonly authentication: 3;
6
+ readonly authorization: 4;
7
+ readonly conflict: 5;
8
+ readonly retryable: 6;
9
+ readonly infrastructure: 7;
10
+ readonly notFound: 8;
11
+ readonly interrupted: 130;
12
+ };
13
+ export type ExitCode = typeof EXIT_CODE[keyof typeof EXIT_CODE];
14
+ export type LocalFailureCode = 'authentication_required' | 'authorization_required' | 'confirmation_required' | 'conflict' | 'contract_violation' | 'infrastructure_failure' | 'interrupted' | 'invalid_cli_input' | 'not_found' | 'request_timeout' | 'retryable_failure' | 'site_ambiguous' | 'site_empty' | 'site_not_accessible' | 'state_error' | 'token_input_required' | 'transport_failure' | 'unexpected';
15
+ export type CliFailureCode = LocalFailureCode | Extract<SdkFailure, {
16
+ _tag: 'ApiFailure';
17
+ }>['code'];
18
+ export interface CliFailure {
19
+ _tag: 'CliFailure';
20
+ code: CliFailureCode;
21
+ exitCode: ExitCode;
22
+ message: string;
23
+ cause?: unknown;
24
+ protocolResponse?: unknown;
25
+ }
26
+ export type CliResult<T> = {
27
+ _tag: 'Ok';
28
+ value: T;
29
+ } | {
30
+ _tag: 'Err';
31
+ error: CliFailure;
32
+ };
33
+ export declare function ok<T>(value: T): CliResult<T>;
34
+ export declare function fail(exitCode: ExitCode, message: string, cause?: unknown, code?: CliFailureCode): CliResult<never>;
35
+ export declare function fromSdkFailure(error: SdkFailure): CliResult<never>;
36
+ export declare function unexpectedFailure(cause: unknown): CliFailure;