@nuxtseo/cli 0.1.4 → 0.2.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 CHANGED
@@ -71,9 +71,14 @@ Outside an interactive terminal, `login` reads a token from stdin instead of
71
71
  pairing, so CI keeps working. Tokens are never accepted as positional arguments
72
72
  or options.
73
73
 
74
- The CLI validates the token with a public Site read, then stores it in the OS
75
- keychain through optional `@napi-rs/keyring`. When keychain support is
76
- unavailable, it warns on stderr and uses `~/.nuxtseo/auth.json` with mode `0600`.
74
+ The CLI validates the token with `GET /api/v1/account/token`, which reports the
75
+ Team, the role, the granted scopes, and the expiry without reading a Site. It
76
+ then stores the token in the OS keychain through optional `@napi-rs/keyring`.
77
+ When keychain support is unavailable, it warns on stderr and uses
78
+ `~/.nuxtseo/auth.json` with mode `0600`.
79
+
80
+ `nuxtseo whoami` reads the same operation, so an agent learns about a missing
81
+ scope before the request that needs it.
77
82
 
78
83
  `nuxtseo logout` removes the local credential. Revoke the token from the
79
84
  NuxtSEO dashboard when it must stop working everywhere. An environment token
@@ -164,75 +169,151 @@ nuxtseo config
164
169
  nuxtseo sites list
165
170
  nuxtseo sites use <site-id>
166
171
  nuxtseo usage
172
+ nuxtseo status
167
173
  nuxtseo actions list
168
174
  nuxtseo actions show <action-id>
169
175
  nuxtseo actions resolve <action-id>
176
+ nuxtseo actions dismiss <action-id>
177
+ nuxtseo backlinks summary
178
+ nuxtseo backlinks referring-domains
179
+ nuxtseo backlinks anchors
180
+ nuxtseo backlinks history
170
181
  nuxtseo backlinks recoverable
171
182
  nuxtseo mentions list
172
183
  nuxtseo page inspect <url>
184
+ nuxtseo page issues
173
185
  nuxtseo page scan <url>
174
186
  nuxtseo performance
187
+ nuxtseo vitals summary
188
+ nuxtseo vitals trend
189
+ nuxtseo vitals findings
190
+ nuxtseo scans list
191
+ nuxtseo scans show <scan-id>
192
+ nuxtseo scans pages
193
+ nuxtseo annotations list
194
+ nuxtseo annotations create
195
+ nuxtseo annotations update <annotation-id>
196
+ nuxtseo annotations delete <annotation-id>
197
+ nuxtseo analytics <view>
175
198
  nuxtseo search status
176
- nuxtseo search analytics <pages|keywords>
199
+ nuxtseo search analytics <view>
177
200
  nuxtseo search indexing <summary|urls>
201
+ nuxtseo search cohorts
202
+ nuxtseo search index-history
203
+ nuxtseo search inspect <url>
204
+ nuxtseo sitemaps list
205
+ nuxtseo sitemaps urls
206
+ nuxtseo sitemaps submit <sitemap-url>
207
+ nuxtseo sitemaps delete <sitemap-url>
178
208
  nuxtseo research overview
209
+ nuxtseo research domain-traffic <domain>
210
+ nuxtseo research domain-availability <domains>
179
211
  nuxtseo research keywords <topic>
180
212
  nuxtseo research serp <keyword>
181
213
  nuxtseo research rankings <domain>
214
+ nuxtseo audit changes
182
215
  nuxtseo audit link-opportunities
216
+ nuxtseo audit link-structure
183
217
  nuxtseo audit content-decay
184
218
  nuxtseo audit duplicates
185
219
  nuxtseo content briefs list
186
220
  nuxtseo content briefs show <brief-id>
187
221
  nuxtseo content briefs create <keyword>
