@gscdump/cli 0.37.2 → 0.37.4
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/bin/gscdump.mjs +3 -0
- package/dist/_chunks/analysis-local.mjs +88 -0
- package/dist/_chunks/analyze.mjs +332 -0
- package/dist/_chunks/auth.mjs +476 -0
- package/dist/_chunks/auth2.mjs +293 -0
- package/dist/_chunks/config.mjs +93 -0
- package/dist/_chunks/config2.mjs +232 -0
- package/dist/_chunks/context.mjs +69 -0
- package/dist/_chunks/doctor.mjs +400 -0
- package/dist/_chunks/dump.mjs +193 -0
- package/dist/_chunks/entities.mjs +414 -0
- package/dist/_chunks/env-file.mjs +53 -0
- package/dist/_chunks/environment.mjs +30 -0
- package/dist/_chunks/error-handler.mjs +86 -0
- package/dist/_chunks/indexing.mjs +314 -0
- package/dist/_chunks/init.mjs +190 -0
- package/dist/_chunks/inspect.mjs +203 -0
- package/dist/_chunks/mcp.mjs +867 -0
- package/dist/_chunks/native-duckdb.mjs +46 -0
- package/dist/_chunks/profile.mjs +290 -0
- package/dist/_chunks/query.mjs +537 -0
- package/dist/_chunks/report.mjs +243 -0
- package/dist/_chunks/sitemaps.mjs +274 -0
- package/dist/_chunks/sites.mjs +500 -0
- package/dist/_chunks/store.mjs +652 -0
- package/dist/_chunks/sync.mjs +489 -0
- package/dist/_chunks/utils.mjs +191 -0
- package/dist/cli.d.mts +28 -0
- package/dist/cli.mjs +104 -0
- package/package.json +15 -7
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -7330
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { OUTPUT_ARGS, applyOutputMode, logger, runWithConcurrency } from "./utils.mjs";
|
|
2
|
+
import { createCommandContext } from "./context.mjs";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { createIndexingMetadataStore, createInspectionStore, createSitemapStore } from "@gscdump/engine/entities";
|
|
8
|
+
import { progressBar } from "gscdump";
|
|
9
|
+
const INSPECTION_QPD_PER_PROPERTY = 2e3;
|
|
10
|
+
const INDEXING_NOT_FOUND_RE = /\b404\b|NOT_FOUND/i;
|
|
11
|
+
const INSPECTION_HISTORY_LOOKBACK_MONTHS = 24;
|
|
12
|
+
async function findLatestInspection(inspector, ctx, url) {
|
|
13
|
+
const now = /* @__PURE__ */ new Date();
|
|
14
|
+
const buckets = [];
|
|
15
|
+
for (let i = 0; i < INSPECTION_HISTORY_LOOKBACK_MONTHS; i++) {
|
|
16
|
+
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - i, 1));
|
|
17
|
+
buckets.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`);
|
|
18
|
+
}
|
|
19
|
+
buckets.push("unknown");
|
|
20
|
+
for (const yearMonth of buckets) {
|
|
21
|
+
const matches = (await inspector.loadHistory(ctx, yearMonth))?.records.filter((r) => r.url === url) ?? [];
|
|
22
|
+
if (matches.length > 0) return matches.reduce((a, b) => a.inspectedAt >= b.inspectedAt ? a : b);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function readUrlList(opts) {
|
|
26
|
+
if (opts.file) return (await readFile(opts.file, "utf8")).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
27
|
+
const chunks = [];
|
|
28
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
29
|
+
return Buffer.concat(chunks).toString("utf8").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
const inspectSubCommand = defineCommand({
|
|
32
|
+
meta: {
|
|
33
|
+
name: "inspect",
|
|
34
|
+
description: "Run URL Inspection for a list of URLs and persist results to the local entity store"
|
|
35
|
+
},
|
|
36
|
+
args: {
|
|
37
|
+
site: {
|
|
38
|
+
type: "string",
|
|
39
|
+
alias: "s",
|
|
40
|
+
description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
|
|
41
|
+
},
|
|
42
|
+
file: {
|
|
43
|
+
type: "string",
|
|
44
|
+
alias: "f",
|
|
45
|
+
description: "Path to a file with one URL per line. If omitted, reads from stdin."
|
|
46
|
+
},
|
|
47
|
+
limit: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: `Max URLs to inspect this run (default: ${INSPECTION_QPD_PER_PROPERTY}, the per-property GSC daily quota)`
|
|
50
|
+
},
|
|
51
|
+
concurrency: {
|
|
52
|
+
type: "string",
|
|
53
|
+
alias: "c",
|
|
54
|
+
default: "4",
|
|
55
|
+
description: "Concurrent in-flight inspect calls (default: 4)"
|
|
56
|
+
},
|
|
57
|
+
...OUTPUT_ARGS
|
|
58
|
+
},
|
|
59
|
+
async run({ args }) {
|
|
60
|
+
const { json, quiet } = applyOutputMode(args);
|
|
61
|
+
const ctx = await createCommandContext({
|
|
62
|
+
needsAuth: true,
|
|
63
|
+
needsStore: true
|
|
64
|
+
});
|
|
65
|
+
const client = ctx.client;
|
|
66
|
+
const store = ctx.store;
|
|
67
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
68
|
+
const limit = args.limit ? Number.parseInt(String(args.limit), 10) : INSPECTION_QPD_PER_PROPERTY;
|
|
69
|
+
const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
|
|
70
|
+
const urls = (await readUrlList({ file: args.file ? String(args.file) : void 0 })).slice(0, limit);
|
|
71
|
+
if (urls.length === 0) {
|
|
72
|
+
logger.warn("No URLs to inspect.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (urls.length === limit && limit < INSPECTION_QPD_PER_PROPERTY) logger.info(`Capping at --limit ${limit}`);
|
|
76
|
+
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.`);
|
|
77
|
+
const inspector = createInspectionStore({ dataSource: store.dataSource });
|
|
78
|
+
let completed = 0;
|
|
79
|
+
let failed = 0;
|
|
80
|
+
const records = [];
|
|
81
|
+
const failures = [];
|
|
82
|
+
await runWithConcurrency(urls, concurrency, async (url) => {
|
|
83
|
+
const result = await client.inspect(siteUrl, url).catch((err) => err);
|
|
84
|
+
if (result instanceof Error) {
|
|
85
|
+
failed++;
|
|
86
|
+
failures.push({
|
|
87
|
+
url,
|
|
88
|
+
error: result.message
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
const ix = result.inspectionResult;
|
|
92
|
+
const indexStatus = ix?.indexStatusResult;
|
|
93
|
+
records.push({
|
|
94
|
+
url,
|
|
95
|
+
inspectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
96
|
+
indexStatus: indexStatus?.verdict ?? void 0,
|
|
97
|
+
lastCrawlTime: indexStatus?.lastCrawlTime ?? void 0,
|
|
98
|
+
googleCanonical: indexStatus?.googleCanonical ?? void 0,
|
|
99
|
+
userCanonical: indexStatus?.userCanonical ?? void 0,
|
|
100
|
+
coverageState: indexStatus?.coverageState ?? void 0,
|
|
101
|
+
robotsTxtState: indexStatus?.robotsTxtState ?? void 0,
|
|
102
|
+
indexingState: indexStatus?.indexingState ?? void 0,
|
|
103
|
+
pageFetchState: indexStatus?.pageFetchState ?? void 0,
|
|
104
|
+
mobileUsabilityVerdict: ix?.mobileUsabilityResult?.verdict ?? void 0,
|
|
105
|
+
richResultsVerdict: ix?.richResultsResult?.verdict ?? void 0,
|
|
106
|
+
raw: ix
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
completed++;
|
|
110
|
+
if (!quiet) process.stdout.write(`\r${progressBar(completed, urls.length, `${url.slice(0, 60)}`)}`);
|
|
111
|
+
});
|
|
112
|
+
if (!quiet) process.stdout.write("\n");
|
|
113
|
+
await inspector.appendHistory({
|
|
114
|
+
userId: store.userId,
|
|
115
|
+
siteId: store.siteIdFor(siteUrl)
|
|
116
|
+
}, records);
|
|
117
|
+
if (json) console.log(JSON.stringify({
|
|
118
|
+
site: siteUrl,
|
|
119
|
+
inspected: records.length,
|
|
120
|
+
failed,
|
|
121
|
+
failures,
|
|
122
|
+
records
|
|
123
|
+
}, null, 2));
|
|
124
|
+
else if (!quiet) {
|
|
125
|
+
logger.success(`Inspected ${records.length}/${urls.length} URL(s)`);
|
|
126
|
+
if (failed > 0) {
|
|
127
|
+
logger.warn(`${failed} failed:`);
|
|
128
|
+
for (const f of failures.slice(0, 5)) console.log(` ${f.url}: ${f.error}`);
|
|
129
|
+
if (failures.length > 5) console.log(` ... and ${failures.length - 5} more`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (failed > 0) process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const showSubCommand = defineCommand({
|
|
136
|
+
meta: {
|
|
137
|
+
name: "show",
|
|
138
|
+
description: "Print the latest inspection record for a URL from the local entity store"
|
|
139
|
+
},
|
|
140
|
+
args: {
|
|
141
|
+
...OUTPUT_ARGS,
|
|
142
|
+
site: {
|
|
143
|
+
type: "string",
|
|
144
|
+
alias: "s",
|
|
145
|
+
description: "Site URL (defaults to config.defaultSite or prompt)"
|
|
146
|
+
},
|
|
147
|
+
url: {
|
|
148
|
+
type: "positional",
|
|
149
|
+
required: true,
|
|
150
|
+
description: "URL to look up"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
async run({ args }) {
|
|
154
|
+
const { json } = applyOutputMode(args);
|
|
155
|
+
const ctx = await createCommandContext({
|
|
156
|
+
needsAuth: true,
|
|
157
|
+
needsStore: true
|
|
158
|
+
});
|
|
159
|
+
const store = ctx.store;
|
|
160
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
161
|
+
const record = await findLatestInspection(createInspectionStore({ dataSource: store.dataSource }), {
|
|
162
|
+
userId: store.userId,
|
|
163
|
+
siteId: store.siteIdFor(siteUrl)
|
|
164
|
+
}, String(args.url));
|
|
165
|
+
if (!record) {
|
|
166
|
+
logger.warn(`No inspection record for ${args.url}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
if (json) {
|
|
170
|
+
console.log(JSON.stringify(record, null, 2));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(` \x1B[1m${record.url}\x1B[0m`);
|
|
175
|
+
console.log(` Inspected: ${record.inspectedAt}`);
|
|
176
|
+
if (record.indexStatus) console.log(` Index: ${record.indexStatus}`);
|
|
177
|
+
if (record.lastCrawlTime) console.log(` Last crawl: ${record.lastCrawlTime}`);
|
|
178
|
+
if (record.googleCanonical) console.log(` Canonical: ${record.googleCanonical}`);
|
|
179
|
+
if (record.coverageState) console.log(` Coverage: ${record.coverageState}`);
|
|
180
|
+
if (record.mobileUsabilityVerdict) console.log(` Mobile: ${record.mobileUsabilityVerdict}`);
|
|
181
|
+
if (record.richResultsVerdict) console.log(` Rich results: ${record.richResultsVerdict}`);
|
|
182
|
+
console.log();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
const sitemapsSnapshotSubCommand = defineCommand({
|
|
186
|
+
meta: {
|
|
187
|
+
name: "snapshot",
|
|
188
|
+
description: "Fetch current sitemap state from GSC and persist to the local entity store"
|
|
189
|
+
},
|
|
190
|
+
args: {
|
|
191
|
+
...OUTPUT_ARGS,
|
|
192
|
+
site: {
|
|
193
|
+
type: "string",
|
|
194
|
+
alias: "s",
|
|
195
|
+
description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
async run({ args }) {
|
|
199
|
+
const { json, quiet } = applyOutputMode(args);
|
|
200
|
+
const ctx = await createCommandContext({
|
|
201
|
+
needsAuth: true,
|
|
202
|
+
needsStore: true
|
|
203
|
+
});
|
|
204
|
+
const client = ctx.client;
|
|
205
|
+
const store = ctx.store;
|
|
206
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
207
|
+
const apiSitemaps = await client.sitemaps.list(siteUrl);
|
|
208
|
+
const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
209
|
+
const records = apiSitemaps.filter((s) => typeof s.path === "string").map((s) => ({
|
|
210
|
+
path: s.path,
|
|
211
|
+
capturedAt,
|
|
212
|
+
lastDownloaded: s.lastDownloaded ?? void 0,
|
|
213
|
+
lastSubmitted: s.lastSubmitted ?? void 0,
|
|
214
|
+
type: s.type ?? void 0,
|
|
215
|
+
isPending: s.isPending ?? void 0,
|
|
216
|
+
isSitemapsIndex: s.isSitemapsIndex ?? void 0,
|
|
217
|
+
errors: s.errors ?? void 0,
|
|
218
|
+
warnings: s.warnings ?? void 0,
|
|
219
|
+
contents: s.contents?.map((c) => ({
|
|
220
|
+
type: c.type ?? void 0,
|
|
221
|
+
submitted: c.submitted ?? void 0,
|
|
222
|
+
indexed: c.indexed ?? void 0
|
|
223
|
+
})),
|
|
224
|
+
raw: s
|
|
225
|
+
}));
|
|
226
|
+
await createSitemapStore({ dataSource: store.dataSource }).writeSnapshot({
|
|
227
|
+
userId: store.userId,
|
|
228
|
+
siteId: store.siteIdFor(siteUrl)
|
|
229
|
+
}, records);
|
|
230
|
+
if (json) {
|
|
231
|
+
console.log(JSON.stringify({
|
|
232
|
+
site: siteUrl,
|
|
233
|
+
capturedAt,
|
|
234
|
+
records
|
|
235
|
+
}, null, 2));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!quiet) {
|
|
239
|
+
logger.success(`Captured ${records.length} sitemap(s) for ${siteUrl}`);
|
|
240
|
+
for (const r of records) {
|
|
241
|
+
const errors = r.errors && r.errors !== "0" ? ` \x1B[31merr=${r.errors}\x1B[0m` : "";
|
|
242
|
+
const warnings = r.warnings && r.warnings !== "0" ? ` \x1B[33mwarn=${r.warnings}\x1B[0m` : "";
|
|
243
|
+
const downloaded = r.lastDownloaded ? ` last=${r.lastDownloaded}` : "";
|
|
244
|
+
console.log(` ${r.path}${downloaded}${errors}${warnings}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const sitemapsShowSubCommand = defineCommand({
|
|
250
|
+
meta: {
|
|
251
|
+
name: "show",
|
|
252
|
+
description: "Print the latest captured sitemap state for a feedpath"
|
|
253
|
+
},
|
|
254
|
+
args: {
|
|
255
|
+
...OUTPUT_ARGS,
|
|
256
|
+
site: {
|
|
257
|
+
type: "string",
|
|
258
|
+
alias: "s",
|
|
259
|
+
description: "Site URL (defaults to config.defaultSite or prompt)"
|
|
260
|
+
},
|
|
261
|
+
path: {
|
|
262
|
+
type: "positional",
|
|
263
|
+
required: true,
|
|
264
|
+
description: "Sitemap path (feedpath)"
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
async run({ args }) {
|
|
268
|
+
const { json } = applyOutputMode(args);
|
|
269
|
+
const ctx = await createCommandContext({
|
|
270
|
+
needsAuth: true,
|
|
271
|
+
needsStore: true
|
|
272
|
+
});
|
|
273
|
+
const store = ctx.store;
|
|
274
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
275
|
+
const record = await createSitemapStore({ dataSource: store.dataSource }).getLatest({
|
|
276
|
+
userId: store.userId,
|
|
277
|
+
siteId: store.siteIdFor(siteUrl)
|
|
278
|
+
}, String(args.path));
|
|
279
|
+
if (!record) {
|
|
280
|
+
logger.warn(`No sitemap record for ${args.path}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
if (json) {
|
|
284
|
+
console.log(JSON.stringify(record, null, 2));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
console.log();
|
|
288
|
+
console.log(` \x1B[1m${record.path}\x1B[0m`);
|
|
289
|
+
console.log(` Captured: ${record.capturedAt}`);
|
|
290
|
+
if (record.lastDownloaded) console.log(` Downloaded: ${record.lastDownloaded}`);
|
|
291
|
+
if (record.lastSubmitted) console.log(` Submitted: ${record.lastSubmitted}`);
|
|
292
|
+
if (record.type) console.log(` Type: ${record.type}`);
|
|
293
|
+
if (record.errors) console.log(` Errors: ${record.errors}`);
|
|
294
|
+
if (record.warnings) console.log(` Warnings: ${record.warnings}`);
|
|
295
|
+
if (record.contents?.length) {
|
|
296
|
+
console.log(` Contents:`);
|
|
297
|
+
for (const c of record.contents) {
|
|
298
|
+
const bits = [
|
|
299
|
+
c.type,
|
|
300
|
+
c.submitted && `submitted=${c.submitted}`,
|
|
301
|
+
c.indexed && `indexed=${c.indexed}`
|
|
302
|
+
].filter(Boolean).join(" ");
|
|
303
|
+
console.log(` ${bits}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
console.log();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
const indexingSubCommand = defineCommand({
|
|
310
|
+
meta: {
|
|
311
|
+
name: "indexing",
|
|
312
|
+
description: "Snapshot Indexing API metadata per URL"
|
|
313
|
+
},
|
|
314
|
+
subCommands: { snapshot: defineCommand({
|
|
315
|
+
meta: {
|
|
316
|
+
name: "snapshot",
|
|
317
|
+
description: "Fetch Indexing API metadata (latest update/remove per URL) and persist to the local entity store"
|
|
318
|
+
},
|
|
319
|
+
args: {
|
|
320
|
+
site: {
|
|
321
|
+
type: "string",
|
|
322
|
+
alias: "s",
|
|
323
|
+
description: "Site URL (e.g., sc-domain:example.com); defaults to config.defaultSite or prompt"
|
|
324
|
+
},
|
|
325
|
+
file: {
|
|
326
|
+
type: "string",
|
|
327
|
+
alias: "f",
|
|
328
|
+
description: "Path to a file with one URL per line. If omitted, reads from stdin."
|
|
329
|
+
},
|
|
330
|
+
concurrency: {
|
|
331
|
+
type: "string",
|
|
332
|
+
alias: "c",
|
|
333
|
+
default: "4",
|
|
334
|
+
description: "Concurrent in-flight getMetadata calls (default: 4)"
|
|
335
|
+
},
|
|
336
|
+
...OUTPUT_ARGS
|
|
337
|
+
},
|
|
338
|
+
async run({ args }) {
|
|
339
|
+
const { quiet } = applyOutputMode(args);
|
|
340
|
+
const ctx = await createCommandContext({
|
|
341
|
+
needsAuth: true,
|
|
342
|
+
needsStore: true
|
|
343
|
+
});
|
|
344
|
+
const client = ctx.client;
|
|
345
|
+
const store = ctx.store;
|
|
346
|
+
const siteUrl = await ctx.resolveSite(args.site ? String(args.site) : void 0);
|
|
347
|
+
const concurrency = Math.max(1, Number.parseInt(String(args.concurrency), 10) || 4);
|
|
348
|
+
const urls = await readUrlList({ file: args.file ? String(args.file) : void 0 });
|
|
349
|
+
if (urls.length === 0) {
|
|
350
|
+
logger.warn("No URLs to fetch metadata for.");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const records = [];
|
|
354
|
+
const failures = [];
|
|
355
|
+
let completed = 0;
|
|
356
|
+
await runWithConcurrency(urls, concurrency, async (url) => {
|
|
357
|
+
const result = await client.indexing.getMetadata(url).catch((err) => err);
|
|
358
|
+
if (result instanceof Error) if (INDEXING_NOT_FOUND_RE.test(result.message)) records.push({
|
|
359
|
+
url,
|
|
360
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
361
|
+
});
|
|
362
|
+
else failures.push({
|
|
363
|
+
url,
|
|
364
|
+
error: result.message
|
|
365
|
+
});
|
|
366
|
+
else records.push({
|
|
367
|
+
url,
|
|
368
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
369
|
+
latestUpdateAt: result.latestUpdate?.notifyTime ?? void 0,
|
|
370
|
+
latestRemoveAt: result.latestRemove?.notifyTime ?? void 0,
|
|
371
|
+
raw: result
|
|
372
|
+
});
|
|
373
|
+
completed++;
|
|
374
|
+
if (!quiet) process.stdout.write(`\r${progressBar(completed, urls.length, url.slice(0, 60))}`);
|
|
375
|
+
});
|
|
376
|
+
if (!quiet) process.stdout.write("\n");
|
|
377
|
+
await createIndexingMetadataStore({ dataSource: store.dataSource }).writeBatch({
|
|
378
|
+
userId: store.userId,
|
|
379
|
+
siteId: store.siteIdFor(siteUrl)
|
|
380
|
+
}, records);
|
|
381
|
+
if (!quiet) {
|
|
382
|
+
logger.success(`Captured metadata for ${records.length}/${urls.length} URL(s)`);
|
|
383
|
+
if (failures.length > 0) {
|
|
384
|
+
logger.warn(`${failures.length} failed:`);
|
|
385
|
+
for (const f of failures.slice(0, 5)) console.log(` ${f.url}: ${f.error}`);
|
|
386
|
+
if (failures.length > 5) console.log(` ... and ${failures.length - 5} more`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (failures.length > 0) process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
}) }
|
|
392
|
+
});
|
|
393
|
+
const entitiesCommand = defineCommand({
|
|
394
|
+
meta: {
|
|
395
|
+
name: "entities",
|
|
396
|
+
description: "Manage local entity snapshots (URL inspections, sitemaps, indexing metadata)"
|
|
397
|
+
},
|
|
398
|
+
subCommands: {
|
|
399
|
+
inspect: inspectSubCommand,
|
|
400
|
+
show: showSubCommand,
|
|
401
|
+
sitemaps: defineCommand({
|
|
402
|
+
meta: {
|
|
403
|
+
name: "sitemaps",
|
|
404
|
+
description: "Snapshot and inspect sitemap state per site"
|
|
405
|
+
},
|
|
406
|
+
subCommands: {
|
|
407
|
+
snapshot: sitemapsSnapshotSubCommand,
|
|
408
|
+
show: sitemapsShowSubCommand
|
|
409
|
+
}
|
|
410
|
+
}),
|
|
411
|
+
indexing: indexingSubCommand
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
export { entitiesCommand };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useCliRuntime } from "./config.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
const ENV_LINE_RE = /^([^=]+)=(.*)$/;
|
|
6
|
+
function parseEnvFile(envPath) {
|
|
7
|
+
let content;
|
|
8
|
+
try {
|
|
9
|
+
content = fs.readFileSync(envPath, "utf-8");
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const env = {};
|
|
14
|
+
for (const line of content.split("\n")) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
17
|
+
const match = trimmed.match(ENV_LINE_RE);
|
|
18
|
+
if (!match) continue;
|
|
19
|
+
const key = match[1].trim();
|
|
20
|
+
let value = match[2].trim();
|
|
21
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
22
|
+
env[key] = value;
|
|
23
|
+
}
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
const appliedEnvKeys = /* @__PURE__ */ new Set();
|
|
27
|
+
let loadedEnvPath = null;
|
|
28
|
+
function getAppliedEnvKeys() {
|
|
29
|
+
return appliedEnvKeys;
|
|
30
|
+
}
|
|
31
|
+
function getLoadedEnvPath() {
|
|
32
|
+
return loadedEnvPath;
|
|
33
|
+
}
|
|
34
|
+
function loadEnvFromCwd() {
|
|
35
|
+
return loadEnvFile({
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
env: useCliRuntime().environment
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function loadEnvFile(opts) {
|
|
41
|
+
const envPath = path.join(opts.cwd, ".env");
|
|
42
|
+
const parsed = parseEnvFile(envPath);
|
|
43
|
+
if (!parsed) return [];
|
|
44
|
+
loadedEnvPath = envPath;
|
|
45
|
+
const applied = [];
|
|
46
|
+
for (const [key, value] of Object.entries(parsed)) if (opts.env[key] === void 0) {
|
|
47
|
+
opts.env[key] = value;
|
|
48
|
+
applied.push(key);
|
|
49
|
+
appliedEnvKeys.add(key);
|
|
50
|
+
}
|
|
51
|
+
return applied;
|
|
52
|
+
}
|
|
53
|
+
export { getAppliedEnvKeys, getLoadedEnvPath, loadEnvFromCwd, parseEnvFile };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useCliRuntime } from "./config.mjs";
|
|
2
|
+
function resolveCliEnvironment(values = useCliRuntime().environment) {
|
|
3
|
+
return {
|
|
4
|
+
accessToken: values.GSC_ACCESS_TOKEN || values.GOOGLE_ACCESS_TOKEN,
|
|
5
|
+
clientId: values.GSC_CLIENT_ID || values.GOOGLE_CLIENT_ID,
|
|
6
|
+
clientSecret: values.GSC_CLIENT_SECRET || values.GOOGLE_CLIENT_SECRET,
|
|
7
|
+
configDir: values.GSCDUMP_CONFIG_DIR,
|
|
8
|
+
forceColor: Boolean(values.FORCE_COLOR),
|
|
9
|
+
noColor: Boolean(values.NO_COLOR),
|
|
10
|
+
profile: values.GSCDUMP_PROFILE,
|
|
11
|
+
refreshToken: values.GSC_REFRESH_TOKEN || values.GOOGLE_REFRESH_TOKEN,
|
|
12
|
+
serviceAccountPath: values.GSC_SERVICE_ACCOUNT_JSON || values.GOOGLE_APPLICATION_CREDENTIALS,
|
|
13
|
+
values
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function pickCliEnvironmentValue(names, values = useCliRuntime().environment) {
|
|
17
|
+
for (const envVar of names) {
|
|
18
|
+
const value = values[envVar];
|
|
19
|
+
if (value) return {
|
|
20
|
+
envVar,
|
|
21
|
+
value
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function applyCliEnvironment(updates, values = useCliRuntime().environment) {
|
|
27
|
+
for (const [key, value] of Object.entries(updates)) if (value === void 0) delete values[key];
|
|
28
|
+
else values[key] = value;
|
|
29
|
+
}
|
|
30
|
+
export { applyCliEnvironment, pickCliEnvironmentValue, resolveCliEnvironment };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { formatAuthProvenance, isAuthError } from "./auth.mjs";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { isQueryError } from "gscdump/query";
|
|
4
|
+
import { formatErrorForCli } from "gscdump/api";
|
|
5
|
+
import { isAnalysisError } from "@gscdump/analysis/errors";
|
|
6
|
+
import { isEngineError } from "@gscdump/engine/errors";
|
|
7
|
+
function extractQueryError(error) {
|
|
8
|
+
if (isQueryError(error)) return error;
|
|
9
|
+
const tagged = error.queryError;
|
|
10
|
+
return isQueryError(tagged) ? tagged : null;
|
|
11
|
+
}
|
|
12
|
+
function extractEngineError(error) {
|
|
13
|
+
if (isEngineError(error)) return error;
|
|
14
|
+
const tagged = error.engineError;
|
|
15
|
+
return isEngineError(tagged) ? tagged : null;
|
|
16
|
+
}
|
|
17
|
+
function extractAnalysisError(error) {
|
|
18
|
+
if (isAnalysisError(error)) return error;
|
|
19
|
+
const tagged = error.analysisError;
|
|
20
|
+
return isAnalysisError(tagged) ? tagged : null;
|
|
21
|
+
}
|
|
22
|
+
function suggestionForTypedError(error) {
|
|
23
|
+
const query = extractQueryError(error);
|
|
24
|
+
if (query) switch (query.kind) {
|
|
25
|
+
case "unresolvable-dataset": return "This breakdown spans separate stored tables. Re-run with --live to compute it from the GSC API.";
|
|
26
|
+
case "unsupported-capability": return `The local engine lacks the "${query.capability}" capability for ${query.context}. Re-run with --live.`;
|
|
27
|
+
case "missing-date-range": return "Add a date range, e.g. --start YYYY-MM-DD --end YYYY-MM-DD.";
|
|
28
|
+
case "invalid-row-limit":
|
|
29
|
+
case "invalid-start-row":
|
|
30
|
+
case "invalid-data-state":
|
|
31
|
+
case "invalid-aggregation-type":
|
|
32
|
+
case "invalid-builder-state": return "";
|
|
33
|
+
}
|
|
34
|
+
const analysis = extractAnalysisError(error);
|
|
35
|
+
if (analysis) switch (analysis.kind) {
|
|
36
|
+
case "missing-report-param": return `Report "${analysis.report}" needs --${analysis.param}.`;
|
|
37
|
+
case "missing-comparison-window": return `Report "${analysis.report}" is a comparison report; pass --vs prev-period (or --vs yoy).`;
|
|
38
|
+
case "missing-brand-terms": return "Pass --brand-terms to segment branded vs non-branded queries.";
|
|
39
|
+
case "unknown-report": return `Unknown report. Available: ${analysis.available.join(", ")}.`;
|
|
40
|
+
case "unknown-analyzer": return "Run `gscdump report list` to see available reports/analyzers.";
|
|
41
|
+
case "required-step-failed": return suggestionForTypedError(analysis.cause);
|
|
42
|
+
}
|
|
43
|
+
const engine = extractEngineError(error);
|
|
44
|
+
if (engine) switch (engine.kind) {
|
|
45
|
+
case "attached-table-missing": return `Local store is missing table(s): ${engine.missing.join(", ")}. Run \`gscdump sync\` first, or pass --live.`;
|
|
46
|
+
case "analyzer-not-found":
|
|
47
|
+
case "analyzer-capability-missing": return "This analysis is not available for the chosen source. Re-run with --live.";
|
|
48
|
+
case "invalid-year-month":
|
|
49
|
+
case "invalid-snapshot-filename":
|
|
50
|
+
case "invalid-schema-identifier": return "The local store appears corrupt. Re-run `gscdump sync` to rebuild it.";
|
|
51
|
+
default: return "";
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
var LocalStoreUnsupportedError = class extends Error {
|
|
56
|
+
tool;
|
|
57
|
+
mode;
|
|
58
|
+
constructor(tool, mode) {
|
|
59
|
+
super(`analysis "${tool}" has no implementation for the ${mode} source`);
|
|
60
|
+
this.name = "LocalStoreUnsupportedError";
|
|
61
|
+
this.tool = tool;
|
|
62
|
+
this.mode = mode;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
async function gscErrorHandler(error) {
|
|
66
|
+
console.error();
|
|
67
|
+
if (error instanceof LocalStoreUnsupportedError) {
|
|
68
|
+
console.error(formatErrorForCli(error));
|
|
69
|
+
if (error.mode === "local") console.error("Pass --live to run against the GSC API.");
|
|
70
|
+
console.error();
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
console.error(formatErrorForCli(error));
|
|
74
|
+
const typedHint = suggestionForTypedError(error);
|
|
75
|
+
if (typedHint) {
|
|
76
|
+
console.error();
|
|
77
|
+
console.error(typedHint);
|
|
78
|
+
}
|
|
79
|
+
if (isAuthError(error)) {
|
|
80
|
+
console.error();
|
|
81
|
+
console.error(await formatAuthProvenance());
|
|
82
|
+
}
|
|
83
|
+
console.error();
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
export { LocalStoreUnsupportedError, gscErrorHandler };
|