@nuxtseo/cli 0.2.1 → 0.3.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/dist/ansi.d.ts +8 -0
- package/dist/ansi.js +35 -0
- package/dist/cli.js +14 -5
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +85 -5
- package/dist/failures.d.ts +46 -6
- package/dist/failures.js +101 -10
- package/dist/pairing.js +2 -4
- package/dist/pull.d.ts +81 -0
- package/dist/pull.js +362 -0
- package/dist/render.d.ts +5 -5
- package/dist/render.js +1 -1
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.js +7 -2
- package/dist/skill.d.ts +99 -0
- package/dist/skill.js +372 -15
- package/dist/update-check.d.ts +5 -0
- package/dist/update-check.js +18 -1
- package/package.json +3 -3
- package/skills/nuxtseo-cli/SKILL.md +57 -0
- package/skills/nuxtseo-cli/references/commands.md +103 -12
- package/skills/nuxtseo-cli/references/protocol.md +72 -1
package/dist/pull.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { publicV1BooleanFlagValue } from '@nuxtseo/protocol/v1/core';
|
|
2
|
+
import { EXIT_CODE, fail, fromSdkFailure, ok } from './failures.js';
|
|
3
|
+
import { DEFAULT_TIMEOUT_MS, parseChoice, parseInteger } from './parse.js';
|
|
4
|
+
import { writeDiagnostic, writeProtocolResponse } from './runtime.js';
|
|
5
|
+
/** Search Console periods `pull` accepts, mirrored from `search analytics`. */
|
|
6
|
+
export const PULL_PERIODS = ['7d', '28d', '3m', '6m', '12m'];
|
|
7
|
+
function operation(name, request) {
|
|
8
|
+
return { name, spendsResearch: false, request };
|
|
9
|
+
}
|
|
10
|
+
function researchOperation(name, request) {
|
|
11
|
+
return { name, spendsResearch: true, request };
|
|
12
|
+
}
|
|
13
|
+
const SEARCH_ANALYTICS_VIEWS = ['pages', 'keywords', 'countries', 'devices', 'timeseries'];
|
|
14
|
+
const ANALYTICS_VIEWS = ['performance', 'top-pages', 'source-medium', 'key-events', 'countries', 'devices'];
|
|
15
|
+
/**
|
|
16
|
+
* Per-operation row limits.
|
|
17
|
+
*
|
|
18
|
+
* `search indexing urls` timed out twice at 500 rows on the 30000 ms deadline
|
|
19
|
+
* and answered at 50. A dump needs every operation to finish, so the limits
|
|
20
|
+
* here favour completion over depth. If one read needs more rows, run that
|
|
21
|
+
* command alone with `--all`.
|
|
22
|
+
*/
|
|
23
|
+
const SEARCH_INDEXING_URL_LIMIT = 50;
|
|
24
|
+
const SITEMAP_URL_LIMIT = 200;
|
|
25
|
+
/**
|
|
26
|
+
* Every read `pull` knows, in the order it writes them.
|
|
27
|
+
*
|
|
28
|
+
* Only Site scoped, read-only, stored-evidence operations belong here. A
|
|
29
|
+
* mutation is excluded because a dump must never change server state. An
|
|
30
|
+
* operation that needs an argument a dump cannot invent, such as a URL, an
|
|
31
|
+
* action ID or a Scan ID, is excluded because there is nothing to pass.
|
|
32
|
+
*/
|
|
33
|
+
export const PULL_OPERATIONS = [
|
|
34
|
+
operation('status', context => context.client.sites.status({
|
|
35
|
+
params: { siteId: context.siteId },
|
|
36
|
+
}, { signal: context.signal })),
|
|
37
|
+
operation('actions list', context => context.client.actions.list({
|
|
38
|
+
params: { siteId: context.siteId },
|
|
39
|
+
query: { limit: 25, offset: 0 },
|
|
40
|
+
}, { signal: context.signal })),
|
|
41
|
+
operation('audit changes', context => context.client.audit.changes({
|
|
42
|
+
params: { siteId: context.siteId },
|
|
43
|
+
query: {},
|
|
44
|
+
}, { signal: context.signal })),
|
|
45
|
+
operation('audit content-decay', context => context.client.audit.contentDecay({
|
|
46
|
+
params: { siteId: context.siteId },
|
|
47
|
+
}, { signal: context.signal })),
|
|
48
|
+
operation('audit duplicates', context => context.client.audit.duplicateClusters({
|
|
49
|
+
params: { siteId: context.siteId },
|
|
50
|
+
}, { signal: context.signal })),
|
|
51
|
+
operation('audit link-opportunities', context => context.client.audit.linkOpportunities({
|
|
52
|
+
params: { siteId: context.siteId },
|
|
53
|
+
}, { signal: context.signal })),
|
|
54
|
+
operation('audit link-structure', context => context.client.audit.linkStructure({
|
|
55
|
+
params: { siteId: context.siteId },
|
|
56
|
+
}, { signal: context.signal })),
|
|
57
|
+
operation('performance', context => context.client.performance.overview({
|
|
58
|
+
params: { siteId: context.siteId },
|
|
59
|
+
}, { signal: context.signal })),
|
|
60
|
+
operation('scans list', context => context.client.performance.scans({
|
|
61
|
+
params: { siteId: context.siteId },
|
|
62
|
+
query: { limit: 25 },
|
|
63
|
+
}, { signal: context.signal })),
|
|
64
|
+
operation('scans pages', context => context.client.performance.monitoredPages({
|
|
65
|
+
params: { siteId: context.siteId },
|
|
66
|
+
}, { signal: context.signal })),
|
|
67
|
+
operation('vitals summary', context => context.client.performance.vitals({
|
|
68
|
+
params: { siteId: context.siteId },
|
|
69
|
+
query: { formFactor: 'PHONE', view: 'summary' },
|
|
70
|
+
}, { signal: context.signal })),
|
|
71
|
+
operation('vitals trend', context => context.client.performance.vitals({
|
|
72
|
+
params: { siteId: context.siteId },
|
|
73
|
+
query: { formFactor: 'PHONE', view: 'trend' },
|
|
74
|
+
}, { signal: context.signal })),
|
|
75
|
+
operation('vitals findings', context => context.client.performance.vitalFindings({
|
|
76
|
+
params: { siteId: context.siteId },
|
|
77
|
+
query: { onlyFailing: publicV1BooleanFlagValue(true), limit: 20, offset: 0 },
|
|
78
|
+
}, { signal: context.signal })),
|
|
79
|
+
operation('search status', context => context.client.search.readStatus({
|
|
80
|
+
params: { siteId: context.siteId },
|
|
81
|
+
}, { signal: context.signal })),
|
|
82
|
+
...SEARCH_ANALYTICS_VIEWS.map(view => operation(`search analytics ${view}`, context => context.client.search.queryAnalytics({
|
|
83
|
+
params: { siteId: context.siteId },
|
|
84
|
+
query: { view, period: context.period, limit: 25, page: 1, sort: 'clicks', sortDir: 'desc' },
|
|
85
|
+
}, { signal: context.signal }))),
|
|
86
|
+
operation('search indexing summary', context => context.client.search.readIndexingDiagnostics({
|
|
87
|
+
params: { siteId: context.siteId },
|
|
88
|
+
query: { view: 'summary', limit: SEARCH_INDEXING_URL_LIMIT, offset: 0 },
|
|
89
|
+
}, { signal: context.signal })),
|
|
90
|
+
operation('search indexing urls', context => context.client.search.readIndexingDiagnostics({
|
|
91
|
+
params: { siteId: context.siteId },
|
|
92
|
+
query: { view: 'urls', limit: SEARCH_INDEXING_URL_LIMIT, offset: 0 },
|
|
93
|
+
}, { signal: context.signal })),
|
|
94
|
+
operation('search cohorts', context => context.client.search.readIndexingCohorts({
|
|
95
|
+
params: { siteId: context.siteId },
|
|
96
|
+
query: { limit: 12, minSectionPages: 5 },
|
|
97
|
+
}, { signal: context.signal })),
|
|
98
|
+
operation('search index-history', context => context.client.search.readIndexingHistory({
|
|
99
|
+
params: { siteId: context.siteId },
|
|
100
|
+
query: { days: 180 },
|
|
101
|
+
}, { signal: context.signal })),
|
|
102
|
+
operation('sitemaps list', context => context.client.sitemaps.list({
|
|
103
|
+
params: { siteId: context.siteId },
|
|
104
|
+
}, { signal: context.signal })),
|
|
105
|
+
operation('sitemaps urls', context => context.client.sitemaps.listUrls({
|
|
106
|
+
params: { siteId: context.siteId },
|
|
107
|
+
query: { limit: SITEMAP_URL_LIMIT },
|
|
108
|
+
}, { signal: context.signal })),
|
|
109
|
+
...ANALYTICS_VIEWS.map(view => operation(`analytics ${view}`, context => context.client.analytics.query({
|
|
110
|
+
params: { siteId: context.siteId, view },
|
|
111
|
+
query: {
|
|
112
|
+
period: '28d',
|
|
113
|
+
compare: 'previous',
|
|
114
|
+
stableData: true,
|
|
115
|
+
hostScope: true,
|
|
116
|
+
performancePhase: 'full',
|
|
117
|
+
comparePrior: true,
|
|
118
|
+
fresh: false,
|
|
119
|
+
},
|
|
120
|
+
}, { signal: context.signal }))),
|
|
121
|
+
operation('research overview', context => context.client.research.overview({
|
|
122
|
+
params: { siteId: context.siteId },
|
|
123
|
+
}, { signal: context.signal })),
|
|
124
|
+
operation('backlinks recoverable', context => context.client.backlinks.recoverable({
|
|
125
|
+
params: { siteId: context.siteId },
|
|
126
|
+
query: { limit: 100, offset: 0 },
|
|
127
|
+
}, { signal: context.signal })),
|
|
128
|
+
operation('mentions list', context => context.client.mentions.list({
|
|
129
|
+
params: { siteId: context.siteId },
|
|
130
|
+
query: { limit: 100, includeFiltered: publicV1BooleanFlagValue(false) },
|
|
131
|
+
}, { signal: context.signal })),
|
|
132
|
+
operation('content briefs list', context => context.client.content.listBriefs({
|
|
133
|
+
params: { siteId: context.siteId },
|
|
134
|
+
query: { limit: 25, offset: 0 },
|
|
135
|
+
}, { signal: context.signal })),
|
|
136
|
+
operation('timeline list', context => context.client.timeline.entries({
|
|
137
|
+
params: { siteId: context.siteId },
|
|
138
|
+
query: { limit: 25 },
|
|
139
|
+
}, { signal: context.signal })),
|
|
140
|
+
operation('annotations list', context => context.client.timeline.listAnnotations({
|
|
141
|
+
params: { siteId: context.siteId },
|
|
142
|
+
}, { signal: context.signal })),
|
|
143
|
+
researchOperation('backlinks summary', context => context.client.backlinks.summary({
|
|
144
|
+
params: { siteId: context.siteId },
|
|
145
|
+
}, { signal: context.signal })),
|
|
146
|
+
researchOperation('backlinks referring-domains', context => context.client.backlinks.referringDomains({
|
|
147
|
+
params: { siteId: context.siteId },
|
|
148
|
+
query: { limit: 100 },
|
|
149
|
+
}, { signal: context.signal })),
|
|
150
|
+
researchOperation('backlinks anchors', context => context.client.backlinks.anchors({
|
|
151
|
+
params: { siteId: context.siteId },
|
|
152
|
+
query: { limit: 100 },
|
|
153
|
+
}, { signal: context.signal })),
|
|
154
|
+
researchOperation('backlinks history', context => context.client.backlinks.history({
|
|
155
|
+
params: { siteId: context.siteId },
|
|
156
|
+
query: {},
|
|
157
|
+
}, { signal: context.signal })),
|
|
158
|
+
];
|
|
159
|
+
function selectorTokens(input) {
|
|
160
|
+
return (input ?? '').split(',').map(token => token.trim()).filter(Boolean);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A token selects an operation when it names that operation, or names a
|
|
164
|
+
* command that contains it. `audit` selects every `audit *` read, so a caller
|
|
165
|
+
* narrows a family without spelling each leaf.
|
|
166
|
+
*/
|
|
167
|
+
function matches(token, name) {
|
|
168
|
+
return name === token || name.startsWith(`${token} `);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The operations one `pull` run performs.
|
|
172
|
+
*
|
|
173
|
+
* The default set is every spend-free read. `--with-research` adds the reads
|
|
174
|
+
* that can start live research. `--include` narrows to the named commands, and
|
|
175
|
+
* naming a research read there is the same consent as `--with-research`.
|
|
176
|
+
* `--exclude` then removes what is left. An unknown name fails before any
|
|
177
|
+
* request, so a typo costs nothing.
|
|
178
|
+
*/
|
|
179
|
+
export function selectPullOperations(selection, operations = PULL_OPERATIONS) {
|
|
180
|
+
const include = selectorTokens(selection.include);
|
|
181
|
+
const exclude = selectorTokens(selection.exclude);
|
|
182
|
+
const unknown = [...include, ...exclude]
|
|
183
|
+
.filter(token => !operations.some(candidate => matches(token, candidate.name)));
|
|
184
|
+
if (unknown.length > 0) {
|
|
185
|
+
return fail(EXIT_CODE.invalidInput, [
|
|
186
|
+
`invalid_cli_input: No pull operation is named ${unknown.map(token => JSON.stringify(token)).join(', ')}.`,
|
|
187
|
+
`Valid names: ${operations.map(candidate => candidate.name).join(', ')}.`,
|
|
188
|
+
].join('\n'));
|
|
189
|
+
}
|
|
190
|
+
const selected = operations.filter((candidate) => {
|
|
191
|
+
const chosen = include.length > 0
|
|
192
|
+
? include.some(token => matches(token, candidate.name))
|
|
193
|
+
: !candidate.spendsResearch || selection.withResearch === true;
|
|
194
|
+
return chosen && !exclude.some(token => matches(token, candidate.name));
|
|
195
|
+
});
|
|
196
|
+
if (selected.length === 0)
|
|
197
|
+
return fail(EXIT_CODE.invalidInput, 'invalid_cli_input: The selected filters leave no operation to run.');
|
|
198
|
+
return ok(selected);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* How bad each outcome is, so one run reports its worst.
|
|
202
|
+
*
|
|
203
|
+
* Authentication outranks the rest because it stops the run: every later read
|
|
204
|
+
* would fail the same way. Interrupted outranks that again, because the caller
|
|
205
|
+
* asked for the stop.
|
|
206
|
+
*/
|
|
207
|
+
const EXIT_SEVERITY = new Map([
|
|
208
|
+
[EXIT_CODE.success, 0],
|
|
209
|
+
[EXIT_CODE.pagingCapReached, 1],
|
|
210
|
+
[EXIT_CODE.retryable, 2],
|
|
211
|
+
[EXIT_CODE.conflict, 3],
|
|
212
|
+
[EXIT_CODE.notFound, 4],
|
|
213
|
+
[EXIT_CODE.authorization, 5],
|
|
214
|
+
[EXIT_CODE.invalidInput, 6],
|
|
215
|
+
[EXIT_CODE.infrastructure, 7],
|
|
216
|
+
[EXIT_CODE.authentication, 8],
|
|
217
|
+
[EXIT_CODE.interrupted, 9],
|
|
218
|
+
]);
|
|
219
|
+
export function worstExitCode(codes) {
|
|
220
|
+
return codes.reduce((worst, code) => (EXIT_SEVERITY.get(code) ?? 0) > (EXIT_SEVERITY.get(worst) ?? 0) ? code : worst, EXIT_CODE.success);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* One read as one NDJSON line.
|
|
224
|
+
*
|
|
225
|
+
* The server envelope is passed through whole, with one `command` field added
|
|
226
|
+
* beside `data` and `meta`. Nothing is merged, renamed, re-ranked or
|
|
227
|
+
* unwrapped, so a line reads exactly like the single command's own output. A
|
|
228
|
+
* failure with no server body writes a `CliError` value instead, tagged the
|
|
229
|
+
* same way.
|
|
230
|
+
*/
|
|
231
|
+
export function pullLine(command, result) {
|
|
232
|
+
if (result._tag === 'Ok')
|
|
233
|
+
return { line: { command, ...result.value }, exitCode: EXIT_CODE.success };
|
|
234
|
+
const converted = fromSdkFailure(result.error);
|
|
235
|
+
const failure = converted.error;
|
|
236
|
+
const body = failure.protocolResponse;
|
|
237
|
+
return {
|
|
238
|
+
line: body !== undefined && body !== null && typeof body === 'object'
|
|
239
|
+
? { command, ...body }
|
|
240
|
+
: {
|
|
241
|
+
command,
|
|
242
|
+
_tag: 'CliError',
|
|
243
|
+
schemaVersion: 1,
|
|
244
|
+
error: { code: failure.code, exitCode: failure.exitCode, message: failure.message },
|
|
245
|
+
},
|
|
246
|
+
exitCode: failure.exitCode,
|
|
247
|
+
failure,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async function forEachBounded(items, concurrency, worker) {
|
|
251
|
+
let cursor = 0;
|
|
252
|
+
const next = async () => {
|
|
253
|
+
while (cursor < items.length) {
|
|
254
|
+
const index = cursor++;
|
|
255
|
+
await worker(items[index], index);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, next));
|
|
259
|
+
}
|
|
260
|
+
export const PULL_DEFAULT_CONCURRENCY = 4;
|
|
261
|
+
export const PULL_MAX_CONCURRENCY = 8;
|
|
262
|
+
function spendNotice(operations) {
|
|
263
|
+
const spending = operations.filter(candidate => candidate.spendsResearch);
|
|
264
|
+
return spending.length === 0
|
|
265
|
+
? undefined
|
|
266
|
+
: [
|
|
267
|
+
`These reads can start live research and draw on the Team research allowance: ${spending.map(candidate => candidate.name).join(', ')}.`,
|
|
268
|
+
'Each envelope reports cache use in `evidence`. Run `nuxtseo usage` to read the allowance.',
|
|
269
|
+
].join('\n');
|
|
270
|
+
}
|
|
271
|
+
export async function runPull(runtime, globals, args, resolve, catalogue = PULL_OPERATIONS) {
|
|
272
|
+
// `pull` answers with NDJSON and nothing else. There is no text rendering
|
|
273
|
+
// for a stream of thirty envelopes, so the flag is required rather than
|
|
274
|
+
// silently ignored.
|
|
275
|
+
if (!globals.json)
|
|
276
|
+
return fail(EXIT_CODE.invalidInput, 'invalid_cli_input: `pull` writes NDJSON only. Run it with --json.');
|
|
277
|
+
const period = parseChoice(args.period, { name: '--period', choices: PULL_PERIODS, defaultValue: '28d' });
|
|
278
|
+
if (period._tag === 'Err')
|
|
279
|
+
return period;
|
|
280
|
+
const concurrency = parseInteger(args.concurrency, {
|
|
281
|
+
name: '--concurrency',
|
|
282
|
+
minimum: 1,
|
|
283
|
+
maximum: PULL_MAX_CONCURRENCY,
|
|
284
|
+
defaultValue: PULL_DEFAULT_CONCURRENCY,
|
|
285
|
+
});
|
|
286
|
+
if (concurrency._tag === 'Err')
|
|
287
|
+
return concurrency;
|
|
288
|
+
const selected = selectPullOperations({
|
|
289
|
+
include: args.include,
|
|
290
|
+
exclude: args.exclude,
|
|
291
|
+
withResearch: args.withResearch,
|
|
292
|
+
}, catalogue);
|
|
293
|
+
if (selected._tag === 'Err')
|
|
294
|
+
return selected;
|
|
295
|
+
const resolved = await resolve(runtime, globals);
|
|
296
|
+
if (resolved._tag === 'Err')
|
|
297
|
+
return resolved;
|
|
298
|
+
const operations = selected.value;
|
|
299
|
+
const notice = spendNotice(operations);
|
|
300
|
+
if (notice)
|
|
301
|
+
writeDiagnostic(runtime, notice);
|
|
302
|
+
writeDiagnostic(runtime, `Pulling ${operations.length} reads for ${resolved.value.siteId}, ${concurrency.value} at a time.`);
|
|
303
|
+
// Every read gets its own deadline. The global `--timeout-ms` signal starts
|
|
304
|
+
// when the process starts, so sharing it would abort the later reads of a
|
|
305
|
+
// long run before they were even sent.
|
|
306
|
+
const timeoutMs = runtime.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
307
|
+
const exitCodes = [];
|
|
308
|
+
const skipped = [];
|
|
309
|
+
let stopped = false;
|
|
310
|
+
let completed = 0;
|
|
311
|
+
await forEachBounded(operations, concurrency.value, async (candidate) => {
|
|
312
|
+
if (stopped) {
|
|
313
|
+
skipped.push(candidate.name);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const signal = AbortSignal.any([runtime.signal, AbortSignal.timeout(timeoutMs)]);
|
|
317
|
+
const result = await candidate.request({
|
|
318
|
+
client: resolved.value.api.client,
|
|
319
|
+
siteId: resolved.value.siteId,
|
|
320
|
+
period: period.value,
|
|
321
|
+
signal,
|
|
322
|
+
});
|
|
323
|
+
const outcome = pullLine(candidate.name, result);
|
|
324
|
+
writeProtocolResponse(runtime, outcome.line);
|
|
325
|
+
exitCodes.push(outcome.exitCode);
|
|
326
|
+
completed++;
|
|
327
|
+
// An expired or rejected credential fails every later read the same way.
|
|
328
|
+
// Stopping there saves the requests and keeps the reason on one line.
|
|
329
|
+
if (outcome.exitCode === EXIT_CODE.authentication || outcome.exitCode === EXIT_CODE.interrupted)
|
|
330
|
+
stopped = true;
|
|
331
|
+
writeDiagnostic(runtime, `[${completed}/${operations.length}] ${candidate.name} ${outcome.failure ? `failed: ${outcome.failure.code}` : 'ok'}`);
|
|
332
|
+
});
|
|
333
|
+
const failed = exitCodes.filter(code => code !== EXIT_CODE.success).length;
|
|
334
|
+
writeDiagnostic(runtime, [
|
|
335
|
+
`Pull finished: ${exitCodes.length - failed} ok, ${failed} failed`,
|
|
336
|
+
skipped.length > 0 ? `, ${skipped.length} skipped after an authentication failure` : '',
|
|
337
|
+
'.',
|
|
338
|
+
].join(''));
|
|
339
|
+
if (skipped.length > 0)
|
|
340
|
+
writeDiagnostic(runtime, `Skipped: ${skipped.join(', ')}.`);
|
|
341
|
+
const worst = worstExitCode(exitCodes);
|
|
342
|
+
if (worst === EXIT_CODE.success)
|
|
343
|
+
return ok(undefined);
|
|
344
|
+
const message = `pull_incomplete: ${failed} of ${operations.length} reads failed. Read each failed line for the cause.`;
|
|
345
|
+
// The run summary is the last NDJSON line, so stdout stays one value per
|
|
346
|
+
// line and the caller never has to read stderr to learn the outcome.
|
|
347
|
+
return {
|
|
348
|
+
_tag: 'Err',
|
|
349
|
+
error: {
|
|
350
|
+
_tag: 'CliFailure',
|
|
351
|
+
code: 'pull_incomplete',
|
|
352
|
+
exitCode: worst,
|
|
353
|
+
message,
|
|
354
|
+
protocolResponse: {
|
|
355
|
+
command: 'pull',
|
|
356
|
+
_tag: 'CliError',
|
|
357
|
+
schemaVersion: 1,
|
|
358
|
+
error: { code: 'pull_incomplete', exitCode: worst, message },
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
package/dist/render.d.ts
CHANGED
|
@@ -3,10 +3,10 @@ import type { AccountTokenData } from '@nuxtseo/protocol/v1/account';
|
|
|
3
3
|
import type { ActionDismiss, ActionList, ActionResolve, ActionShow } from '@nuxtseo/protocol/v1/actions';
|
|
4
4
|
import type { AnalyticsQueryData, AnalyticsView } from '@nuxtseo/protocol/v1/analytics';
|
|
5
5
|
import type { AuditChanges, AuditContentDecay, AuditDuplicateClusters, AuditLinkOpportunities, AuditLinkStructure } from '@nuxtseo/protocol/v1/audit';
|
|
6
|
-
import type { BacklinkAnchors, BacklinksHistory, BacklinksSummary,
|
|
6
|
+
import type { BacklinkAnchors, BacklinksHistory, BacklinksSummary, MentionsData, RecoverableBacklinksData, ReferringDomains } from '@nuxtseo/protocol/v1/backlinks';
|
|
7
7
|
import type { ContentBrief, ContentBriefCreateData, ContentBriefListData } from '@nuxtseo/protocol/v1/content';
|
|
8
8
|
import type { IndexCohorts, IndexingDiagnosticsData, IndexingHistoryData, SearchAnalyticsData, SearchStatusData, UrlInspectionData } from '@nuxtseo/protocol/v1/gsc';
|
|
9
|
-
import type {
|
|
9
|
+
import type { PageInspectData, PageIssues, PageScan } from '@nuxtseo/protocol/v1/pages';
|
|
10
10
|
import type { FieldVitalFindings, FieldVitals, MonitoredPages, PerformanceScanDetail, PerformanceScans, SitePerformanceOverview } from '@nuxtseo/protocol/v1/performance';
|
|
11
11
|
import type { DomainAvailability, DomainTraffic, KeywordResearchData, RankingsResearchData, SerpResearchData, StoredResearchOverview } from '@nuxtseo/protocol/v1/research';
|
|
12
12
|
import type { SiteSitemapActionResponse, SiteSitemapsResponse, SiteSitemapUrlsResponse } from '@nuxtseo/protocol/v1/sitemaps';
|
|
@@ -16,7 +16,7 @@ export declare function renderUsage(response: AccountUsageResponse): string;
|
|
|
16
16
|
export declare function renderActions(data: ActionList): string;
|
|
17
17
|
export declare function renderAction(data: ActionShow): string;
|
|
18
18
|
export declare function renderActionResolution(data: ActionResolve): string;
|
|
19
|
-
export declare function renderPageInspection(data:
|
|
19
|
+
export declare function renderPageInspection(data: PageInspectData): string;
|
|
20
20
|
export declare function renderPageScan(data: PageScan): string;
|
|
21
21
|
export declare function renderPerformance(data: SitePerformanceOverview): string;
|
|
22
22
|
export declare function renderSearchStatus(data: SearchStatusData): string;
|
|
@@ -46,8 +46,8 @@ export declare function renderContentDecay(data: AuditContentDecay): string;
|
|
|
46
46
|
export declare function renderDuplicateClusters(data: AuditDuplicateClusters): string;
|
|
47
47
|
export declare function renderLinkOpportunities(data: AuditLinkOpportunities): string;
|
|
48
48
|
export declare function renderTimeline(data: TimelineEntries): string;
|
|
49
|
-
export declare function renderRecoverableBacklinks(data:
|
|
50
|
-
export declare function renderMentions(data:
|
|
49
|
+
export declare function renderRecoverableBacklinks(data: RecoverableBacklinksData): string;
|
|
50
|
+
export declare function renderMentions(data: MentionsData): string;
|
|
51
51
|
export declare function renderFieldVitals(data: FieldVitals): string;
|
|
52
52
|
export declare function renderFieldVitalFindings(data: FieldVitalFindings): string;
|
|
53
53
|
export declare function renderBacklinksSummary(data: BacklinksSummary): string;
|
package/dist/render.js
CHANGED
|
@@ -190,7 +190,7 @@ export function renderSearchAnalytics(data) {
|
|
|
190
190
|
return [
|
|
191
191
|
`${data.preset} (${data.rows.length} of ${data.total}) ${data.description}`,
|
|
192
192
|
...data.rows.map(row => [
|
|
193
|
-
`${row.clicks} clicks ${row.impressions} impressions position ${row.pos.toFixed(1)}`,
|
|
193
|
+
`${row.clicks} clicks ${row.impressions} impressions position ${row.pos === null ? 'none' : row.pos.toFixed(1)}`,
|
|
194
194
|
` ${row.keyword}`,
|
|
195
195
|
row.page ? ` ${row.page}` : '',
|
|
196
196
|
].filter(Boolean).join('\n')),
|
package/dist/runtime.d.ts
CHANGED
|
@@ -9,11 +9,16 @@ export interface CliRuntime {
|
|
|
9
9
|
inputIsTTY: boolean;
|
|
10
10
|
interactive: boolean;
|
|
11
11
|
signal: AbortSignal;
|
|
12
|
+
/** Read once per request. Each read carries its own `--timeout-ms` deadline. */
|
|
12
13
|
requestSignal: AbortSignal;
|
|
13
14
|
requestTimeoutMs?: number;
|
|
14
15
|
readStdin: () => Promise<string>;
|
|
15
16
|
}
|
|
16
17
|
export declare function writeOutput(runtime: CliRuntime, text: string): void;
|
|
17
18
|
export declare function writeDiagnostic(runtime: CliRuntime, text: string): void;
|
|
19
|
+
/**
|
|
20
|
+
* Machine output is written through these two functions only. Both strip ANSI
|
|
21
|
+
* escapes, so no dependency can colour a string into a JSON envelope.
|
|
22
|
+
*/
|
|
18
23
|
export declare function writeProtocolResponse(runtime: CliRuntime, response: unknown): void;
|
|
19
24
|
export declare function writeCliResponse(runtime: CliRuntime, response: unknown): void;
|
package/dist/runtime.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { stringifyWithoutAnsi } from './ansi.js';
|
|
1
2
|
export function writeOutput(runtime, text) {
|
|
2
3
|
runtime.output.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
3
4
|
}
|
|
4
5
|
export function writeDiagnostic(runtime, text) {
|
|
5
6
|
runtime.error.write(text.endsWith('\n') ? text : `${text}\n`);
|
|
6
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Machine output is written through these two functions only. Both strip ANSI
|
|
10
|
+
* escapes, so no dependency can colour a string into a JSON envelope.
|
|
11
|
+
*/
|
|
7
12
|
export function writeProtocolResponse(runtime, response) {
|
|
8
|
-
writeOutput(runtime,
|
|
13
|
+
writeOutput(runtime, stringifyWithoutAnsi(response));
|
|
9
14
|
}
|
|
10
15
|
export function writeCliResponse(runtime, response) {
|
|
11
|
-
writeOutput(runtime,
|
|
16
|
+
writeOutput(runtime, stringifyWithoutAnsi(response));
|
|
12
17
|
}
|
package/dist/skill.d.ts
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import type { CliResult } from './failures.js';
|
|
2
|
+
import type { StatePaths } from './state/index.js';
|
|
2
3
|
export declare const SKILL_NAME = "nuxtseo-cli";
|
|
4
|
+
/** The stamp an install leaves beside the skill so a later run can read its version. */
|
|
5
|
+
export declare const SKILL_VERSION_FILENAME = ".skill-version.json";
|
|
6
|
+
/** The skill entry file, which also carries the version in its frontmatter. */
|
|
7
|
+
export declare const SKILL_MARKDOWN_FILENAME = "SKILL.md";
|
|
8
|
+
/**
|
|
9
|
+
* How long one answer stays authoritative, matching the npm update check.
|
|
10
|
+
*
|
|
11
|
+
* Declared here rather than imported so `update-check.ts` can depend on this
|
|
12
|
+
* module and the two never form an import cycle.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SKILL_CHECK_INTERVAL_MS: number;
|
|
3
15
|
export declare const SKILL_AGENTS: readonly ['claude', 'codex'];
|
|
4
16
|
export type SkillAgent = typeof SKILL_AGENTS[number];
|
|
5
17
|
export interface SkillInstallation {
|
|
6
18
|
agent: SkillAgent;
|
|
7
19
|
source: string;
|
|
20
|
+
/** The path the caller asked for, which may be a symlink. */
|
|
8
21
|
destination: string;
|
|
22
|
+
/** Where the files actually landed, after following a symlinked destination. */
|
|
23
|
+
resolvedDestination: string;
|
|
24
|
+
version: string;
|
|
25
|
+
/** Files written by this install. */
|
|
26
|
+
written: number;
|
|
27
|
+
/** Files removed because this release no longer ships them. */
|
|
28
|
+
pruned: number;
|
|
9
29
|
}
|
|
10
30
|
/**
|
|
11
31
|
* The skill shipped inside this package. A global install puts it outside the
|
|
@@ -14,9 +34,88 @@ export interface SkillInstallation {
|
|
|
14
34
|
*/
|
|
15
35
|
export declare function skillSourceDirectory(moduleUrl?: string): string;
|
|
16
36
|
export declare function skillDestination(homeDirectory: string, agent: SkillAgent): string;
|
|
37
|
+
/**
|
|
38
|
+
* The install target, followed through a symlink.
|
|
39
|
+
*
|
|
40
|
+
* People keep their skills in one directory and link each agent's skill folder
|
|
41
|
+
* at it. `fs.cp` lstats the target, sees a link rather than a directory, and
|
|
42
|
+
* refuses with `ERR_FS_CP_DIR_TO_NON_DIR` before it copies anything. That
|
|
43
|
+
* reported as a permission problem on a directory the user owned and could
|
|
44
|
+
* write. Follow the link first so the copy writes where the person pointed it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveSkillTarget(destination: string): Promise<string>;
|
|
47
|
+
/** An errno or Node error as a person can act on it: code, call, and path. */
|
|
48
|
+
export declare function describeIoCause(cause: unknown): string;
|
|
49
|
+
/**
|
|
50
|
+
* Write the running version into the skill frontmatter.
|
|
51
|
+
*
|
|
52
|
+
* An agent reads `SKILL.md` and nothing else, so the file has to say which
|
|
53
|
+
* release it came from. Without it a skill from an older CLI hides every
|
|
54
|
+
* command added since, and the agent cannot tell.
|
|
55
|
+
*/
|
|
56
|
+
export declare function stampSkillVersion(markdown: string, version: string): string;
|
|
57
|
+
/** The version recorded in a `SKILL.md` frontmatter, when it carries one. */
|
|
58
|
+
export declare function frontmatterVersion(markdown: string): string | null;
|
|
59
|
+
export type InstalledSkill = {
|
|
60
|
+
_tag: 'SkillMissing';
|
|
61
|
+
} | {
|
|
62
|
+
_tag: 'SkillUnversioned';
|
|
63
|
+
} | {
|
|
64
|
+
_tag: 'SkillVersion';
|
|
65
|
+
version: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Which version of the skill sits in a destination directory.
|
|
69
|
+
*
|
|
70
|
+
* Reads the stamp first, then the frontmatter, so a skill installed by hand or
|
|
71
|
+
* by an older release still reports something the caller can compare.
|
|
72
|
+
*/
|
|
73
|
+
export declare function readInstalledSkill(destination: string): Promise<InstalledSkill>;
|
|
74
|
+
export interface SkillNotice {
|
|
75
|
+
agent: SkillAgent;
|
|
76
|
+
installed: string | null;
|
|
77
|
+
current: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A stale installed skill, or `null` when nothing needs saying.
|
|
81
|
+
*
|
|
82
|
+
* An absent skill is not stale: the person may not use one. Any other
|
|
83
|
+
* difference from the running binary is, in both directions, because the two
|
|
84
|
+
* describe different command sets.
|
|
85
|
+
*/
|
|
86
|
+
export declare function skillNoticeFor(installed: InstalledSkill, current: string, agent: SkillAgent): SkillNotice | null;
|
|
87
|
+
export declare function skillNoticeLine(notice: SkillNotice): string;
|
|
88
|
+
export interface SkillCheckCache {
|
|
89
|
+
lastCheckedAt: string;
|
|
90
|
+
cliVersion: string;
|
|
91
|
+
installed: Partial<Record<SkillAgent, string | null>>;
|
|
92
|
+
}
|
|
93
|
+
export declare function parseSkillCheckCache(content: string): SkillCheckCache | null;
|
|
94
|
+
/**
|
|
95
|
+
* Whether the filesystem has to be read again.
|
|
96
|
+
*
|
|
97
|
+
* A cache from a different binary is never reusable: the binary version is half
|
|
98
|
+
* of the comparison, so an upgrade must warn on the very next run.
|
|
99
|
+
*/
|
|
100
|
+
export declare function skillCheckDue(cache: SkillCheckCache | null, now: Date, current: string): boolean;
|
|
101
|
+
export interface SkillCheckOptions {
|
|
102
|
+
paths: StatePaths;
|
|
103
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
104
|
+
now?: () => Date;
|
|
105
|
+
/** The home the agent directories sit in; defaults to the state directory's parent. */
|
|
106
|
+
homeDirectory?: string;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* One line's worth of "your installed skill is not this binary".
|
|
110
|
+
*
|
|
111
|
+
* Costs one cached read per day, and nothing at all when the caller disabled
|
|
112
|
+
* update checks.
|
|
113
|
+
*/
|
|
114
|
+
export declare function checkSkillVersion(options: SkillCheckOptions): Promise<SkillNotice | null>;
|
|
17
115
|
export declare function installSkill(options: {
|
|
18
116
|
agent: SkillAgent;
|
|
19
117
|
homeDirectory: string;
|
|
20
118
|
target?: string;
|
|
21
119
|
sourceDirectory?: string;
|
|
120
|
+
paths?: StatePaths;
|
|
22
121
|
}): Promise<CliResult<SkillInstallation>>;
|