222
+ nuxtseo timeline list
223
+ nuxtseo skill install
188
224
  ```
189
225
 
226
+ `search analytics` takes `pages`, `keywords`, `countries`, `devices`,
227
+ `timeseries`, `page-detail`, `keyword-detail`, or `analysis`. `page-detail`
228
+ needs `--page-url`, `keyword-detail` needs `--keyword`, and `analysis` needs
229
+ `--preset`. The `brand-only` and `non-brand` presets also need `--brand-terms`.
230
+ The CLI refuses a missing argument before it sends a request.
231
+
232
+ `analytics` takes `performance`, `top-pages`, `source-medium`, `key-events`,
233
+ `countries`, `devices`, or `dimension`. The `dimension` view needs
234
+ `--dimension`.
235
+
190
236
  Run bare `nuxtseo` in a terminal for a task-based menu. It groups Site fixes,
191
237
  performance, research, content, and account setup. The menu prints the direct
192
238
  command it runs.
193
239
 
240
+ `performance` reads the stored Lighthouse lab overview. Real-user field data is
241
+ a different dataset: `vitals summary` and `vitals trend` read CrUX field p75,
242
+ and `vitals findings` names the DOM element behind each failing vital.
243
+ `status` reads the Site assessment: the verdict, the one ranked Next Action, and
244
+ what changed. Start there. `status`, `page issues` and `search cohorts` refuse
245
+ with exit `4` or `5` on an archived or paused Site.
246
+ The four live `backlinks` reads need the Site to have a URL. Without one they
247
+ exit `2`, and retrying will not help; set the Site URL first.
248
+ `search cohorts` returns two lists. Only `established` is proven; `ranked` is
249
+ ordered by raw rate with no significance test and is leads only.
250
+ `backlinks summary`, `backlinks referring-domains`, `backlinks anchors`,
251
+ `backlinks history`, `research domain-traffic` and `research domain-availability`
252
+ can use the Team research limit. Each response reports
253
+ cache use in `evidence`; a cached response spends nothing. `backlinks recoverable`
254
+ reads retained rows and spends nothing.
194
255
  `search status` reads stored connection state. It never waits for Google.
195
256
  `search indexing summary` reads retained URL Inspection coverage.
257
+ `search index-history` dates an indexing change against known releases.
258
+ `search inspect <url>` reads Google's own verdict for one URL.
259
+ `page inspect <url>` reads the NuxtSEO observation store for the same URL.
196
260
  `research keywords`, `research serp`, and `research rankings` can use the Team
197
- research allowance. Keyword responses report cache use in `evidence`. SERP and
198
- ranking responses report `cached: true`. Cached responses use no unit.
199
-
200
- The CLI fetches one page per invocation. It does not auto-page or merge
201
- responses.
202
-
203
- | Command | Paging inputs | Default |
204
- | --- | --- | --- |
205
- | `actions list` | `--limit 1..25`, `--offset >=0` | `--limit 10 --offset 0` |
206
- | `actions show` | `--group-id`, `--cursor`, `--limit 1..100` | `--limit 50` |
207
- | `page inspect` | `--limit 1..200`, `--offset >=0` | `--limit 100 --offset 0` |
208
- | `backlinks recoverable` | `--limit 1..200`, `--offset >=0` | `--limit 100 --offset 0` |
209
- | `mentions list` | `--limit 1..200` | `--limit 100` |
210
- | `search analytics` | `--limit 1..100`, `--page >=1` | `--limit 25 --page 1` |
211
- | `search indexing` | `--limit 1..500`, `--offset >=0` | `--limit 50 --offset 0` |
212
- | `content briefs list` | `--limit 1..100`, `--offset >=0` | `--limit 25 --offset 0` |
261
+ research limit. Keyword responses report cache use in `evidence`. SERP and
262
+ ranking responses report `cached: true`. A cached response spends nothing.
263
+
264
+ The CLI fetches one page per invocation by default. It never merges responses.
265
+
266
+ | Command | Paging inputs | Default | `--all` |
267
+ | --- | --- | --- | --- |
268
+ | `actions list` | `--limit 1..25`, `--offset >=0` | `--limit 10 --offset 0` | yes |
269
+ | `actions show` | `--group-id`, `--cursor`, `--limit 1..100` | `--limit 50` | no |
270
+ | `page inspect` | `--limit 1..200`, `--offset >=0` | `--limit 100 --offset 0` | yes |
271
+ | `backlinks recoverable` | `--limit 1..200`, `--offset >=0` | `--limit 100 --offset 0` | yes |
272
+ | `backlinks referring-domains` | `--limit 1..1000` | `--limit 100` | no |
273
+ | `backlinks anchors` | `--limit 1..1000` | `--limit 100` | no |
274
+ | `vitals findings` | `--limit 1..50`, `--offset >=0` | `--limit 20 --offset 0` | yes |
275
+ | `page issues` | `--limit 1..200`, `--offset >=0` | `--limit 100 --offset 0` | yes |
276
+ | `scans list` | `--limit 1..100` | `--limit 25` | no |
277
+ | `search cohorts` | `--limit 1..50`, `--min-pages 1..500` | `--limit 12 --min-pages 5` | no |
278
+ | `mentions list` | `--limit 1..200` | `--limit 100` | no |
279
+ | `search analytics` | `--limit 1..100`, `--page >=1` | `--limit 25 --page 1` | row views only |
280
+ | `search indexing` | `--limit 1..500`, `--offset >=0` | `--limit 50 --offset 0` | `urls` view only |
281
+ | `sitemaps urls` | `--cursor`, `--limit 1..1000` | `--limit 500` | yes |
282
+ | `content briefs list` | `--limit 1..100`, `--offset >=0` | `--limit 25 --offset 0` | yes |
283
+ | `timeline list` | `--kind`, `--feature`, `--severity`, `--since`, `--cursor`, `--limit 1..100` | `--limit 25` | no |
213
284
 
214
285
  Keep the server order for actions. For another page, pass the next offset or
215
286
  cursor reported by the response. A cursor is opaque; do not edit or infer it.
216
287
 
217
- ## Coding agents
288
+ ### `--all`
218
289
 
219
- The package ships an agent skill that teaches coding agents how to drive the
220
- CLI. Copy it into a global skill directory after installation.
290
+ `--all` repeats the same operation until the server reports no more pages. It
291
+ writes one complete envelope per page, newline delimited. The CLI never merges,
292
+ unwraps, or re-ranks a page. Read the stream one JSON value per line.
221
293
 
222
- Claude Code:
294
+ The loop stops after 50 requests. If pages remain at that point, the CLI exits
295
+ `9` and stderr names the exact argument that resumes the read, for example
296
+ `--offset 50`. Exit `9` is a stop, never an end of data.
223
297
 
224
- ```sh
225
- mkdir -p ~/.claude/skills
226
- cp -R node_modules/@nuxtseo/cli/skills/nuxtseo-cli ~/.claude/skills/
227
- ```
298
+ `--all` covers the Search Console views the server pages: `pages`, `keywords`,
299
+ `countries`, `devices`, and `analysis`, plus the `urls` indexing view. Every
300
+ other view returns one payload, so the CLI refuses `--all` there with exit `2`.
228
301
 
229
- Codex:
302
+ ## Coding agents
303
+
304
+ The package ships an agent skill that teaches coding agents how to drive the
305
+ CLI. Install it with the CLI itself; a global install has no local
306
+ `node_modules` to copy from.
230
307
 
231
308
  ```sh
