@gscdump/cli 1.5.4 → 2.0.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 +2 -2
- package/dist/commands/entities.mjs +233 -373
- package/dist/commands/sitemaps.mjs +216 -5
- package/dist/mcp/server/index.mjs +2 -2
- package/dist/package.mjs +1 -1
- package/dist/sitemap.mjs +77 -15
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -47,14 +47,14 @@ gscdump mcp
|
|
|
47
47
|
| `sites [--owner-only] [--with-sitemaps]` | List available GSC sites |
|
|
48
48
|
| `sites add <url>` / `sites delete <url> [--yes]` | Register / remove a property in Search Console (add registers in unverified state) |
|
|
49
49
|
| `sites verify-token <url> [--method]` / `sites verify <url> [--method]` | Get a verification token, then trigger ownership verification (META/FILE/DNS_TXT/DNS_CNAME/ANALYTICS/TAG_MANAGER) |
|
|
50
|
-
| `sitemaps` |
|
|
50
|
+
| `sitemaps` | GSC CRUD, explicit live probes, and hosted canonical reads (`current`, `history`, `membership`, `lastmod`, `export`) |
|
|
51
51
|
| `inspect <url>` / `inspect batch [--concurrency]` | URL inspection (single URL or batch from file/stdin); renders index status, rich results, AMP, mobile usability |
|
|
52
52
|
| `indexing` | Notify Google about URL changes (`submit`, `remove`, `status`, `batch [--concurrency] [--yes]`); supports `--retries` |
|
|
53
53
|
| `sync` | Sync GSC data to the local Parquet store; `--retry-failed`, `--dry-run` |
|
|
54
54
|
| `query` | Run a search analytics query (local store by default; `--live` hits GSC API). Filters: `--query`, `--page`, `--country`, `--device`, `--search-appearance`, `--type`, `--data-state`, `--aggregation-type`. `--explain` previews the request body; `--output -` writes to stdout. |
|
|
55
55
|
| `dump` | Export from the store to a directory (`--format parquet\|json\|ndjson\|csv`, `--tables`, `--all-sites`) |
|
|
56
56
|
| `analyze <tool>` | Run an SEO analyzer against the store (`--live` for row-based against fresh API) |
|
|
57
|
-
| `entities` | Snapshot URL inspections
|
|
57
|
+
| `entities` | Snapshot URL inspections and indexing metadata into the local entity store |
|
|
58
58
|
| `store stats` | Show row/byte counts per table and on-disk footprint |
|
|
59
59
|
| `store compact` | Roll daily partitions older than N days into monthly files (`--dry-run`) |
|
|
60
60
|
| `store gc` | Delete orphaned objects past the grace window (`--dry-run`) |
|
|
@@ -4,7 +4,7 @@ import process from "node:process";
|
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
6
|
import { Buffer } from "node:buffer";
|
|
7
|
-
import { createIndexingMetadataStore, createInspectionStore
|
|
7
|
+
import { createIndexingMetadataStore, createInspectionStore } from "@gscdump/engine/entities";
|
|
8
8
|
const INSPECTION_QPD_PER_PROPERTY = 2e3;
|
|
9
9
|
const INDEXING_NOT_FOUND_RE = /\b404\b|NOT_FOUND/i;
|
|
10
10
|
const INSPECTION_HISTORY_LOOKBACK_MONTHS = 24;
|
|
@@ -27,390 +27,250 @@ async function readUrlList(opts) {
|
|
|
27
27
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
28
28
|
return Buffer.concat(chunks).toString("utf8").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
29
29
|
}
|
|
30
|
-
const
|
|
31
|
-
meta: {
|
|
32
|
-
name: "inspect",
|
|
33
|
-
description: "Run URL Inspection for a list of URLs and persist results to the local entity store"
|
|
34
|
-
},
|
|
35
|
-
args: {
|
|
36
|
-
site: {
|
|
37
|
-
type: "string",
|
|
38
|
-
alias: "s",
|
|
39
|
-
description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
|
|
40
|
-
},
|
|
41
|
-
file: {
|
|
42
|
-
type: "string",
|
|
43
|
-
alias: "f",
|
|
44
|
-
description: "Path to a file with one URL per line. If omitted, reads from stdin."
|
|
45
|
-
},
|
|
46
|
-
limit: {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: `Max URLs to inspect this run (default: ${INSPECTION_QPD_PER_PROPERTY}, the per-property GSC daily quota)`
|
|
49
|
-
},
|
|
50
|
-
concurrency: {
|
|
51
|
-
type: "string",
|
|
52
|
-
alias: "c",
|
|
53
|
-
default: "4",
|
|
54
|
-
description: "Concurrent in-flight inspect calls (default: 4)"
|
|
55
|
-
},
|
|
56
|
-
...OUTPUT_ARGS
|
|
57
|
-
},
|
|
58
|
-
async run({ args }) {
|
|
59
|
-
const { json, quiet } = applyOutputMode(args);
|
|
60
|
-
const ctx = await createCommandContext({
|
|
61
|
-
needsAuth: true,
|
|
62
|
-
needsStore: true
|
|
63
|
-
});
|
|
64
|
-
const client = ctx.client;
|
|
65
|
-
const store = ctx.store;
|
|
66
|
-
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
67
|
-
const limit = args.limit ? Number.parseInt(String(args.limit), 10) : INSPECTION_QPD_PER_PROPERTY;
|
|
68
|
-
const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
|
|
69
|
-
const urls = (await readUrlList({ file: args.file ? String(args.file) : void 0 })).slice(0, limit);
|
|
70
|
-
if (urls.length === 0) {
|
|
71
|
-
logger.warn("No URLs to inspect.");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (urls.length === limit && limit < INSPECTION_QPD_PER_PROPERTY) logger.info(`Capping at --limit ${limit}`);
|
|
75
|
-
if (urls.length === INSPECTION_QPD_PER_PROPERTY) logger.info(`Hit per-property daily inspection quota (${INSPECTION_QPD_PER_PROPERTY}); remaining URLs will be queued for tomorrow.`);
|
|
76
|
-
const inspector = createInspectionStore({ dataSource: store.dataSource });
|
|
77
|
-
let completed = 0;
|
|
78
|
-
let failed = 0;
|
|
79
|
-
const records = [];
|
|
80
|
-
const failures = [];
|
|
81
|
-
await runWithConcurrency(urls, concurrency, async (url) => {
|
|
82
|
-
const result = await client.inspect(siteUrl, url).catch((err) => err);
|
|
83
|
-
if (result instanceof Error) {
|
|
84
|
-
failed++;
|
|
85
|
-
failures.push({
|
|
86
|
-
url,
|
|
87
|
-
error: result.message
|
|
88
|
-
});
|
|
89
|
-
} else {
|
|
90
|
-
const ix = result.inspectionResult;
|
|
91
|
-
const indexStatus = ix?.indexStatusResult;
|
|
92
|
-
records.push({
|
|
93
|
-
url,
|
|
94
|
-
inspectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
95
|
-
indexStatus: indexStatus?.verdict ?? void 0,
|
|
96
|
-
lastCrawlTime: indexStatus?.lastCrawlTime ?? void 0,
|
|
97
|
-
googleCanonical: indexStatus?.googleCanonical ?? void 0,
|
|
98
|
-
userCanonical: indexStatus?.userCanonical ?? void 0,
|
|
99
|
-
coverageState: indexStatus?.coverageState ?? void 0,
|
|
100
|
-
robotsTxtState: indexStatus?.robotsTxtState ?? void 0,
|
|
101
|
-
indexingState: indexStatus?.indexingState ?? void 0,
|
|
102
|
-
pageFetchState: indexStatus?.pageFetchState ?? void 0,
|
|
103
|
-
mobileUsabilityVerdict: ix?.mobileUsabilityResult?.verdict ?? void 0,
|
|
104
|
-
richResultsVerdict: ix?.richResultsResult?.verdict ?? void 0,
|
|
105
|
-
raw: ix
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
completed++;
|
|
109
|
-
if (!quiet) process.stdout.write(`\r${progressBar(completed, urls.length, `${url.slice(0, 60)}`)}`);
|
|
110
|
-
});
|
|
111
|
-
if (!quiet) process.stdout.write("\n");
|
|
112
|
-
await inspector.appendHistory({
|
|
113
|
-
userId: store.userId,
|
|
114
|
-
siteId: store.siteIdFor(siteUrl)
|
|
115
|
-
}, records);
|
|
116
|
-
if (json) console.log(JSON.stringify({
|
|
117
|
-
site: siteUrl,
|
|
118
|
-
inspected: records.length,
|
|
119
|
-
failed,
|
|
120
|
-
failures,
|
|
121
|
-
records
|
|
122
|
-
}, null, 2));
|
|
123
|
-
else if (!quiet) {
|
|
124
|
-
logger.success(`Inspected ${records.length}/${urls.length} URL(s)`);
|
|
125
|
-
if (failed > 0) {
|
|
126
|
-
logger.warn(`${failed} failed:`);
|
|
127
|
-
for (const f of failures.slice(0, 5)) console.log(` ${f.url}: ${f.error}`);
|
|
128
|
-
if (failures.length > 5) console.log(` ... and ${failures.length - 5} more`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (failed > 0) process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
const showSubCommand = defineCommand({
|
|
135
|
-
meta: {
|
|
136
|
-
name: "show",
|
|
137
|
-
description: "Print the latest inspection record for a URL from the local entity store"
|
|
138
|
-
},
|
|
139
|
-
args: {
|
|
140
|
-
...OUTPUT_ARGS,
|
|
141
|
-
site: {
|
|
142
|
-
type: "string",
|
|
143
|
-
alias: "s",
|
|
144
|
-
description: "Site URL (defaults to config.defaultSite or prompt)"
|
|
145
|
-
},
|
|
146
|
-
url: {
|
|
147
|
-
type: "positional",
|
|
148
|
-
required: true,
|
|
149
|
-
description: "URL to look up"
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
async run({ args }) {
|
|
153
|
-
const { json } = applyOutputMode(args);
|
|
154
|
-
const ctx = await createCommandContext({
|
|
155
|
-
needsAuth: true,
|
|
156
|
-
needsStore: true
|
|
157
|
-
});
|
|
158
|
-
const store = ctx.store;
|
|
159
|
-
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
160
|
-
const record = await findLatestInspection(createInspectionStore({ dataSource: store.dataSource }), {
|
|
161
|
-
userId: store.userId,
|
|
162
|
-
siteId: store.siteIdFor(siteUrl)
|
|
163
|
-
}, String(args.url));
|
|
164
|
-
if (!record) {
|
|
165
|
-
logger.warn(`No inspection record for ${args.url}`);
|
|
166
|
-
process.exit(1);
|
|
167
|
-
}
|
|
168
|
-
if (json) {
|
|
169
|
-
console.log(JSON.stringify(record, null, 2));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
console.log();
|
|
173
|
-
console.log(` \x1B[1m${record.url}\x1B[0m`);
|
|
174
|
-
console.log(` Inspected: ${record.inspectedAt}`);
|
|
175
|
-
if (record.indexStatus) console.log(` Index: ${record.indexStatus}`);
|
|
176
|
-
if (record.lastCrawlTime) console.log(` Last crawl: ${record.lastCrawlTime}`);
|
|
177
|
-
if (record.googleCanonical) console.log(` Canonical: ${record.googleCanonical}`);
|
|
178
|
-
if (record.coverageState) console.log(` Coverage: ${record.coverageState}`);
|
|
179
|
-
if (record.mobileUsabilityVerdict) console.log(` Mobile: ${record.mobileUsabilityVerdict}`);
|
|
180
|
-
if (record.richResultsVerdict) console.log(` Rich results: ${record.richResultsVerdict}`);
|
|
181
|
-
console.log();
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
const sitemapsSnapshotSubCommand = defineCommand({
|
|
185
|
-
meta: {
|
|
186
|
-
name: "snapshot",
|
|
187
|
-
description: "Fetch current sitemap state from GSC and persist to the local entity store"
|
|
188
|
-
},
|
|
189
|
-
args: {
|
|
190
|
-
...OUTPUT_ARGS,
|
|
191
|
-
site: {
|
|
192
|
-
type: "string",
|
|
193
|
-
alias: "s",
|
|
194
|
-
description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
async run({ args }) {
|
|
198
|
-
const { json, quiet } = applyOutputMode(args);
|
|
199
|
-
const ctx = await createCommandContext({
|
|
200
|
-
needsAuth: true,
|
|
201
|
-
needsStore: true
|
|
202
|
-
});
|
|
203
|
-
const client = ctx.client;
|
|
204
|
-
const store = ctx.store;
|
|
205
|
-
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
206
|
-
const apiSitemaps = await client.sitemaps.list(siteUrl);
|
|
207
|
-
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
208
|
-
const records = apiSitemaps.filter((s) => typeof s.path === "string").map((s) => ({
|
|
209
|
-
path: s.path,
|
|
210
|
-
capturedAt,
|
|
211
|
-
lastDownloaded: s.lastDownloaded ?? void 0,
|
|
212
|
-
lastSubmitted: s.lastSubmitted ?? void 0,
|
|
213
|
-
type: s.type ?? void 0,
|
|
214
|
-
isPending: s.isPending ?? void 0,
|
|
215
|
-
isSitemapsIndex: s.isSitemapsIndex ?? void 0,
|
|
216
|
-
errors: s.errors ?? void 0,
|
|
217
|
-
warnings: s.warnings ?? void 0,
|
|
218
|
-
contents: s.contents?.map((c) => ({
|
|
219
|
-
type: c.type ?? void 0,
|
|
220
|
-
submitted: c.submitted ?? void 0,
|
|
221
|
-
indexed: c.indexed ?? void 0
|
|
222
|
-
})),
|
|
223
|
-
raw: s
|
|
224
|
-
}));
|
|
225
|
-
await createSitemapStore({
|
|
226
|
-
dataSource: store.dataSource,
|
|
227
|
-
withMutation: store.withSitemapMutation
|
|
228
|
-
}).writeSnapshot({
|
|
229
|
-
userId: store.userId,
|
|
230
|
-
siteId: store.siteIdFor(siteUrl)
|
|
231
|
-
}, records);
|
|
232
|
-
if (json) {
|
|
233
|
-
console.log(JSON.stringify({
|
|
234
|
-
site: siteUrl,
|
|
235
|
-
capturedAt,
|
|
236
|
-
records
|
|
237
|
-
}, null, 2));
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (!quiet) {
|
|
241
|
-
logger.success(`Captured ${records.length} sitemap(s) for ${siteUrl}`);
|
|
242
|
-
for (const r of records) {
|
|
243
|
-
const errors = r.errors && r.errors !== "0" ? ` \x1B[31merr=${r.errors}\x1B[0m` : "";
|
|
244
|
-
const warnings = r.warnings && r.warnings !== "0" ? ` \x1B[33mwarn=${r.warnings}\x1B[0m` : "";
|
|
245
|
-
const downloaded = r.lastDownloaded ? ` last=${r.lastDownloaded}` : "";
|
|
246
|
-
console.log(` ${r.path}${downloaded}${errors}${warnings}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
const sitemapsShowSubCommand = defineCommand({
|
|
252
|
-
meta: {
|
|
253
|
-
name: "show",
|
|
254
|
-
description: "Print the latest captured sitemap state for a feedpath"
|
|
255
|
-
},
|
|
256
|
-
args: {
|
|
257
|
-
...OUTPUT_ARGS,
|
|
258
|
-
site: {
|
|
259
|
-
type: "string",
|
|
260
|
-
alias: "s",
|
|
261
|
-
description: "Site URL (defaults to config.defaultSite or prompt)"
|
|
262
|
-
},
|
|
263
|
-
path: {
|
|
264
|
-
type: "positional",
|
|
265
|
-
required: true,
|
|
266
|
-
description: "Sitemap path (feedpath)"
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
async run({ args }) {
|
|
270
|
-
const { json } = applyOutputMode(args);
|
|
271
|
-
const ctx = await createCommandContext({
|
|
272
|
-
needsAuth: true,
|
|
273
|
-
needsStore: true
|
|
274
|
-
});
|
|
275
|
-
const store = ctx.store;
|
|
276
|
-
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
277
|
-
const record = await createSitemapReadStore({ dataSource: store.dataSource }).getLatest({
|
|
278
|
-
userId: store.userId,
|
|
279
|
-
siteId: store.siteIdFor(siteUrl)
|
|
280
|
-
}, String(args.path));
|
|
281
|
-
if (!record) {
|
|
282
|
-
logger.warn(`No sitemap record for ${args.path}`);
|
|
283
|
-
process.exit(1);
|
|
284
|
-
}
|
|
285
|
-
if (json) {
|
|
286
|
-
console.log(JSON.stringify(record, null, 2));
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
console.log();
|
|
290
|
-
console.log(` \x1B[1m${record.path}\x1B[0m`);
|
|
291
|
-
console.log(` Captured: ${record.capturedAt}`);
|
|
292
|
-
if (record.lastDownloaded) console.log(` Downloaded: ${record.lastDownloaded}`);
|
|
293
|
-
if (record.lastSubmitted) console.log(` Submitted: ${record.lastSubmitted}`);
|
|
294
|
-
if (record.type) console.log(` Type: ${record.type}`);
|
|
295
|
-
if (record.errors) console.log(` Errors: ${record.errors}`);
|
|
296
|
-
if (record.warnings) console.log(` Warnings: ${record.warnings}`);
|
|
297
|
-
if (record.contents?.length) {
|
|
298
|
-
console.log(` Contents:`);
|
|
299
|
-
for (const c of record.contents) {
|
|
300
|
-
const bits = [
|
|
301
|
-
c.type,
|
|
302
|
-
c.submitted && `submitted=${c.submitted}`,
|
|
303
|
-
c.indexed && `indexed=${c.indexed}`
|
|
304
|
-
].filter(Boolean).join(" ");
|
|
305
|
-
console.log(` ${bits}`);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
console.log();
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
const indexingSubCommand = defineCommand({
|
|
30
|
+
const entitiesCommand = defineCommand({
|
|
312
31
|
meta: {
|
|
313
|
-
name: "
|
|
314
|
-
description: "
|
|
32
|
+
name: "entities",
|
|
33
|
+
description: "Manage local entity snapshots (URL inspections and indexing metadata)"
|
|
315
34
|
},
|
|
316
|
-
subCommands: {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
args: {
|
|
322
|
-
site: {
|
|
323
|
-
type: "string",
|
|
324
|
-
alias: "s",
|
|
325
|
-
description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
|
|
326
|
-
},
|
|
327
|
-
file: {
|
|
328
|
-
type: "string",
|
|
329
|
-
alias: "f",
|
|
330
|
-
description: "Path to a file with one URL per line. If omitted, reads from stdin."
|
|
35
|
+
subCommands: {
|
|
36
|
+
inspect: defineCommand({
|
|
37
|
+
meta: {
|
|
38
|
+
name: "inspect",
|
|
39
|
+
description: "Run URL Inspection for a list of URLs and persist results to the local entity store"
|
|
331
40
|
},
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
41
|
+
args: {
|
|
42
|
+
site: {
|
|
43
|
+
type: "string",
|
|
44
|
+
alias: "s",
|
|
45
|
+
description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
|
|
46
|
+
},
|
|
47
|
+
file: {
|
|
48
|
+
type: "string",
|
|
49
|
+
alias: "f",
|
|
50
|
+
description: "Path to a file with one URL per line. If omitted, reads from stdin."
|
|
51
|
+
},
|
|
52
|
+
limit: {
|
|
53
|
+
type: "string",
|
|
54
|
+
description: `Max URLs to inspect this run (default: ${INSPECTION_QPD_PER_PROPERTY}, the per-property GSC daily quota)`
|
|
55
|
+
},
|
|
56
|
+
concurrency: {
|
|
57
|
+
type: "string",
|
|
58
|
+
alias: "c",
|
|
59
|
+
default: "4",
|
|
60
|
+
description: "Concurrent in-flight inspect calls (default: 4)"
|
|
61
|
+
},
|
|
62
|
+
...OUTPUT_ARGS
|
|
337
63
|
},
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
needsAuth: true,
|
|
344
|
-
needsStore: true
|
|
345
|
-
});
|
|
346
|
-
const client = ctx.client;
|
|
347
|
-
const store = ctx.store;
|
|
348
|
-
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
349
|
-
const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
|
|
350
|
-
const urls = await readUrlList({ file: args.file ? String(args.file) : void 0 });
|
|
351
|
-
if (urls.length === 0) {
|
|
352
|
-
logger.warn("No URLs to fetch metadata for.");
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const records = [];
|
|
356
|
-
const failures = [];
|
|
357
|
-
let completed = 0;
|
|
358
|
-
await runWithConcurrency(urls, concurrency, async (url) => {
|
|
359
|
-
const result = await client.indexing.getMetadata(url).catch((err) => err);
|
|
360
|
-
if (result instanceof Error) if (INDEXING_NOT_FOUND_RE.test(result.message)) records.push({
|
|
361
|
-
url,
|
|
362
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
363
|
-
});
|
|
364
|
-
else failures.push({
|
|
365
|
-
url,
|
|
366
|
-
error: result.message
|
|
64
|
+
async run({ args }) {
|
|
65
|
+
const { json, quiet } = applyOutputMode(args);
|
|
66
|
+
const ctx = await createCommandContext({
|
|
67
|
+
needsAuth: true,
|
|
68
|
+
needsStore: true
|
|
367
69
|
});
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
70
|
+
const client = ctx.client;
|
|
71
|
+
const store = ctx.store;
|
|
72
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
73
|
+
const limit = args.limit ? Number.parseInt(String(args.limit), 10) : INSPECTION_QPD_PER_PROPERTY;
|
|
74
|
+
const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
|
|
75
|
+
const urls = (await readUrlList({ file: args.file ? String(args.file) : void 0 })).slice(0, limit);
|
|
76
|
+
if (urls.length === 0) {
|
|
77
|
+
logger.warn("No URLs to inspect.");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (urls.length === limit && limit < INSPECTION_QPD_PER_PROPERTY) logger.info(`Capping at --limit ${limit}`);
|
|
81
|
+
if (urls.length === INSPECTION_QPD_PER_PROPERTY) logger.info(`Hit per-property daily inspection quota (${INSPECTION_QPD_PER_PROPERTY}); remaining URLs will be queued for tomorrow.`);
|
|
82
|
+
const inspector = createInspectionStore({ dataSource: store.dataSource });
|
|
83
|
+
let completed = 0;
|
|
84
|
+
let failed = 0;
|
|
85
|
+
const records = [];
|
|
86
|
+
const failures = [];
|
|
87
|
+
await runWithConcurrency(urls, concurrency, async (url) => {
|
|
88
|
+
const result = await client.inspect(siteUrl, url).catch((err) => err);
|
|
89
|
+
if (result instanceof Error) {
|
|
90
|
+
failed++;
|
|
91
|
+
failures.push({
|
|
92
|
+
url,
|
|
93
|
+
error: result.message
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
const ix = result.inspectionResult;
|
|
97
|
+
const indexStatus = ix?.indexStatusResult;
|
|
98
|
+
records.push({
|
|
99
|
+
url,
|
|
100
|
+
inspectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
101
|
+
indexStatus: indexStatus?.verdict ?? void 0,
|
|
102
|
+
lastCrawlTime: indexStatus?.lastCrawlTime ?? void 0,
|
|
103
|
+
googleCanonical: indexStatus?.googleCanonical ?? void 0,
|
|
104
|
+
userCanonical: indexStatus?.userCanonical ?? void 0,
|
|
105
|
+
coverageState: indexStatus?.coverageState ?? void 0,
|
|
106
|
+
robotsTxtState: indexStatus?.robotsTxtState ?? void 0,
|
|
107
|
+
indexingState: indexStatus?.indexingState ?? void 0,
|
|
108
|
+
pageFetchState: indexStatus?.pageFetchState ?? void 0,
|
|
109
|
+
mobileUsabilityVerdict: ix?.mobileUsabilityResult?.verdict ?? void 0,
|
|
110
|
+
richResultsVerdict: ix?.richResultsResult?.verdict ?? void 0,
|
|
111
|
+
raw: ix
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
completed++;
|
|
115
|
+
if (!quiet) process.stdout.write(`\r${progressBar(completed, urls.length, `${url.slice(0, 60)}`)}`);
|
|
374
116
|
});
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
117
|
+
if (!quiet) process.stdout.write("\n");
|
|
118
|
+
await inspector.appendHistory({
|
|
119
|
+
userId: store.userId,
|
|
120
|
+
siteId: store.siteIdFor(siteUrl)
|
|
121
|
+
}, records);
|
|
122
|
+
if (json) console.log(JSON.stringify({
|
|
123
|
+
site: siteUrl,
|
|
124
|
+
inspected: records.length,
|
|
125
|
+
failed,
|
|
126
|
+
failures,
|
|
127
|
+
records
|
|
128
|
+
}, null, 2));
|
|
129
|
+
else if (!quiet) {
|
|
130
|
+
logger.success(`Inspected ${records.length}/${urls.length} URL(s)`);
|
|
131
|
+
if (failed > 0) {
|
|
132
|
+
logger.warn(`${failed} failed:`);
|
|
133
|
+
for (const f of failures.slice(0, 5)) console.log(` ${f.url}: ${f.error}`);
|
|
134
|
+
if (failures.length > 5) console.log(` ... and ${failures.length - 5} more`);
|
|
135
|
+
}
|
|
389
136
|
}
|
|
137
|
+
if (failed > 0) process.exit(1);
|
|
390
138
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}) }
|
|
394
|
-
});
|
|
395
|
-
const entitiesCommand = defineCommand({
|
|
396
|
-
meta: {
|
|
397
|
-
name: "entities",
|
|
398
|
-
description: "Manage local entity snapshots (URL inspections, sitemaps, indexing metadata)"
|
|
399
|
-
},
|
|
400
|
-
subCommands: {
|
|
401
|
-
inspect: inspectSubCommand,
|
|
402
|
-
show: showSubCommand,
|
|
403
|
-
sitemaps: defineCommand({
|
|
139
|
+
}),
|
|
140
|
+
show: defineCommand({
|
|
404
141
|
meta: {
|
|
405
|
-
name: "
|
|
406
|
-
description: "
|
|
142
|
+
name: "show",
|
|
143
|
+
description: "Print the latest inspection record for a URL from the local entity store"
|
|
144
|
+
},
|
|
145
|
+
args: {
|
|
146
|
+
...OUTPUT_ARGS,
|
|
147
|
+
site: {
|
|
148
|
+
type: "string",
|
|
149
|
+
alias: "s",
|
|
150
|
+
description: "Site URL (defaults to config.defaultSite or prompt)"
|
|
151
|
+
},
|
|
152
|
+
url: {
|
|
153
|
+
type: "positional",
|
|
154
|
+
required: true,
|
|
155
|
+
description: "URL to look up"
|
|
156
|
+
}
|
|
407
157
|
},
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
158
|
+
async run({ args }) {
|
|
159
|
+
const { json } = applyOutputMode(args);
|
|
160
|
+
const ctx = await createCommandContext({
|
|
161
|
+
needsAuth: true,
|
|
162
|
+
needsStore: true
|
|
163
|
+
});
|
|
164
|
+
const store = ctx.store;
|
|
165
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
166
|
+
const record = await findLatestInspection(createInspectionStore({ dataSource: store.dataSource }), {
|
|
167
|
+
userId: store.userId,
|
|
168
|
+
siteId: store.siteIdFor(siteUrl)
|
|
169
|
+
}, String(args.url));
|
|
170
|
+
if (!record) {
|
|
171
|
+
logger.warn(`No inspection record for ${args.url}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
if (json) {
|
|
175
|
+
console.log(JSON.stringify(record, null, 2));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(` \x1B[1m${record.url}\x1B[0m`);
|
|
180
|
+
console.log(` Inspected: ${record.inspectedAt}`);
|
|
181
|
+
if (record.indexStatus) console.log(` Index: ${record.indexStatus}`);
|
|
182
|
+
if (record.lastCrawlTime) console.log(` Last crawl: ${record.lastCrawlTime}`);
|
|
183
|
+
if (record.googleCanonical) console.log(` Canonical: ${record.googleCanonical}`);
|
|
184
|
+
if (record.coverageState) console.log(` Coverage: ${record.coverageState}`);
|
|
185
|
+
if (record.mobileUsabilityVerdict) console.log(` Mobile: ${record.mobileUsabilityVerdict}`);
|
|
186
|
+
if (record.richResultsVerdict) console.log(` Rich results: ${record.richResultsVerdict}`);
|
|
187
|
+
console.log();
|
|
411
188
|
}
|
|
412
189
|
}),
|
|
413
|
-
indexing:
|
|
190
|
+
indexing: defineCommand({
|
|
191
|
+
meta: {
|
|
192
|
+
name: "indexing",
|
|
193
|
+
description: "Snapshot Indexing API metadata per URL"
|
|
194
|
+
},
|
|
195
|
+
subCommands: { snapshot: defineCommand({
|
|
196
|
+
meta: {
|
|
197
|
+
name: "snapshot",
|
|
198
|
+
description: "Fetch Indexing API metadata (latest update/remove per URL) and persist to the local entity store"
|
|
199
|
+
},
|
|
200
|
+
args: {
|
|
201
|
+
site: {
|
|
202
|
+
type: "string",
|
|
203
|
+
alias: "s",
|
|
204
|
+
description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
|
|
205
|
+
},
|
|
206
|
+
file: {
|
|
207
|
+
type: "string",
|
|
208
|
+
alias: "f",
|
|
209
|
+
description: "Path to a file with one URL per line. If omitted, reads from stdin."
|
|
210
|
+
},
|
|
211
|
+
concurrency: {
|
|
212
|
+
type: "string",
|
|
213
|
+
alias: "c",
|
|
214
|
+
default: "4",
|
|
215
|
+
description: "Concurrent in-flight getMetadata calls (default: 4)"
|
|
216
|
+
},
|
|
217
|
+
...OUTPUT_ARGS
|
|
218
|
+
},
|
|
219
|
+
async run({ args }) {
|
|
220
|
+
const { quiet } = applyOutputMode(args);
|
|
221
|
+
const ctx = await createCommandContext({
|
|
222
|
+
needsAuth: true,
|
|
223
|
+
needsStore: true
|
|
224
|
+
});
|
|
225
|
+
const client = ctx.client;
|
|
226
|
+
const store = ctx.store;
|
|
227
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
228
|
+
const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
|
|
229
|
+
const urls = await readUrlList({ file: args.file ? String(args.file) : void 0 });
|
|
230
|
+
if (urls.length === 0) {
|
|
231
|
+
logger.warn("No URLs to fetch metadata for.");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const records = [];
|
|
235
|
+
const failures = [];
|
|
236
|
+
let completed = 0;
|
|
237
|
+
await runWithConcurrency(urls, concurrency, async (url) => {
|
|
238
|
+
const result = await client.indexing.getMetadata(url).catch((err) => err);
|
|
239
|
+
if (result instanceof Error) if (INDEXING_NOT_FOUND_RE.test(result.message)) records.push({
|
|
240
|
+
url,
|
|
241
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
242
|
+
});
|
|
243
|
+
else failures.push({
|
|
244
|
+
url,
|
|
245
|
+
error: result.message
|
|
246
|
+
});
|
|
247
|
+
else records.push({
|
|
248
|
+
url,
|
|
249
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
250
|
+
latestUpdateAt: result.latestUpdate?.notifyTime ?? void 0,
|
|
251
|
+
latestRemoveAt: result.latestRemove?.notifyTime ?? void 0,
|
|
252
|
+
raw: result
|
|
253
|
+
});
|
|
254
|
+
completed++;
|
|
255
|
+
if (!quiet) process.stdout.write(`\r${progressBar(completed, urls.length, url.slice(0, 60))}`);
|
|
256
|
+
});
|
|
257
|
+
if (!quiet) process.stdout.write("\n");
|
|
258
|
+
await createIndexingMetadataStore({ dataSource: store.dataSource }).writeBatch({
|
|
259
|
+
userId: store.userId,
|
|
260
|
+
siteId: store.siteIdFor(siteUrl)
|
|
261
|
+
}, records);
|
|
262
|
+
if (!quiet) {
|
|
263
|
+
logger.success(`Captured metadata for ${records.length}/${urls.length} URL(s)`);
|
|
264
|
+
if (failures.length > 0) {
|
|
265
|
+
logger.warn(`${failures.length} failed:`);
|
|
266
|
+
for (const f of failures.slice(0, 5)) console.log(` ${f.url}: ${f.error}`);
|
|
267
|
+
if (failures.length > 5) console.log(` ... and ${failures.length - 5} more`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (failures.length > 0) process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
}) }
|
|
273
|
+
})
|
|
414
274
|
}
|
|
415
275
|
});
|
|
416
276
|
export { entitiesCommand };
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
+
import { resolveCliEnvironment } from "../environment.mjs";
|
|
1
2
|
import { OUTPUT_ARGS, applyOutputMode, logger, noSubcommandSelected } from "../utils.mjs";
|
|
2
3
|
import { createCommandContext } from "../context.mjs";
|
|
3
4
|
import { gscErrorHandler } from "../error-handler.mjs";
|
|
4
|
-
import { loadSitemapUrls } from "../sitemap.mjs";
|
|
5
|
+
import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
|
|
5
6
|
import process from "node:process";
|
|
6
7
|
import { defineCommand } from "citty";
|
|
7
8
|
import { fetchSitemap } from "gscdump";
|
|
8
|
-
import {
|
|
9
|
+
import { createGscdumpV1Client } from "@gscdump/sdk/v1";
|
|
10
|
+
const HOSTED_ARGS = {
|
|
11
|
+
"api-root": {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Hosted API root; defaults to GSCDUMP_API_ROOT or https://gscdump.com/api/_gscdump"
|
|
14
|
+
},
|
|
15
|
+
"api-key": {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Hosted API key; defaults to GSCDUMP_API_KEY"
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function hostedClient(args) {
|
|
21
|
+
const environment = resolveCliEnvironment().values;
|
|
22
|
+
const apiRoot = String(args["api-root"] || environment.GSCDUMP_API_ROOT || "https://gscdump.com/api/_gscdump");
|
|
23
|
+
const apiKey = String(args["api-key"] || environment.GSCDUMP_API_KEY || "");
|
|
24
|
+
if (!apiKey) throw new Error("Hosted sitemap reads require --api-key or GSCDUMP_API_KEY");
|
|
25
|
+
return createGscdumpV1Client({
|
|
26
|
+
apiRoot,
|
|
27
|
+
credential: apiKey
|
|
28
|
+
});
|
|
29
|
+
}
|
|
9
30
|
const listCommand = defineCommand({
|
|
10
31
|
meta: {
|
|
11
32
|
name: "list",
|
|
@@ -197,7 +218,7 @@ const sitemapsCommand = defineCommand({
|
|
|
197
218
|
async run({ args }) {
|
|
198
219
|
const { json } = applyOutputMode(args);
|
|
199
220
|
const domain = String(args.domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
200
|
-
const discovery = await
|
|
221
|
+
const discovery = await discoverLiveSitemap(domain);
|
|
201
222
|
const url = discovery._tag === "found" ? discovery.url : null;
|
|
202
223
|
if (json) {
|
|
203
224
|
console.log(JSON.stringify({
|
|
@@ -208,7 +229,7 @@ const sitemapsCommand = defineCommand({
|
|
|
208
229
|
return;
|
|
209
230
|
}
|
|
210
231
|
if (discovery._tag === "incomplete") {
|
|
211
|
-
logger.error(`Sitemap discovery was incomplete: ${discovery.failures[0]
|
|
232
|
+
logger.error(`Sitemap discovery was incomplete: ${discovery.failures[0] ?? "unknown failure"}`);
|
|
212
233
|
process.exit(1);
|
|
213
234
|
}
|
|
214
235
|
if (!url) {
|
|
@@ -266,6 +287,191 @@ const sitemapsCommand = defineCommand({
|
|
|
266
287
|
if (!complete) logger.warn("Sitemap walk was truncated or a child document failed");
|
|
267
288
|
for (const u of urls) console.log(u);
|
|
268
289
|
}
|
|
290
|
+
}),
|
|
291
|
+
current: defineCommand({
|
|
292
|
+
meta: {
|
|
293
|
+
name: "current",
|
|
294
|
+
description: "Read the hosted canonical sitemap generation and feed list"
|
|
295
|
+
},
|
|
296
|
+
args: {
|
|
297
|
+
...OUTPUT_ARGS,
|
|
298
|
+
...HOSTED_ARGS,
|
|
299
|
+
"site-id": {
|
|
300
|
+
type: "positional",
|
|
301
|
+
required: true,
|
|
302
|
+
description: "Hosted gscdump site id"
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
async run({ args }) {
|
|
306
|
+
const { json } = applyOutputMode(args);
|
|
307
|
+
const result = await hostedClient(args).getSiteSitemaps({ params: { siteId: String(args["site-id"]) } });
|
|
308
|
+
if (json) {
|
|
309
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const generation = result.data.generation;
|
|
313
|
+
console.log(generation ? `Generation ${generation.id}, observed ${new Date(generation.observedAt).toISOString()}` : "No canonical generation");
|
|
314
|
+
for (const sitemap of result.data.sitemaps) console.log(`${sitemap.path}\t${sitemap.urlCount}`);
|
|
315
|
+
}
|
|
316
|
+
}),
|
|
317
|
+
history: defineCommand({
|
|
318
|
+
meta: {
|
|
319
|
+
name: "history",
|
|
320
|
+
description: "Read hosted canonical sitemap membership and lastmod changes"
|
|
321
|
+
},
|
|
322
|
+
args: {
|
|
323
|
+
...OUTPUT_ARGS,
|
|
324
|
+
...HOSTED_ARGS,
|
|
325
|
+
"site-id": {
|
|
326
|
+
type: "positional",
|
|
327
|
+
required: true,
|
|
328
|
+
description: "Hosted gscdump site id"
|
|
329
|
+
},
|
|
330
|
+
"days": {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: "History window in days"
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
async run({ args }) {
|
|
336
|
+
const { json } = applyOutputMode(args);
|
|
337
|
+
const days = args.days ? Number.parseInt(String(args.days), 10) : void 0;
|
|
338
|
+
const result = await hostedClient(args).getSiteSitemapChanges({
|
|
339
|
+
params: { siteId: String(args["site-id"]) },
|
|
340
|
+
query: { ...days ? { days } : {} }
|
|
341
|
+
});
|
|
342
|
+
console.log(JSON.stringify(result.data, null, json ? 2 : 0));
|
|
343
|
+
}
|
|
344
|
+
}),
|
|
345
|
+
membership: defineCommand({
|
|
346
|
+
meta: {
|
|
347
|
+
name: "membership",
|
|
348
|
+
description: "Query exact hosted membership evidence for comma-separated URLs"
|
|
349
|
+
},
|
|
350
|
+
args: {
|
|
351
|
+
...OUTPUT_ARGS,
|
|
352
|
+
...HOSTED_ARGS,
|
|
353
|
+
"site-id": {
|
|
354
|
+
type: "positional",
|
|
355
|
+
required: true,
|
|
356
|
+
description: "Hosted gscdump site id"
|
|
357
|
+
},
|
|
358
|
+
"urls": {
|
|
359
|
+
type: "string",
|
|
360
|
+
required: true,
|
|
361
|
+
description: "Comma-separated exact URLs"
|
|
362
|
+
},
|
|
363
|
+
"generation": {
|
|
364
|
+
type: "string",
|
|
365
|
+
description: "Pin the query to a generation id"
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
async run({ args }) {
|
|
369
|
+
const { json } = applyOutputMode(args);
|
|
370
|
+
const urls = String(args.urls).split(",").map((url) => url.trim()).filter(Boolean);
|
|
371
|
+
const result = await hostedClient(args).querySitemapMembership({
|
|
372
|
+
params: { siteId: String(args["site-id"]) },
|
|
373
|
+
body: {
|
|
374
|
+
urls,
|
|
375
|
+
...args.generation ? { generationId: String(args.generation) } : {}
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
if (json) {
|
|
379
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
for (const evidence of result.data.evidence) {
|
|
383
|
+
const detail = evidence._tag === "present" ? `${evidence.feedpath}\t${evidence.lastmod ?? ""}` : evidence._tag === "absent" ? String(evidence.observedAt) : evidence.reason;
|
|
384
|
+
console.log(`${evidence._tag}\t${evidence.url}\t${detail}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}),
|
|
388
|
+
lastmod: defineCommand({
|
|
389
|
+
meta: {
|
|
390
|
+
name: "lastmod",
|
|
391
|
+
description: "List a generation-pinned hosted page of URL lastmod evidence"
|
|
392
|
+
},
|
|
393
|
+
args: {
|
|
394
|
+
...OUTPUT_ARGS,
|
|
395
|
+
...HOSTED_ARGS,
|
|
396
|
+
"site-id": {
|
|
397
|
+
type: "positional",
|
|
398
|
+
required: true,
|
|
399
|
+
description: "Hosted gscdump site id"
|
|
400
|
+
},
|
|
401
|
+
"generation": {
|
|
402
|
+
type: "string",
|
|
403
|
+
description: "Pin to a generation id"
|
|
404
|
+
},
|
|
405
|
+
"feedpath": {
|
|
406
|
+
type: "string",
|
|
407
|
+
description: "Restrict to one exact sitemap feed URL"
|
|
408
|
+
},
|
|
409
|
+
"cursor": {
|
|
410
|
+
type: "string",
|
|
411
|
+
description: "Opaque cursor from the prior page"
|
|
412
|
+
},
|
|
413
|
+
"limit": {
|
|
414
|
+
type: "string",
|
|
415
|
+
description: "Page size, maximum 10000"
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
async run({ args }) {
|
|
419
|
+
const { json } = applyOutputMode(args);
|
|
420
|
+
const result = await hostedClient(args).listSitemapUrls({
|
|
421
|
+
params: { siteId: String(args["site-id"]) },
|
|
422
|
+
query: {
|
|
423
|
+
...args.generation ? { generationId: String(args.generation) } : {},
|
|
424
|
+
...args.feedpath ? { feedpath: String(args.feedpath) } : {},
|
|
425
|
+
...args.cursor ? { cursor: String(args.cursor) } : {},
|
|
426
|
+
...args.limit ? { limit: Number.parseInt(String(args.limit), 10) } : {}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
if (json) {
|
|
430
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
for (const item of result.data.items) console.log(`${item.url}\t${item.lastmod ?? ""}\t${item.feedpath}`);
|
|
434
|
+
if (result.data.page.nextCursor) logger.info(`Next cursor: ${result.data.page.nextCursor}`);
|
|
435
|
+
}
|
|
436
|
+
}),
|
|
437
|
+
export: defineCommand({
|
|
438
|
+
meta: {
|
|
439
|
+
name: "export",
|
|
440
|
+
description: "Get a generation-pinned hosted bulk export descriptor"
|
|
441
|
+
},
|
|
442
|
+
args: {
|
|
443
|
+
...OUTPUT_ARGS,
|
|
444
|
+
...HOSTED_ARGS,
|
|
445
|
+
"site-id": {
|
|
446
|
+
type: "positional",
|
|
447
|
+
required: true,
|
|
448
|
+
description: "Hosted gscdump site id"
|
|
449
|
+
},
|
|
450
|
+
"generation": {
|
|
451
|
+
type: "string",
|
|
452
|
+
description: "Pin to a generation id"
|
|
453
|
+
},
|
|
454
|
+
"feedpath": {
|
|
455
|
+
type: "string",
|
|
456
|
+
description: "Restrict to one exact sitemap feed URL"
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
async run({ args }) {
|
|
460
|
+
const { json } = applyOutputMode(args);
|
|
461
|
+
const result = await hostedClient(args).getSitemapExport({
|
|
462
|
+
params: { siteId: String(args["site-id"]) },
|
|
463
|
+
query: {
|
|
464
|
+
...args.generation ? { generationId: String(args.generation) } : {},
|
|
465
|
+
...args.feedpath ? { feedpath: String(args.feedpath) } : {}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
if (json) {
|
|
469
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (result.data.export._tag === "unavailable") throw new Error(`Sitemap export unavailable: ${result.data.export.reason}`);
|
|
473
|
+
console.log(result.data.export.url);
|
|
474
|
+
}
|
|
269
475
|
})
|
|
270
476
|
},
|
|
271
477
|
async run({ args }) {
|
|
@@ -275,7 +481,12 @@ const sitemapsCommand = defineCommand({
|
|
|
275
481
|
"submit",
|
|
276
482
|
"delete",
|
|
277
483
|
"discover",
|
|
278
|
-
"urls"
|
|
484
|
+
"urls",
|
|
485
|
+
"current",
|
|
486
|
+
"history",
|
|
487
|
+
"membership",
|
|
488
|
+
"lastmod",
|
|
489
|
+
"export"
|
|
279
490
|
])) return;
|
|
280
491
|
await listCommand.run?.({
|
|
281
492
|
args,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { discoverLiveSitemap } from "../../sitemap.mjs";
|
|
1
2
|
import { diagnostics } from "../handlers/diagnostics.mjs";
|
|
2
3
|
import { batchInspectUrls as batchInspectUrls$1, batchRequestIndexing as batchRequestIndexing$1, getIndexingStatus, requestIndexing as requestIndexing$1 } from "../handlers/indexing.mjs";
|
|
3
4
|
import { listReports, runReportHandler } from "../handlers/reports.mjs";
|
|
@@ -5,7 +6,6 @@ import { getSitemap, listSitesWithSitemaps } from "../handlers/sites.mjs";
|
|
|
5
6
|
import "../handlers/index.mjs";
|
|
6
7
|
import { batchInspectUrlsInput, batchRequestIndexingInput, getIndexingStatusInput, inspectUrlInput, listReportsInput, listSitemapsInput, listSitesInput, requestIndexingInput, runReportInput, sitemapInput } from "../types.mjs";
|
|
7
8
|
import { addSite, deleteSite, getIndexingMetadata, getVerificationToken, getVerifiedSite, googleSearchConsole, listVerifiedSites, runSequentialBatch, unverifySite, verifySite } from "gscdump";
|
|
8
|
-
import { discoverSitemapResult } from "gscdump/sitemap";
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
async function runMcpSearchAnalyticsQuery(client, args) {
|
|
@@ -386,7 +386,7 @@ function createGscMcpServer(options) {
|
|
|
386
386
|
inputSchema: z.object({ domain: z.string().describe("Domain (e.g., example.com)") }).shape
|
|
387
387
|
}, async ({ domain }) => {
|
|
388
388
|
const cleaned = String(domain).replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
389
|
-
const discovery = await
|
|
389
|
+
const discovery = await discoverLiveSitemap(cleaned);
|
|
390
390
|
return { content: [{
|
|
391
391
|
type: "text",
|
|
392
392
|
text: JSON.stringify({
|
package/dist/package.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var version = "
|
|
1
|
+
var version = "2.0.0";
|
|
2
2
|
export { version };
|
package/dist/sitemap.mjs
CHANGED
|
@@ -1,27 +1,89 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { createSitemapReader, parseRobotsSitemaps } from "sitemapd";
|
|
2
|
+
import { createFetchDocumentLoader } from "sitemapd/fetch";
|
|
3
|
+
const COMMON_PATHS = [
|
|
4
|
+
"/sitemap.xml",
|
|
5
|
+
"/sitemap_index.xml",
|
|
6
|
+
"/sitemap-index.xml",
|
|
7
|
+
"/sitemaps.xml"
|
|
8
|
+
];
|
|
9
|
+
function publicHttpTarget(url) {
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = new URL(url);
|
|
13
|
+
} catch {
|
|
14
|
+
return {
|
|
15
|
+
_tag: "deny",
|
|
16
|
+
reason: "invalid URL"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? { _tag: "allow" } : {
|
|
20
|
+
_tag: "deny",
|
|
21
|
+
reason: "only HTTP(S) sitemap targets are supported"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function createLiveReader(fetch = globalThis.fetch) {
|
|
25
|
+
return createSitemapReader({
|
|
26
|
+
loadDocument: createFetchDocumentLoader({ fetch }),
|
|
27
|
+
authorizeTarget: ({ url }) => publicHttpTarget(url)
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function readFailure(result) {
|
|
31
|
+
if (result._tag === "not_found") return `${result.url}: HTTP ${result.status}`;
|
|
32
|
+
return `${result.url}: ${result.code ?? result.reason}: ${result.detail}`;
|
|
11
33
|
}
|
|
12
34
|
async function loadSitemapUrls(url, options = {}) {
|
|
13
|
-
const result = await
|
|
14
|
-
if (result.
|
|
35
|
+
const result = await createLiveReader().walk(url, options);
|
|
36
|
+
if (result.documentsRead === 0) return {
|
|
15
37
|
_tag: "error",
|
|
16
|
-
message:
|
|
38
|
+
message: result.failures[0] ? readFailure(result.failures[0].result) : "No sitemap document was found"
|
|
17
39
|
};
|
|
18
40
|
return {
|
|
19
41
|
_tag: "ok",
|
|
20
42
|
value: {
|
|
21
43
|
urls: result.entries.map((entry) => entry.loc),
|
|
22
|
-
complete: result.complete,
|
|
44
|
+
complete: result._tag === "complete",
|
|
23
45
|
documentsRead: result.documentsRead
|
|
24
46
|
}
|
|
25
47
|
};
|
|
26
48
|
}
|
|
27
|
-
|
|
49
|
+
async function discoverLiveSitemap(input, fetch = globalThis.fetch) {
|
|
50
|
+
let origin;
|
|
51
|
+
try {
|
|
52
|
+
origin = new URL(/^https?:\/\//i.test(input) ? input : `https://${input}`).origin;
|
|
53
|
+
} catch {
|
|
54
|
+
return {
|
|
55
|
+
_tag: "incomplete",
|
|
56
|
+
failures: [`${input}: invalid site URL`]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const reader = createLiveReader(fetch);
|
|
60
|
+
const failures = [];
|
|
61
|
+
for (const path of COMMON_PATHS) {
|
|
62
|
+
const result = await reader.read(`${origin}${path}`);
|
|
63
|
+
if (result._tag === "ok") return {
|
|
64
|
+
_tag: "found",
|
|
65
|
+
url: result.url,
|
|
66
|
+
source: "common_path"
|
|
67
|
+
};
|
|
68
|
+
if (result._tag !== "not_found" && !(result._tag === "failure" && result.reason === "document")) failures.push(readFailure(result));
|
|
69
|
+
}
|
|
70
|
+
const robotsUrl = `${origin}/robots.txt`;
|
|
71
|
+
const robots = await fetch(robotsUrl, { redirect: "manual" }).then(async (response) => response.ok ? response.text() : "").catch((error) => {
|
|
72
|
+
failures.push(`${robotsUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
return "";
|
|
74
|
+
});
|
|
75
|
+
for (const reference of parseRobotsSitemaps(robots, robotsUrl)) {
|
|
76
|
+
const result = await reader.read(reference.loc);
|
|
77
|
+
if (result._tag === "ok") return {
|
|
78
|
+
_tag: "found",
|
|
79
|
+
url: result.url,
|
|
80
|
+
source: "robots"
|
|
81
|
+
};
|
|
82
|
+
if (result._tag !== "not_found") failures.push(readFailure(result));
|
|
83
|
+
}
|
|
84
|
+
return failures.length > 0 ? {
|
|
85
|
+
_tag: "incomplete",
|
|
86
|
+
failures
|
|
87
|
+
} : { _tag: "not_found" };
|
|
88
|
+
}
|
|
89
|
+
export { discoverLiveSitemap, loadSitemapUrls };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"description": "CLI for Google Search Console - dump, query, and run MCP server",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -49,11 +49,13 @@
|
|
|
49
49
|
"google-auth-library": "^10.9.1",
|
|
50
50
|
"ofetch": "^1.5.1",
|
|
51
51
|
"open": "^11.0.0",
|
|
52
|
+
"sitemapd": "^0.1.0",
|
|
52
53
|
"zod": "^4.4.3",
|
|
53
|
-
"@gscdump/
|
|
54
|
-
"@gscdump/
|
|
55
|
-
"gscdump": "^
|
|
56
|
-
"@gscdump/
|
|
54
|
+
"@gscdump/engine": "^2.0.0",
|
|
55
|
+
"@gscdump/analysis": "^2.0.0",
|
|
56
|
+
"@gscdump/engine-gsc-api": "^2.0.0",
|
|
57
|
+
"@gscdump/sdk": "^2.0.0",
|
|
58
|
+
"gscdump": "^2.0.0"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
59
61
|
"vitest": "^4.1.10"
|