@nuxtseo/cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +34 -6
- package/dist/failures.js +12 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/render.js +13 -2
- package/dist/state/paths.d.ts +2 -0
- package/dist/state/paths.js +2 -0
- package/dist/update-check.d.ts +25 -0
- package/dist/update-check.js +107 -0
- package/package.json +4 -7
- package/skills/nuxtseo-cli/SKILL.md +9 -4
- package/skills/nuxtseo-cli/references/protocol.md +17 -1
package/dist/cli.js
CHANGED
|
@@ -7,16 +7,28 @@ import { EXIT_CODE, fail, fromSdkFailure, ok, unexpectedFailure } from './failur
|
|
|
7
7
|
import { extractGlobalOptions } from './parse.js';
|
|
8
8
|
import { writeCliResponse, writeDiagnostic, writeOutput, writeProtocolResponse } from './runtime.js';
|
|
9
9
|
import { getCredentialStatus, readConfig, resolveApiUrl, resolveSiteId, updateConfig } from './state/index.js';
|
|
10
|
+
import { checkForUpdate, updateNoticeLine } from './update-check.js';
|
|
10
11
|
import { VERSION } from './version.js';
|
|
11
|
-
function
|
|
12
|
+
function writeUpdateNotice(runtime, notice) {
|
|
13
|
+
if (notice)
|
|
14
|
+
writeDiagnostic(runtime, updateNoticeLine(notice));
|
|
15
|
+
}
|
|
16
|
+
function report(runtime, failure, json = false, notice = null) {
|
|
12
17
|
if (json) {
|
|
13
18
|
if (failure.protocolResponse !== undefined) {
|
|
14
19
|
writeProtocolResponse(runtime, failure.protocolResponse);
|
|
15
20
|
}
|
|
16
21
|
else {
|
|
22
|
+
// Exit 2 and exit 7 are where a stale binary masquerades as broken docs
|
|
23
|
+
// or a broken server. Version skew is a plausible cause there, so the
|
|
24
|
+
// envelope names both versions and stderr carries the update command.
|
|
25
|
+
const carriesVersionSkew = failure.exitCode === EXIT_CODE.invalidInput || failure.exitCode === EXIT_CODE.infrastructure;
|
|
17
26
|
writeCliResponse(runtime, {
|
|
18
27
|
_tag: 'CliError',
|
|
19
28
|
schemaVersion: 1,
|
|
29
|
+
...(carriesVersionSkew
|
|
30
|
+
? { cliVersion: VERSION, ...(notice ? { latestKnownVersion: notice.latest } : {}) }
|
|
31
|
+
: {}),
|
|
20
32
|
error: {
|
|
21
33
|
code: failure.code,
|
|
22
34
|
exitCode: failure.exitCode,
|
|
@@ -336,9 +348,16 @@ async function interactiveBare(runtime, globals) {
|
|
|
336
348
|
}
|
|
337
349
|
export async function runCli(rawArgs, runtime) {
|
|
338
350
|
const jsonRequested = rawArgs.includes('--json');
|
|
351
|
+
// Started before parsing so the registry round trip overlaps the command. The
|
|
352
|
+
// check reads a local cache and never rejects; a slow registry only delays
|
|
353
|
+
// this line, never the command result itself.
|
|
354
|
+
const updateCheck = checkForUpdate({ paths: runtime.paths, env: runtime.env });
|
|
339
355
|
const parsed = extractGlobalOptions(rawArgs);
|
|
340
|
-
if (parsed._tag === 'Err')
|
|
341
|
-
|
|
356
|
+
if (parsed._tag === 'Err') {
|
|
357
|
+
const notice = await updateCheck;
|
|
358
|
+
writeUpdateNotice(runtime, notice);
|
|
359
|
+
return report(runtime, parsed.error, jsonRequested, notice);
|
|
360
|
+
}
|
|
342
361
|
const { args, options: globals } = parsed.value;
|
|
343
362
|
const effectiveRuntime = {
|
|
344
363
|
...runtime,
|
|
@@ -350,21 +369,28 @@ export async function runCli(rawArgs, runtime) {
|
|
|
350
369
|
requestTimeoutMs: globals.timeoutMs,
|
|
351
370
|
};
|
|
352
371
|
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
|
|
372
|
+
const notice = await updateCheck;
|
|
353
373
|
if (globals.json)
|
|
354
374
|
writeCliResponse(effectiveRuntime, { _tag: 'CliVersion', schemaVersion: 1, version: VERSION });
|
|
355
375
|
else
|
|
356
376
|
writeOutput(effectiveRuntime, VERSION);
|
|
377
|
+
writeUpdateNotice(effectiveRuntime, notice);
|
|
357
378
|
return EXIT_CODE.success;
|
|
358
379
|
}
|
|
359
380
|
if (args.includes('--help') || args.includes('-h')) {
|
|
360
381
|
const command = createRootCommand(effectiveRuntime, globals, { result: null });
|
|
361
382
|
const validOptions = validateCommandOptions(command, args);
|
|
362
|
-
if (validOptions._tag === 'Err')
|
|
363
|
-
|
|
383
|
+
if (validOptions._tag === 'Err') {
|
|
384
|
+
const notice = await updateCheck;
|
|
385
|
+
writeUpdateNotice(effectiveRuntime, notice);
|
|
386
|
+
return report(effectiveRuntime, validOptions.error, globals.json, notice);
|
|
387
|
+
}
|
|
364
388
|
if (globals.json)
|
|
365
389
|
writeCliResponse(effectiveRuntime, describeCommand(command, args, VERSION));
|
|
366
390
|
else
|
|
367
391
|
writeOutput(effectiveRuntime, await requestedUsage(effectiveRuntime, globals, args));
|
|
392
|
+
const notice = await updateCheck;
|
|
393
|
+
writeUpdateNotice(effectiveRuntime, notice);
|
|
368
394
|
return EXIT_CODE.success;
|
|
369
395
|
}
|
|
370
396
|
const result = args.length === 0
|
|
@@ -372,7 +398,9 @@ export async function runCli(rawArgs, runtime) {
|
|
|
372
398
|
? await interactiveBare(effectiveRuntime, globals)
|
|
373
399
|
: await nonInteractiveBare(effectiveRuntime, globals)
|
|
374
400
|
: await runExplicit(args, effectiveRuntime, globals);
|
|
401
|
+
const notice = await updateCheck;
|
|
402
|
+
writeUpdateNotice(effectiveRuntime, notice);
|
|
375
403
|
return result._tag === 'Ok'
|
|
376
404
|
? EXIT_CODE.success
|
|
377
|
-
: report(effectiveRuntime, timeoutRetryFailure(result.error, rawArgs, globals.timeoutMs), globals.json);
|
|
405
|
+
: report(effectiveRuntime, timeoutRetryFailure(result.error, rawArgs, globals.timeoutMs), globals.json, notice);
|
|
378
406
|
}
|
package/dist/failures.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { VERSION } from './version.js';
|
|
2
|
+
const UPDATE_COMMAND = 'pnpm add -g @nuxtseo/cli';
|
|
3
|
+
// `contract_violation` most often means this CLI is older than the server
|
|
4
|
+
// contract. One line routes an agent to update before anything else.
|
|
5
|
+
const CONTRACT_REMEDIATION = [
|
|
6
|
+
'Most common cause: this CLI is older than the server contract.',
|
|
7
|
+
`Update first with ${UPDATE_COMMAND}, then run the command again.`,
|
|
8
|
+
].join(' ');
|
|
1
9
|
export const EXIT_CODE = {
|
|
2
10
|
success: 0,
|
|
3
11
|
invalidInput: 2,
|
|
@@ -97,12 +105,15 @@ export function fromSdkFailure(error) {
|
|
|
97
105
|
error.code === 'auth_expired'
|
|
98
106
|
? 'Open site settings in the dashboard, then Search Console, to reconnect.'
|
|
99
107
|
: undefined,
|
|
108
|
+
error.code === 'contract_violation'
|
|
109
|
+
? CONTRACT_REMEDIATION
|
|
110
|
+
: undefined,
|
|
100
111
|
].filter((line) => line !== undefined).join('\n'),
|
|
101
112
|
protocolResponse: error.response,
|
|
102
113
|
},
|
|
103
114
|
};
|
|
104
115
|
case 'ContractFailure':
|
|
105
|
-
return fail(EXIT_CODE.infrastructure, [`contract_violation: ${error.message}`, error.requestId ? `Request ID: ${error.requestId}` : undefined]
|
|
116
|
+
return fail(EXIT_CODE.infrastructure, [`contract_violation: ${error.message}`, error.requestId ? `Request ID: ${error.requestId}` : undefined, CONTRACT_REMEDIATION, `Current CLI version: ${VERSION}`]
|
|
106
117
|
.filter((line) => line !== undefined)
|
|
107
118
|
.join('\n'), undefined, 'contract_violation');
|
|
108
119
|
case 'TransportFailure': {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,6 @@ export { runCli } from './cli.js';
|
|
|
2
2
|
export { EXIT_CODE } from './failures.js';
|
|
3
3
|
export type { CliFailure, CliFailureCode, CliResult, ExitCode, LocalFailureCode } from './failures.js';
|
|
4
4
|
export type { CliRuntime } from './runtime.js';
|
|
5
|
+
export { checkForUpdate, isNewerVersion, updateNoticeFor } from './update-check.js';
|
|
6
|
+
export type { UpdateNotice } from './update-check.js';
|
|
5
7
|
export { VERSION } from './version.js';
|
package/dist/index.js
CHANGED
package/dist/render.js
CHANGED
|
@@ -34,6 +34,13 @@ export function renderUsage(response) {
|
|
|
34
34
|
});
|
|
35
35
|
return [`Plan: ${response.data.plan}`, ...meters].join('\n');
|
|
36
36
|
}
|
|
37
|
+
function evidenceAgeLine(freshness) {
|
|
38
|
+
if (freshness.verdict === 'aged')
|
|
39
|
+
return ` evidence observed ${freshness.ageHours}h ago (${freshness.confidence})`;
|
|
40
|
+
if (freshness.verdict === 'unknown')
|
|
41
|
+
return ' evidence age unknown';
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
37
44
|
export function renderActions(data) {
|
|
38
45
|
if (data.actions.length === 0)
|
|
39
46
|
return 'No next actions.';
|
|
@@ -42,7 +49,8 @@ export function renderActions(data) {
|
|
|
42
49
|
...data.actions.map(action => [
|
|
43
50
|
`${action.id} ${action.diagnosis}`,
|
|
44
51
|
` ${action.status}, effort ${action.effort}, ${action.affectedPages ?? 'unknown'} affected pages`,
|
|
45
|
-
|
|
52
|
+
evidenceAgeLine(action.evidence.freshness),
|
|
53
|
+
].filter(Boolean).join('\n')),
|
|
46
54
|
data.page.hasMore ? `More actions available after offset ${data.page.offset + data.page.limit}.` : '',
|
|
47
55
|
].filter(Boolean).join('\n');
|
|
48
56
|
}
|
|
@@ -76,9 +84,12 @@ export function renderPageInspection(data) {
|
|
|
76
84
|
['SEO', data.performance.lighthouse.seo],
|
|
77
85
|
])
|
|
78
86
|
: 'No Lighthouse scan.';
|
|
87
|
+
const empty = data.observations.coverage === 'issues-open'
|
|
88
|
+
? ''
|
|
89
|
+
: ` (${data.observations.coverage})`;
|
|
79
90
|
return [
|
|
80
91
|
data.page.url,
|
|
81
|
-
`Observations: ${data.observations.total}`,
|
|
92
|
+
`Observations: ${data.observations.total}${empty}`,
|
|
82
93
|
...observations,
|
|
83
94
|
lighthouse,
|
|
84
95
|
`Tracked keywords: ${data.search.keywords.length}`,
|
package/dist/state/paths.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export declare const STATE_DIRECTORY_NAME = ".nuxtseo";
|
|
2
2
|
export declare const AUTH_FILENAME = "auth.json";
|
|
3
3
|
export declare const CONFIG_FILENAME = "config.json";
|
|
4
|
+
export declare const UPDATE_CHECK_FILENAME = "update-check.json";
|
|
4
5
|
export interface StatePaths {
|
|
5
6
|
directory: string;
|
|
6
7
|
authFile: string;
|
|
7
8
|
configFile: string;
|
|
9
|
+
updateCheckFile: string;
|
|
8
10
|
}
|
|
9
11
|
export declare function createStatePaths(homeDirectory: string): StatePaths;
|
|
10
12
|
export declare function defaultStatePaths(): StatePaths;
|
package/dist/state/paths.js
CHANGED
|
@@ -3,12 +3,14 @@ import { join } from 'pathe';
|
|
|
3
3
|
export const STATE_DIRECTORY_NAME = '.nuxtseo';
|
|
4
4
|
export const AUTH_FILENAME = 'auth.json';
|
|
5
5
|
export const CONFIG_FILENAME = 'config.json';
|
|
6
|
+
export const UPDATE_CHECK_FILENAME = 'update-check.json';
|
|
6
7
|
export function createStatePaths(homeDirectory) {
|
|
7
8
|
const directory = join(homeDirectory, STATE_DIRECTORY_NAME);
|
|
8
9
|
return {
|
|
9
10
|
directory,
|
|
10
11
|
authFile: join(directory, AUTH_FILENAME),
|
|
11
12
|
configFile: join(directory, CONFIG_FILENAME),
|
|
13
|
+
updateCheckFile: join(directory, UPDATE_CHECK_FILENAME),
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
export function defaultStatePaths() {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { StatePaths } from './state/index.js';
|
|
2
|
+
/** How long a registry answer stays authoritative, matching npm's update cache. */
|
|
3
|
+
export declare const UPDATE_CHECK_INTERVAL_MS: number;
|
|
4
|
+
export interface UpdateNotice {
|
|
5
|
+
current: string;
|
|
6
|
+
latest: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UpdateCheckCache {
|
|
9
|
+
lastCheckedAt: string;
|
|
10
|
+
latest: string | null;
|
|
11
|
+
}
|
|
12
|
+
export type RegistryLatestFetch = (url: string, signal: AbortSignal) => Promise<string | null>;
|
|
13
|
+
export interface UpdateCheckOptions {
|
|
14
|
+
paths: StatePaths;
|
|
15
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
16
|
+
fetch?: RegistryLatestFetch;
|
|
17
|
+
now?: () => Date;
|
|
18
|
+
}
|
|
19
|
+
export declare function isNewerVersion(candidate: string, current: string): boolean;
|
|
20
|
+
export declare function updateNoticeFor(latest: string | null, current: string): UpdateNotice | null;
|
|
21
|
+
export declare function updateCheckDue(cache: UpdateCheckCache | null, now: Date): boolean;
|
|
22
|
+
export declare function parseUpdateCheckCache(content: string): UpdateCheckCache | null;
|
|
23
|
+
export declare function fetchRegistryLatest(url: string, signal: AbortSignal): Promise<string | null>;
|
|
24
|
+
export declare function checkForUpdate(options: UpdateCheckOptions): Promise<UpdateNotice | null>;
|
|
25
|
+
export declare function updateNoticeLine(notice: UpdateNotice): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { VERSION } from './version.js';
|
|
3
|
+
/** How long a registry answer stays authoritative, matching npm's update cache. */
|
|
4
|
+
export const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
5
|
+
const REGISTRY_LATEST_URL = 'https://registry.npmjs.org/@nuxtseo/cli/latest';
|
|
6
|
+
const FETCH_TIMEOUT_MS = 2_000;
|
|
7
|
+
function updateCheckDisabled(env) {
|
|
8
|
+
return env?.NUXTSEO_NO_UPDATE_CHECK === '1' || env?.NUXTSEO_NO_UPDATE_CHECK === 'true';
|
|
9
|
+
}
|
|
10
|
+
export function isNewerVersion(candidate, current) {
|
|
11
|
+
const core = (value) => {
|
|
12
|
+
const parts = value.trim().replace(/^v/, '').split('-')[0].split('.');
|
|
13
|
+
if (parts.length < 3)
|
|
14
|
+
return null;
|
|
15
|
+
const numbers = parts.slice(0, 3).map(part => Number.parseInt(part, 10));
|
|
16
|
+
return numbers.some(part => Number.isNaN(part)) ? null : numbers;
|
|
17
|
+
};
|
|
18
|
+
const candidateCore = core(candidate);
|
|
19
|
+
const currentCore = core(current);
|
|
20
|
+
if (!candidateCore || !currentCore)
|
|
21
|
+
return false;
|
|
22
|
+
for (let index = 0; index < 3; index++) {
|
|
23
|
+
if (candidateCore[index] !== currentCore[index])
|
|
24
|
+
return candidateCore[index] > currentCore[index];
|
|
25
|
+
}
|
|
26
|
+
// Same core: a prerelease candidate is not an update over the stable current.
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
export function updateNoticeFor(latest, current) {
|
|
30
|
+
return latest !== null && isNewerVersion(latest, current) ? { current, latest } : null;
|
|
31
|
+
}
|
|
32
|
+
export function updateCheckDue(cache, now) {
|
|
33
|
+
if (!cache)
|
|
34
|
+
return true;
|
|
35
|
+
const checkedAt = Date.parse(cache.lastCheckedAt);
|
|
36
|
+
return Number.isNaN(checkedAt) || now.getTime() - checkedAt >= UPDATE_CHECK_INTERVAL_MS;
|
|
37
|
+
}
|
|
38
|
+
export function parseUpdateCheckCache(content) {
|
|
39
|
+
let value;
|
|
40
|
+
try {
|
|
41
|
+
value = JSON.parse(content);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// A corrupt cache file is disposable state, not an error: refetch instead.
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value !== 'object' || value === null)
|
|
48
|
+
return null;
|
|
49
|
+
const record = value;
|
|
50
|
+
if (typeof record.lastCheckedAt !== 'string')
|
|
51
|
+
return null;
|
|
52
|
+
if (record.latest !== null && typeof record.latest !== 'string')
|
|
53
|
+
return null;
|
|
54
|
+
return { lastCheckedAt: record.lastCheckedAt, latest: record.latest };
|
|
55
|
+
}
|
|
56
|
+
async function readUpdateCheckCache(paths) {
|
|
57
|
+
const content = await readFile(paths.updateCheckFile, 'utf8').catch(() => {
|
|
58
|
+
// A missing or unreadable cache file is the common first-run case.
|
|
59
|
+
return null;
|
|
60
|
+
});
|
|
61
|
+
return typeof content === 'string' ? parseUpdateCheckCache(content) : null;
|
|
62
|
+
}
|
|
63
|
+
async function writeUpdateCheckCache(paths, cache) {
|
|
64
|
+
// Persisting the cache is best effort. A failed write only costs one extra
|
|
65
|
+
// registry fetch on the next run, so the failure is ignorable by design.
|
|
66
|
+
await mkdir(paths.directory, { recursive: true, mode: 0o700 }).catch(() => {
|
|
67
|
+
// See the comment above: the cache is disposable state.
|
|
68
|
+
return undefined;
|
|
69
|
+
});
|
|
70
|
+
await writeFile(paths.updateCheckFile, `${JSON.stringify(cache, null, 2)}\n`, {
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
mode: 0o600,
|
|
73
|
+
}).catch(() => {
|
|
74
|
+
// See the comment above: the cache is disposable state.
|
|
75
|
+
return undefined;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function fetchRegistryLatest(url, signal) {
|
|
79
|
+
const response = await fetch(url, { signal, headers: { accept: 'application/json' } });
|
|
80
|
+
if (!response.ok)
|
|
81
|
+
return null;
|
|
82
|
+
const body = await response.json();
|
|
83
|
+
if (typeof body !== 'object' || body === null)
|
|
84
|
+
return null;
|
|
85
|
+
const version = body.version;
|
|
86
|
+
return typeof version === 'string' && version ? version : null;
|
|
87
|
+
}
|
|
88
|
+
export async function checkForUpdate(options) {
|
|
89
|
+
if (updateCheckDisabled(options.env))
|
|
90
|
+
return null;
|
|
91
|
+
const now = options.now?.() ?? new Date();
|
|
92
|
+
const cache = await readUpdateCheckCache(options.paths);
|
|
93
|
+
if (!updateCheckDue(cache, now))
|
|
94
|
+
return updateNoticeFor(cache.latest, VERSION);
|
|
95
|
+
const fetchLatest = options.fetch ?? fetchRegistryLatest;
|
|
96
|
+
const latest = await fetchLatest(REGISTRY_LATEST_URL, AbortSignal.timeout(FETCH_TIMEOUT_MS)).catch(() => {
|
|
97
|
+
// Registry reachability is best effort. The run continues without a hint.
|
|
98
|
+
return null;
|
|
99
|
+
});
|
|
100
|
+
if (latest !== null)
|
|
101
|
+
await writeUpdateCheckCache(options.paths, { lastCheckedAt: now.toISOString(), latest });
|
|
102
|
+
// A stale cached answer still beats no answer while the registry is unreachable.
|
|
103
|
+
return updateNoticeFor(latest ?? cache?.latest ?? null, VERSION);
|
|
104
|
+
}
|
|
105
|
+
export function updateNoticeLine(notice) {
|
|
106
|
+
return `${notice.latest} available, current ${notice.current}. Update with: pnpm add -g @nuxtseo/cli`;
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtseo/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"description": "Command line interface for the NuxtSEO public API.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://nuxtseo.com/pro",
|
|
@@ -30,13 +30,11 @@
|
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=22"
|
|
32
32
|
},
|
|
33
|
-
"peerDependencies": {
|
|
34
|
-
"@nuxtseo/sdk": "^0.1.1"
|
|
35
|
-
},
|
|
36
33
|
"dependencies": {
|
|
37
34
|
"@clack/prompts": "^1.7.0",
|
|
38
35
|
"citty": "^0.2.2",
|
|
39
|
-
"pathe": "^2.0.3"
|
|
36
|
+
"pathe": "^2.0.3",
|
|
37
|
+
"@nuxtseo/sdk": "^0.1.2"
|
|
40
38
|
},
|
|
41
39
|
"optionalDependencies": {
|
|
42
40
|
"@napi-rs/keyring": "^1.3.0"
|
|
@@ -46,8 +44,7 @@
|
|
|
46
44
|
"@types/node": "^26.2.0",
|
|
47
45
|
"publint": "^0.3.24",
|
|
48
46
|
"typescript": "npm:typescript-native-bridge@6.0.3-bridge.10.tsgo.7.0.2",
|
|
49
|
-
"@nuxtseo/
|
|
50
|
-
"@nuxtseo/protocol": "0.1.1"
|
|
47
|
+
"@nuxtseo/protocol": "0.1.2"
|
|
51
48
|
},
|
|
52
49
|
"publishConfig": {
|
|
53
50
|
"access": "public"
|
|
@@ -105,12 +105,12 @@ handling failures, read [CLI protocol](references/protocol.md).
|
|
|
105
105
|
| `sites list` | Every accessible Site | Source of Site IDs |
|
|
106
106
|
| `sites use <site-id>` | – | Persists a default Site for the human, not for you |
|
|
107
107
|
| `usage` | Plan and meters | `--group integrations\|compute\|capacity` |
|
|
108
|
-
| `actions list` | Server-ranked issues and opportunities | `--limit 1..25` (default 10), `--offset` |
|
|
108
|
+
| `actions list` | Server-ranked issues and opportunities | `--limit 1..25` (default 10), `--offset`. JSON carries `evidence.freshness`; `verdict: "aged"` means the observation is over a day old, so live-check before fixing |
|
|
109
109
|
| `actions show <action-id>` | One issue or opportunity plus evidence | `--limit 1..100` (default 50), `--group-id`, `--cursor` |
|
|
110
110
|
| `actions resolve <action-id>` | – | Mutation. Claims the issue or opportunity and starts server verification |
|
|
111
111
|
| `backlinks recoverable` | Stored recoverable backlinks | `--limit 1..200`, `--offset` |
|
|
112
112
|
| `mentions list` | Stored mentions | `--limit 1..200` |
|
|
113
|
-
| `page inspect <url>` | Stored observations, Lighthouse, keywords | `--limit 1..200`, `--offset`, `--include-resolved` |
|
|
113
|
+
| `page inspect <url>` | Stored observations, Lighthouse, keywords | `--limit 1..200`, `--offset`, `--include-resolved`. JSON carries `observations.coverage`: `never-scanned` means no source recorded this Page, `scanned-clear` means recorded and all clear |
|
|
114
114
|
| `page scan <url>` | – | Mutation. Starts mobile and desktop scans |
|
|
115
115
|
| `performance` | Site performance overview | Medians for perf, a11y, SEO, LCP, TBT, CLS |
|
|
116
116
|
| `search status` | Stored Search Console connection | Provider free |
|
|
@@ -141,7 +141,10 @@ flag, and prefer the table above for anything it already answers.
|
|
|
141
141
|
This is the sequence that turns CLI output into a code change:
|
|
142
142
|
|
|
143
143
|
1. `nuxtseo actions list --site <id> --json` gives ranked work with an ID, a
|
|
144
|
-
diagnosis, an effort, and an affected page count.
|
|
144
|
+
diagnosis, an effort, and an affected page count. Check
|
|
145
|
+
`evidence.freshness` per row: `verdict: "aged"` with a large `ageHours`
|
|
146
|
+
means the evidence is old. Live-check a cheap sample before fixing, because
|
|
147
|
+
the site may have moved on since the observation.
|
|
145
148
|
2. `nuxtseo actions show <action-id> --site <id> --json` gives the evidence:
|
|
146
149
|
which pages, which finding type, and when it was observed.
|
|
147
150
|
3. Fix the cause in the repository. The evidence names URLs; map them back to
|
|
@@ -164,4 +167,6 @@ command exits `5` with `stale_evidence`; re-run step 2 and decide again.
|
|
|
164
167
|
- Do not loop over Pages, keywords, or domains unattended. Check `usage` first.
|
|
165
168
|
- Live research can consume allowance. A cached result does not consume a unit.
|
|
166
169
|
- Report failures as they are. The CLI has no MCP or private-route fallback.
|
|
167
|
-
- Do not treat an empty result as clean.
|
|
170
|
+
- Do not treat an empty result as clean. `page inspect` JSON says which empty
|
|
171
|
+
it is through `observations.coverage`; other commands may still have
|
|
172
|
+
incomplete evidence.
|
|
@@ -32,6 +32,22 @@ Local outcomes have no server body. They carry `schemaVersion: 1` and one tag:
|
|
|
32
32
|
|
|
33
33
|
Discriminate on `_tag`. Protocol envelopes never carry one.
|
|
34
34
|
|
|
35
|
+
A `CliError` for exit `2` or exit `7` also carries `cliVersion`, and
|
|
36
|
+
`latestKnownVersion` when the CLI knows the registry holds a newer version.
|
|
37
|
+
Those two exits are where a stale binary looks like broken docs or a broken
|
|
38
|
+
server; check the versions before debugging anything else.
|
|
39
|
+
|
|
40
|
+
## Update hints
|
|
41
|
+
|
|
42
|
+
Every run checks the npm registry for a newer CLI, cached for 24 hours. When a
|
|
43
|
+
newer version is known, one line goes to stderr, for example:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
0.2.0 available, current 0.1.1. Update with: pnpm add -g @nuxtseo/cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Set `NUXTSEO_NO_UPDATE_CHECK=1` to disable the check, for example in CI.
|
|
50
|
+
|
|
35
51
|
## Paging
|
|
36
52
|
|
|
37
53
|
One invocation makes one request. The CLI never auto-pages or merges responses.
|
|
@@ -51,7 +67,7 @@ Branch on the exit code, not message text.
|
|
|
51
67
|
| `4` | Forbidden, scope, or entitlement failure | Report the plan or token blocker |
|
|
52
68
|
| `5` | Conflict, stale evidence, or ambiguous Site | Re-read. Pass `--site` if ambiguous |
|
|
53
69
|
| `6` | Rate limit, quota, provider outage, or timeout | Read retry metadata, then wait |
|
|
54
|
-
| `7` | Local state, network, contract, or infrastructure failure | Report it with the request ID |
|
|
70
|
+
| `7` | Local state, network, contract, or infrastructure failure | Report it with the request ID. If the message names `contract_violation`, update the CLI first |
|
|
55
71
|
| `8` | Resource or Site not found | Run `sites list` for a valid Site ID |
|
|
56
72
|
| `127` | Shell cannot find `nuxtseo` | Install the CLI |
|
|
57
73
|
| `130` | Interrupted or cancelled | Check whether a mutation ran before retrying |
|