232
- mkdir -p ~/.codex/skills
233
- cp -R node_modules/@nuxtseo/cli/skills/nuxtseo-cli ~/.codex/skills/
309
+ nuxtseo skill install # ~/.claude/skills/nuxtseo-cli
310
+ nuxtseo skill install --agent codex # ~/.codex/skills/nuxtseo-cli
311
+ nuxtseo skill install --target ./.claude/skills
234
312
  ```
235
313
 
314
+ The command reports the destination it wrote. With `--json` it emits
315
+ `CliSkillInstall` carrying the source and the destination.
316
+
236
317
  The skill covers the JSON contract, Site selection, paging, mutation consent,
237
318
  and what to do for each exit code.
238
319
 
@@ -248,6 +329,7 @@ and what to do for each exit code.
248
329
  | `6` | Rate, quota, provider, or request timeout | Read retry metadata on stderr and retry later |
249
330
  | `7` | Local state, network, contract, or infrastructure failure | Fix the named path or network issue; keep the request ID |
250
331
  | `8` | Resource or accessible Site not found | Run `sites list`, then `sites use` or pass `--site` |
332
+ | `9` | `--all` reached its 50 request cap | Resume with the argument stderr names |
251
333
  | `130` | Interrupted or cancelled | Confirm no mutation result before retrying |
252
334
 
253
335
  The CLI prints the exact server error code. When available, stderr also includes
package/dist/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createBearerTransport, createPublicV1Client } from '@nuxtseo/sdk';
2
2
  import { EXIT_CODE, fail, ok } from './failures.js';
3
3
  import { readConfig, resolveApiUrl, resolveCredential } from './state/index.js';
4
+ import { VERSION } from './version.js';
4
5
  export function fromStateError(error) {
5
6
  const exitCode = error._tag === 'InvalidCredential'
6
7
  ? EXIT_CODE.authentication
@@ -16,7 +17,12 @@ export function fromStateError(error) {
16
17
  }
17
18
  export function createApiClient(apiUrl, token) {
18
19
  return createPublicV1Client({
19
- transport: createBearerTransport({ baseUrl: apiUrl, token }),
20
+ transport: createBearerTransport({
21
+ baseUrl: apiUrl,
22
+ token,
23
+ // ADR-0121: agent_connected classifies the access surface by this prefix.
24
+ headers: { 'user-agent': `nuxtseo-cli/${VERSION}` },
25
+ }),
20
26
  });
21
27
  }
22
28
  export async function loadApiContext(runtime, globals) {
package/dist/cli.js CHANGED
@@ -208,7 +208,7 @@ async function interactiveBare(runtime, globals) {
208
208
  message: 'Choose a task',
209
209
  options: [
210
210
  ...(!auth.value.authenticated ? [{ value: 'login', label: 'Log in', hint: 'Team API token' }] : []),
211
- { value: 'fix', label: 'Fix my Site', hint: 'Issues, Pages, and scans' },
211
+ { value: 'fix', label: 'Fix my Site', hint: 'Issues, Pages, and Scans' },
212
212
  { value: 'performance', label: 'Understand performance', hint: 'Performance and Search Console' },
213
213
  { value: 'research', label: 'Research growth', hint: 'Keywords, SERPs, and competitors' },
214
214
  { value: 'content', label: 'Plan content', hint: 'Briefs, decay, and duplicates' },
@@ -227,7 +227,10 @@ async function interactiveBare(runtime, globals) {
227
227
  const task = await prompts.select({
228
228
  message: 'Fix my Site',
229
229
  options: [
230
+ { value: 'status', label: 'What should I fix first?', hint: 'assessment and next action' },
230
231
  { value: 'actions', label: 'Issues and opportunities' },
232
+ { value: 'page-issues', label: 'Raw Page Issue rows for one issue type' },
233
+ { value: 'changes', label: 'What the last crawl changed' },
231
234
  { value: 'inspect', label: 'Inspect a Page' },
232
235
  { value: 'scan', label: 'Scan a Page' },
233
236
  { value: 'backlinks', label: 'Recover broken Backlinks' },
@@ -244,15 +247,30 @@ async function interactiveBare(runtime, globals) {
244
247
  ? url
245
248
  : runInteractiveCommand(['page', task, url.value], runtime, globals);
246
249
  }
247
- return runInteractiveCommand(task === 'actions' ? ['actions', 'list'] : ['backlinks', 'recoverable'], runtime, globals);
250
+ const command = task === 'actions'
251
+ ? ['actions', 'list']
252
+ : task === 'changes'
253
+ ? ['audit', 'changes']
254
+ : task === 'status'
255
+ ? ['status']
256
+ : task === 'page-issues'
257
+ ? ['page', 'issues', '--source', 'crawl', '--issue', 'broken-internal-links']
258
+ : ['backlinks', 'recoverable'];
259
+ return runInteractiveCommand(command, runtime, globals);
248
260
  }
249
261
  if (group === 'performance') {
250
262
  const task = await prompts.select({
251
263
  message: 'Understand performance',
252
264
  options: [
253
- { value: 'performance', label: 'Performance overview' },
265
+ { value: 'performance', label: 'Performance overview', hint: 'stored Lighthouse lab Scans' },
266
+ { value: 'vitals', label: 'Field Core Web Vitals', hint: 'CrUX real-user p75' },
267
+ { value: 'findings', label: 'What to fix for Core Web Vitals', hint: 'element and fix prompt' },
268
+ { value: 'scans', label: 'Lighthouse Scans', hint: 'retained lab Scans' },
269
+ { value: 'cohorts', label: 'Indexing by route family', hint: 'which template Google declines' },
254
270
  { value: 'connection', label: 'Search Console connection' },
255
271
  { value: 'indexing', label: 'Search indexing coverage' },
272
+ { value: 'sitemaps', label: 'Search Console sitemaps' },
273
+ { value: 'traffic', label: 'Web Analytics' },
256
274
  { value: 'pages', label: 'Search Console Pages' },
257
275
  { value: 'keywords', label: 'Search Console queries' },
258
276
  ],
@@ -264,11 +282,23 @@ async function interactiveBare(runtime, globals) {
264
282
  return ok(undefined);
265
283
  const command = task === 'performance'
266
284
  ? ['performance']
267
- : task === 'connection'
268
- ? ['search', 'status']
269
- : task === 'indexing'
270
- ? ['search', 'indexing', 'summary']
271
- : ['search', 'analytics', task];
285
+ : task === 'vitals'
286
+ ? ['vitals', 'summary']
287
+ : task === 'findings'
288
+ ? ['vitals', 'findings']
289
+ : task === 'scans'
290
+ ? ['scans', 'list']
291
+ : task === 'cohorts'
292
+ ? ['search', 'cohorts']
293
+ : task === 'connection'
294
+ ? ['search', 'status']
295
+ : task === 'indexing'
296
+ ? ['search', 'indexing', 'summary']
297
+ : task === 'sitemaps'
298
+ ? ['sitemaps', 'list']
299
+ : task === 'traffic'
300
+ ? ['analytics', 'performance']
301
+ : ['search', 'analytics', task];
272
302
  return runInteractiveCommand(command, runtime, globals);
273
303
  }
274
304
  if (group === 'research') {
@@ -276,10 +306,13 @@ async function interactiveBare(runtime, globals) {
276
306
  message: 'Research growth',
277
307
  options: [
278
308
  { value: 'overview', label: 'Site and competitors', hint: 'stored data' },
279
- { value: 'keywords', label: 'Keyword ideas', hint: 'uses live research allowance' },
280
- { value: 'serp', label: 'SERP analysis', hint: 'uses live research allowance' },
281
- { value: 'rankings', label: 'Domain rankings', hint: 'uses live research allowance' },
309
+ { value: 'keywords', label: 'Keyword ideas', hint: 'uses the live research limit' },
310
+ { value: 'serp', label: 'SERP analysis', hint: 'uses the live research limit' },
311
+ { value: 'rankings', label: 'Domain rankings', hint: 'uses the live research limit' },
282
312
  { value: 'links', label: 'Link opportunities', hint: 'stored data' },
313
+ { value: 'structure', label: 'Link structure', hint: 'stored data' },
314
+ { value: 'backlinks', label: 'Backlink profile', hint: 'uses the live research limit' },
315
+ { value: 'domain', label: 'Any domain traffic estimate', hint: 'uses the live research limit' },
283
316
  ],
284
317
  input: runtime.input,
285
318
  output: runtime.error,
@@ -291,6 +324,20 @@ async function interactiveBare(runtime, globals) {
291
324
  return runInteractiveCommand(['research', 'overview'], runtime, globals);
292
325
  if (task === 'links')
293
326
  return runInteractiveCommand(['audit', 'link-opportunities'], runtime, globals);
327
+ if (task === 'structure')
328
+ return runInteractiveCommand(['audit', 'link-structure'], runtime, globals);
329
+ if (task === 'backlinks')
330
+ return runInteractiveCommand(['backlinks', 'summary'], runtime, globals);
331
+ if (task === 'domain') {
332
+ const domain = await promptTextValue(runtime, {
333
+ message: 'Which domain?',
334
+ placeholder: 'example.com',
335
+ label: 'Domain',
336
+ });
337
+ if (domain._tag === 'Err')
338
+ return domain;
339
+ return runInteractiveCommand(['research', 'domain-traffic', domain.value], runtime, globals);
340
+ }
294
341
  const label = task === 'rankings' ? 'Domain' : 'Keyword';
295
342
  const value = await promptTextValue(runtime, {
296
343
  message: task === 'rankings' ? 'Which domain?' : 'Which keyword or topic?',
@@ -309,6 +356,7 @@ async function interactiveBare(runtime, globals) {
309
356
  { value: 'create', label: 'Create a Content Brief' },
310
357
  { value: 'decay', label: 'Content decay' },
311
358
  { value: 'duplicates', label: 'Duplicate clusters' },
359
+ { value: 'annotations', label: 'Chart annotations', hint: 'mark what you shipped' },
312
360
  ],
313
361
  input: runtime.input,
314
362
  output: runtime.error,
@@ -322,6 +370,8 @@ async function interactiveBare(runtime, globals) {
322
370
  return runInteractiveCommand(['audit', 'content-decay'], runtime, globals);
323
371
  if (task === 'duplicates')
324
372
  return runInteractiveCommand(['audit', 'duplicates'], runtime, globals);
373
+ if (task === 'annotations')
374
+ return runInteractiveCommand(['annotations', 'list'], runtime, globals);
325
375
  const keyword = await promptTextValue(runtime, {
326
376
  message: 'Which target keyword?',
327
377
  placeholder: 'nuxt seo',
@@ -336,6 +386,7 @@ async function interactiveBare(runtime, globals) {
336
386
  options: [
337
387
  { value: 'site', label: 'Switch Site', hint: config.value.siteId ?? 'none selected' },
338
388
  { value: 'usage', label: 'Account usage' },
389
+ { value: 'whoami', label: 'Credential and scopes' },
339
390
  { value: 'config', label: 'Configuration' },
340
391
  { value: 'logout', label: 'Log out' },
341
392
  ],
@@ -82,7 +82,9 @@ function optionName(argument) {
82
82
  export function validateCommandOptions(root, rawArgs) {
83
83
  const resolved = resolveCommand(root, rawArgs);
84
84
  const definitions = commandArgs(resolved.command);
85
+ const positionals = Object.values(definitions).filter(definition => definition.type === 'positional');
85
86
  const aliases = new Map();
87
+ let positionalIndex = 0;
86
88
  for (const definition of Object.values(definitions)) {
87
89
  const alias = 'alias' in definition ? definition.alias : undefined;
88
90
  const names = alias === undefined
@@ -116,7 +118,15 @@ export function validateCommandOptions(root, rawArgs) {
116
118
  return fail(EXIT_CODE.invalidInput, `invalid_cli_input: Unknown option ${JSON.stringify(token)}.`);
117
119
  if (definition.type !== 'boolean' && !resolved.remaining[index + 1]?.startsWith('-'))
118
120
  index++;
121
+ continue;
122
+ }
123
+ if (positionalIndex < positionals.length) {
124
+ positionalIndex++;
125
+ continue;
119
126
  }
127
+ if (Object.keys(commandChildren(resolved.command)).length > 0)
128
+ return fail(EXIT_CODE.invalidInput, `invalid_cli_input: Unknown command ${token}`);
129
+ return fail(EXIT_CODE.invalidInput, `invalid_cli_input: Unexpected argument ${JSON.stringify(token)}.`);
120
130
  }
121
131
  return ok(undefined);
122
132
  }
@@ -5,4 +5,23 @@ import type { CliRuntime } from './runtime.js';
5
5
  export interface CommandExecution {
6
6
  result: CliResult<void> | null;
7
7
  }
8
+ /**
9
+ * `--all` request cap. One page per request stays the default. `--all` repeats
10
+ * the same operation and writes one complete envelope per page, so nothing is
11
+ * merged or renamed. The cap bounds an unattended loop; reaching it exits 9 and
12
+ * names the resume argument, so a caller never mistakes a stop for an end.
13
+ */
14
+ export declare const ALL_PAGES_REQUEST_CAP = 50;
15
+ export interface PagePosition {
16
+ offset?: number;
17
+ page?: number;
18
+ cursor?: string;
19
+ }
20
+ export type NextPage = {
21
+ _tag: 'More';
22
+ position: PagePosition;
23
+ resume: string;
24
+ } | {
25
+ _tag: 'Done';
26
+ };
8
27
  export declare function createRootCommand(runtime: CliRuntime, globals: GlobalOptions, execution: CommandExecution): CommandDef<any>;