@gscdump/cli 5.0.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis-local.mjs +2 -2
- package/dist/auth-state.mjs +60 -7
- package/dist/bing-hosted.mjs +26 -5
- package/dist/cli.d.mts +6 -0
- package/dist/cli.mjs +47 -6
- package/dist/command-meta.mjs +2 -2
- package/dist/commands/analyze.mjs +2 -2
- package/dist/commands/auth.mjs +13 -14
- package/dist/commands/bing.mjs +7 -2
- package/dist/commands/config.mjs +5 -15
- package/dist/commands/doctor.mjs +2 -2
- package/dist/commands/dump.mjs +2 -4
- package/dist/commands/entities.mjs +3 -9
- package/dist/commands/indexing-urls.mjs +2 -2
- package/dist/commands/indexing-watch.mjs +10 -7
- package/dist/commands/indexing.mjs +6 -22
- package/dist/commands/init.mjs +3 -6
- package/dist/commands/mcp.mjs +1 -1
- package/dist/commands/papercut.mjs +2 -5
- package/dist/commands/profile.mjs +5 -14
- package/dist/commands/query.mjs +16 -40
- package/dist/commands/report.mjs +2 -2
- package/dist/commands/sitemaps.mjs +3 -10
- package/dist/commands/sites.mjs +7 -12
- package/dist/commands/skill.mjs +1 -4
- package/dist/commands/sync.mjs +2 -8
- package/dist/config.mjs +1 -1
- package/dist/context.mjs +3 -3
- package/dist/error-handler.mjs +72 -16
- package/dist/hosted-auth.mjs +9 -1
- package/dist/hosted-session.mjs +17 -0
- package/dist/hosted-site.mjs +41 -18
- package/dist/mcp/errors.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/route.mjs +5 -9
- package/dist/runtime.d.mts +2 -0
- package/dist/runtime.mjs +1 -0
- package/dist/stop.mjs +23 -0
- package/dist/utils.mjs +13 -14
- package/package.json +7 -7
- package/skills/gscdump/SKILL.md +43 -4
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandLineError } from "../stop.mjs";
|
|
1
2
|
import { indexingCommandMeta } from "../command-meta.mjs";
|
|
2
3
|
import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, readUrlList } from "../utils.mjs";
|
|
3
4
|
import { createCommandContext } from "../context.mjs";
|
|
@@ -5,7 +6,6 @@ import { loadSitemapUrls } from "../sitemap.mjs";
|
|
|
5
6
|
import { indexingSummaryCommand } from "./indexing-summary.mjs";
|
|
6
7
|
import { indexingUrlsCommand } from "./indexing-urls.mjs";
|
|
7
8
|
import { indexingWatchCommand } from "./indexing-watch.mjs";
|
|
8
|
-
import process from "node:process";
|
|
9
9
|
import { defineCommand } from "citty";
|
|
10
10
|
import { batchRequestIndexing, getIndexingMetadata, requestIndexing, runSequentialBatch } from "gscdump/indexing";
|
|
11
11
|
const RETRIES_ARG = { retries: {
|
|
@@ -16,10 +16,7 @@ async function resolveUrlSource(args) {
|
|
|
16
16
|
const fromSitemap = args["from-sitemap"];
|
|
17
17
|
if (fromSitemap) {
|
|
18
18
|
const result = await loadSitemapUrls(String(fromSitemap));
|
|
19
|
-
if (result._tag === "error") {
|
|
20
|
-
logger.error(`Sitemap fetch failed: ${result.message}`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
19
|
+
if (result._tag === "error") throw new Error(`Sitemap fetch failed: ${result.message}`);
|
|
23
20
|
if (!result.value.complete) logger.warn("Sitemap walk was incomplete; indexing only the URLs that were read");
|
|
24
21
|
return result.value.urls;
|
|
25
22
|
}
|
|
@@ -214,20 +211,10 @@ const batchCommand = defineCommand({
|
|
|
214
211
|
const delayMs = parseIntegerOption(args["delay-ms"], "--delay-ms", 0) ?? 100;
|
|
215
212
|
const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 1;
|
|
216
213
|
const urls = await resolveUrlSource(args);
|
|
217
|
-
if (urls.length === 0)
|
|
218
|
-
logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
|
|
219
|
-
process.exit(1);
|
|
220
|
-
}
|
|
214
|
+
if (urls.length === 0) throw commandLineError("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
|
|
221
215
|
const type = String(args.type);
|
|
222
|
-
if (type !== "URL_UPDATED" && type !== "URL_DELETED") {
|
|
223
|
-
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
if (urls.length > INDEXING_DAILY_QUOTA && !args.yes && !args.json) {
|
|
227
|
-
logger.warn(`Submitting ${urls.length} URLs but the Indexing API daily quota is ${INDEXING_DAILY_QUOTA}/day.`);
|
|
228
|
-
logger.warn(`Excess submissions will fail with quota errors. Pass --yes to proceed anyway.`);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
216
|
+
if (type !== "URL_UPDATED" && type !== "URL_DELETED") throw commandLineError(`Invalid --type: ${type}. Use URL_UPDATED or URL_DELETED.`);
|
|
217
|
+
if (urls.length > INDEXING_DAILY_QUOTA && !args.yes && !args.json) throw commandLineError(`Submitting ${urls.length} URLs but the Indexing API daily quota is ${INDEXING_DAILY_QUOTA}/day. Excess submissions will fail with quota errors. Pass --yes to proceed anyway.`);
|
|
231
218
|
if (urls.length > INDEXING_DAILY_QUOTA && !args.json && !args.quiet) logger.warn(`Proceeding with ${urls.length} URLs (over the ${INDEXING_DAILY_QUOTA}/day quota). Excess will fail.`);
|
|
232
219
|
const ctx = await createCommandContext({
|
|
233
220
|
needsAuth: true,
|
|
@@ -290,10 +277,7 @@ const batchStatusCommand = defineCommand({
|
|
|
290
277
|
const delayMs = parseIntegerOption(args["delay-ms"], "--delay-ms", 0) ?? 100;
|
|
291
278
|
const concurrency = parseIntegerOption(args.concurrency, "--concurrency") ?? 1;
|
|
292
279
|
const urls = await resolveUrlSource(args);
|
|
293
|
-
if (urls.length === 0)
|
|
294
|
-
logger.error("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
|
|
295
|
-
process.exit(1);
|
|
296
|
-
}
|
|
280
|
+
if (urls.length === 0) throw commandLineError("No URLs provided. Pass URLs as args, --file, --from-sitemap, or stdin.");
|
|
297
281
|
const ctx = await createCommandContext({
|
|
298
282
|
needsAuth: true,
|
|
299
283
|
fetchOptions: { retry }
|
package/dist/commands/init.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { initCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
3
2
|
import { saveAuthentication } from "../auth-state.mjs";
|
|
3
|
+
import { initCommandMeta } from "../command-meta.mjs";
|
|
4
4
|
import { defaultDataDir, loadConfig, saveConfig } from "../config.mjs";
|
|
5
5
|
import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
|
|
6
6
|
import { ACCESS_NOT_SET_UP, getAuth, loadTokens, resolveBYOK, resolveServiceAccount } from "../auth.mjs";
|
|
@@ -100,10 +100,7 @@ const initCommand = defineCommand({
|
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
if (!canPrompt()) {
|
|
103
|
-
if (!await loadTokens()) {
|
|
104
|
-
logger.error(`Init cannot prompt without a terminal.\n${ACCESS_NOT_SET_UP}`);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
103
|
+
if (!await loadTokens()) throw new Error(`Init cannot prompt without a terminal.\n${ACCESS_NOT_SET_UP}`);
|
|
107
104
|
await saveConfig({
|
|
108
105
|
...config,
|
|
109
106
|
dataDir: config.dataDir ?? defaultDataDir()
|
|
@@ -114,7 +111,7 @@ const initCommand = defineCommand({
|
|
|
114
111
|
return;
|
|
115
112
|
}
|
|
116
113
|
console.log();
|
|
117
|
-
console.log(" \x1B[1mWelcome to
|
|
114
|
+
console.log(" \x1B[1mWelcome to gscdump\x1B[0m");
|
|
118
115
|
console.log(" \x1B[90mGoogle Search Console data extraction CLI\x1B[0m");
|
|
119
116
|
console.log();
|
|
120
117
|
const dataDir = args.store ? await promptDataDir(config.dataDir) : void 0;
|
package/dist/commands/mcp.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mcpCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { runWithCliRuntime, useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { mcpCommandMeta } from "../command-meta.mjs";
|
|
3
3
|
import { VERSION } from "../utils.mjs";
|
|
4
4
|
import { probeAuth } from "../auth.mjs";
|
|
5
5
|
import { createCommandContext } from "../context.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { papercutCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { papercutCommandMeta } from "../command-meta.mjs";
|
|
3
3
|
import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
|
|
4
4
|
import { buildPapercutBody, resolvePapercutUrl, submitPapercut } from "../papercut.mjs";
|
|
5
5
|
import process from "node:process";
|
|
@@ -62,10 +62,7 @@ const papercutCommand = defineCommand({
|
|
|
62
62
|
fetch: globalThis.fetch,
|
|
63
63
|
url
|
|
64
64
|
});
|
|
65
|
-
if (result._tag === "Err")
|
|
66
|
-
logger.error(result.message);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
65
|
+
if (result._tag === "Err") throw new Error(result.message);
|
|
69
66
|
if (json) {
|
|
70
67
|
console.log(JSON.stringify(result.receipt));
|
|
71
68
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { profileCommandMeta } from "../command-meta.mjs";
|
|
2
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { commandLineError } from "../stop.mjs";
|
|
3
|
+
import { profileCommandMeta } from "../command-meta.mjs";
|
|
3
4
|
import { getConfigDir, setConfigDir } from "../config.mjs";
|
|
4
5
|
import { resolveCliEnvironment } from "../environment.mjs";
|
|
5
6
|
import { ACTIVE_MARKER, PROFILES_DIR, ROOT_DIR, getProfileDir, readActiveMarkerSync } from "./profile-selection.mjs";
|
|
@@ -100,10 +101,7 @@ const pathCmd = defineCommand({
|
|
|
100
101
|
} },
|
|
101
102
|
async run({ args }) {
|
|
102
103
|
const name = args.name ? String(args.name) : resolveActiveProfile();
|
|
103
|
-
if (!name)
|
|
104
|
-
logger.error("No profile specified and none active (set --profile, GSCDUMP_PROFILE, or run `gscdump profile use <name>`)");
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
104
|
+
if (!name) throw commandLineError("No profile specified and none active (set --profile, GSCDUMP_PROFILE, or run `gscdump profile use <name>`)");
|
|
107
105
|
console.log(getProfileDir(name));
|
|
108
106
|
}
|
|
109
107
|
});
|
|
@@ -135,11 +133,7 @@ const useCmd = defineCommand({
|
|
|
135
133
|
applyOutputMode(args);
|
|
136
134
|
const name = String(args.name);
|
|
137
135
|
const dir = getProfileDir(name);
|
|
138
|
-
if (!await fs.stat(dir).then(() => true).catch(() => false)) {
|
|
139
|
-
logger.error(`Profile not found: ${name}`);
|
|
140
|
-
logger.info(`Create it with: gscdump profile create ${name}`);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
136
|
+
if (!await fs.stat(dir).then(() => true).catch(() => false)) throw new Error(`Profile not found: ${name}. Create it with: gscdump profile create ${name}`);
|
|
143
137
|
await setActiveProfile(name);
|
|
144
138
|
logger.success(`Active profile: ${name}`);
|
|
145
139
|
}
|
|
@@ -207,10 +201,7 @@ const deleteCmd = defineCommand({
|
|
|
207
201
|
applyOutputMode(args);
|
|
208
202
|
const name = String(args.name);
|
|
209
203
|
const dir = getProfileDir(name);
|
|
210
|
-
if (!await fs.stat(dir).then(() => true).catch(() => false)) {
|
|
211
|
-
logger.error(`Profile not found: ${name}`);
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
204
|
+
if (!await fs.stat(dir).then(() => true).catch(() => false)) throw new Error(`Profile not found: ${name}`);
|
|
214
205
|
if (!args.yes) {
|
|
215
206
|
const ok = await confirm({
|
|
216
207
|
message: `Delete profile "${name}" at ${dir}? Tokens and config will be lost.`,
|
package/dist/commands/query.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { queryCommandMeta } from "../command-meta.mjs";
|
|
2
|
-
import { queryHostedRows } from "../hosted-query.mjs";
|
|
3
1
|
import { useCliRuntime } from "../runtime.mjs";
|
|
2
|
+
import { commandLineError } from "../stop.mjs";
|
|
4
3
|
import { resolveAuthentication } from "../auth-state.mjs";
|
|
4
|
+
import { queryCommandMeta } from "../command-meta.mjs";
|
|
5
5
|
import { loadConfig } from "../config.mjs";
|
|
6
|
+
import { queryHostedRows } from "../hosted-query.mjs";
|
|
6
7
|
import { terminalOutputOptions } from "../render/terminal.mjs";
|
|
7
|
-
import { ALL_SEARCH_TYPES, logger, parseSearchType, toCSV } from "../utils.mjs";
|
|
8
|
+
import { ALL_SEARCH_TYPES, logger, parseSearchType, setJsonOutput, toCSV } from "../utils.mjs";
|
|
8
9
|
import { allTables } from "../local-store.mjs";
|
|
9
10
|
import { createCommandContext, formatSiteResolution } from "../context.mjs";
|
|
10
11
|
import { decideRoute, describeStop, liveNote, readRouteState, readSiteStates, resolveReadSite, stopAtRoute } from "../route.mjs";
|
|
@@ -211,10 +212,8 @@ const queryCommand = defineCommand({
|
|
|
211
212
|
async run({ args }) {
|
|
212
213
|
const ctxConfig = await loadConfig();
|
|
213
214
|
const format = args.format ?? ctxConfig.defaultFormat ?? "json";
|
|
214
|
-
if (format !== "json" && format !== "csv" && format !== "table")
|
|
215
|
-
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
215
|
+
if (format !== "json" && format !== "csv" && format !== "table") throw commandLineError("Invalid --format. Use table, json, or csv.");
|
|
216
|
+
setJsonOutput(format === "json");
|
|
218
217
|
if (args.sql || args.schema) {
|
|
219
218
|
await runSqlMode({
|
|
220
219
|
mode: args.schema ? { kind: "schema" } : {
|
|
@@ -233,31 +232,19 @@ const queryCommand = defineCommand({
|
|
|
233
232
|
const dimNames = await resolveDimensions(args);
|
|
234
233
|
const windowFlags = await promptRange(args);
|
|
235
234
|
const invalidWindow = checkWindowFlags(windowFlags);
|
|
236
|
-
if (invalidWindow)
|
|
237
|
-
logger.error(invalidWindow.message);
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
235
|
+
if (invalidWindow) throw commandLineError(invalidWindow.message);
|
|
240
236
|
await promptFilters(args);
|
|
241
237
|
const limitArg = String(args.limit ?? ctxConfig.defaultLimit ?? 1e3);
|
|
242
238
|
const rowLimit = Number(limitArg);
|
|
243
|
-
if (!POSITIVE_INTEGER_RE.test(limitArg) || !Number.isSafeInteger(rowLimit) || rowLimit < 1)
|
|
244
|
-
logger.error("Invalid --limit. Use a positive safe integer, such as --limit 1000.");
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
239
|
+
if (!POSITIVE_INTEGER_RE.test(limitArg) || !Number.isSafeInteger(rowLimit) || rowLimit < 1) throw commandLineError("Invalid --limit. Use a positive safe integer, such as --limit 1000.");
|
|
247
240
|
const filters = parseFilterArgs(args);
|
|
248
241
|
const searchType = parseSearchType(args.type ?? ctxConfig.defaultSearchType, "--type");
|
|
249
242
|
const localSearchType = searchType ?? "web";
|
|
250
243
|
const dataState = args["data-state"] ? String(args["data-state"]) : ctxConfig.defaultDataState;
|
|
251
244
|
const aggregationType = args["aggregation-type"] ? String(args["aggregation-type"]) : void 0;
|
|
252
245
|
const filterDims = filterDimensions(filters);
|
|
253
|
-
if (dataState && !DATA_STATES.includes(dataState)) {
|
|
254
|
-
|
|
255
|
-
process.exit(1);
|
|
256
|
-
}
|
|
257
|
-
if (aggregationType && !AGGREGATION_TYPES.includes(aggregationType)) {
|
|
258
|
-
logger.error(`Invalid --aggregation-type: ${aggregationType}. Allowed: ${AGGREGATION_TYPES.join(", ")}`);
|
|
259
|
-
process.exit(1);
|
|
260
|
-
}
|
|
246
|
+
if (dataState && !DATA_STATES.includes(dataState)) throw commandLineError(`Invalid --data-state: ${dataState}. Allowed: ${DATA_STATES.join(", ")}`);
|
|
247
|
+
if (aggregationType && !AGGREGATION_TYPES.includes(aggregationType)) throw commandLineError(`Invalid --aggregation-type: ${aggregationType}. Allowed: ${AGGREGATION_TYPES.join(", ")}`);
|
|
261
248
|
const forceLive = Boolean(args.live);
|
|
262
249
|
if (!forceLive && (await resolveAuthentication())._tag === "Hosted") {
|
|
263
250
|
if (dataState || aggregationType) logger.warn("--data-state / --aggregation-type are ignored without --live");
|
|
@@ -341,7 +328,6 @@ const queryCommand = defineCommand({
|
|
|
341
328
|
auth
|
|
342
329
|
});
|
|
343
330
|
const route = decideRoute(req, routeState);
|
|
344
|
-
const json = format === "json";
|
|
345
331
|
if (route.kind === "live") {
|
|
346
332
|
const live = await connect();
|
|
347
333
|
const siteUrl = site ?? await live.resolveSite(void 0);
|
|
@@ -406,7 +392,7 @@ const queryCommand = defineCommand({
|
|
|
406
392
|
}, null, 2));
|
|
407
393
|
return;
|
|
408
394
|
}
|
|
409
|
-
if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, routeState.auth
|
|
395
|
+
if (route.kind === "syncing" || route.kind === "prompt") stopAtRoute(route, req, routeState.auth);
|
|
410
396
|
if (dataState || aggregationType) logger.warn("--data-state / --aggregation-type are ignored without --live");
|
|
411
397
|
const siteUrl = site;
|
|
412
398
|
if (!args.quiet) logger.debug(`Querying ${siteUrl} from local Parquet store...`);
|
|
@@ -417,10 +403,7 @@ const queryCommand = defineCommand({
|
|
|
417
403
|
table,
|
|
418
404
|
searchType: localSearchType,
|
|
419
405
|
...probe ? { profiler: probe.profiler } : {}
|
|
420
|
-
}, state)
|
|
421
|
-
logger.error(`Query failed: ${e.message}`);
|
|
422
|
-
process.exit(1);
|
|
423
|
-
});
|
|
406
|
+
}, state);
|
|
424
407
|
if (probe) logProfile(probe.spans);
|
|
425
408
|
await writeOutput({
|
|
426
409
|
output: {
|
|
@@ -443,10 +426,7 @@ const queryCommand = defineCommand({
|
|
|
443
426
|
async function resolveDimensions(args) {
|
|
444
427
|
if (args.dimensions != null) {
|
|
445
428
|
const dimensions = String(args.dimensions).split(",").map((d) => d.trim());
|
|
446
|
-
if (dimensions.some((d) => !DIMENSIONS.includes(d))) {
|
|
447
|
-
logger.error(`Invalid --dimensions. Use a comma-separated list from: ${DIMENSIONS.join(", ")}.`);
|
|
448
|
-
process.exit(1);
|
|
449
|
-
}
|
|
429
|
+
if (dimensions.some((d) => !DIMENSIONS.includes(d))) throw commandLineError(`Invalid --dimensions. Use a comma-separated list from: ${DIMENSIONS.join(", ")}.`);
|
|
450
430
|
return [...new Set(dimensions)];
|
|
451
431
|
}
|
|
452
432
|
if (args.interactive) {
|
|
@@ -500,10 +480,7 @@ async function promptRange(args) {
|
|
|
500
480
|
}
|
|
501
481
|
function windowOrExit(flags, anchor) {
|
|
502
482
|
const window = parseWindowFlags(flags, DEFAULT_WINDOW, anchor);
|
|
503
|
-
if (!window.ok)
|
|
504
|
-
logger.error(window.error.message);
|
|
505
|
-
process.exit(1);
|
|
506
|
-
}
|
|
483
|
+
if (!window.ok) throw commandLineError(window.error.message);
|
|
507
484
|
return window.value;
|
|
508
485
|
}
|
|
509
486
|
async function promptFilters(args) {
|
|
@@ -595,7 +572,7 @@ async function runSqlMode(opts) {
|
|
|
595
572
|
});
|
|
596
573
|
if (route.kind === "syncing" || route.kind === "prompt") {
|
|
597
574
|
views.close();
|
|
598
|
-
stopAtRoute(route, req, routeState.auth
|
|
575
|
+
stopAtRoute(route, req, routeState.auth);
|
|
599
576
|
}
|
|
600
577
|
}
|
|
601
578
|
}
|
|
@@ -620,8 +597,7 @@ async function runSql(views, sql) {
|
|
|
620
597
|
for (const warning of warnings) logger.warn(warning);
|
|
621
598
|
return {
|
|
622
599
|
...await views.run(sql).catch((e) => {
|
|
623
|
-
|
|
624
|
-
process.exit(1);
|
|
600
|
+
throw new Error(`SQL failed: ${e.message}`, { cause: e });
|
|
625
601
|
}),
|
|
626
602
|
warnings
|
|
627
603
|
};
|
package/dist/commands/report.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { reportCommandMeta } from "../command-meta.mjs";
|
|
2
2
|
import { terminalOutputOptions } from "../render/terminal.mjs";
|
|
3
|
-
import { logger, parseFetchBudget } from "../utils.mjs";
|
|
3
|
+
import { logger, parseFetchBudget, setJsonOutput } from "../utils.mjs";
|
|
4
4
|
import { COMPARISON_FLAGS, PERIOD_FLAGS, parseWindowFlags, windowFlagErrorToException } from "../window.mjs";
|
|
5
5
|
import { analysisNeeds, analyzerSources, analyzerTables, resolveAnalysisSource } from "../analysis-local.mjs";
|
|
6
6
|
import { renderCliReport } from "../render/report.mjs";
|
|
@@ -127,6 +127,7 @@ function makeReportCommand(report) {
|
|
|
127
127
|
...reportArgs
|
|
128
128
|
},
|
|
129
129
|
async run({ args }) {
|
|
130
|
+
setJsonOutput(Boolean(args.json));
|
|
130
131
|
const flags = windowFlags(args);
|
|
131
132
|
const params = buildReportParams(report, args);
|
|
132
133
|
const fetchBudget = parseFetchBudget(args["fetch-budget"]);
|
|
@@ -158,7 +159,6 @@ function makeReportCommand(report) {
|
|
|
158
159
|
const { source, siteUrl, anchor, isLive } = await resolveAnalysisSource({
|
|
159
160
|
site: args.site,
|
|
160
161
|
live: !!args.live,
|
|
161
|
-
json: !!args.json,
|
|
162
162
|
label: `report ${report.id}`,
|
|
163
163
|
types: provisional.map((step) => step.type),
|
|
164
164
|
sources: reportSources(provisional),
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { sitemapsCommandMeta } from "../command-meta.mjs";
|
|
2
2
|
import { OUTPUT_ARGS, applyOutputMode, logger, parseIntegerOption, plural } from "../utils.mjs";
|
|
3
3
|
import { createCommandContext } from "../context.mjs";
|
|
4
|
-
import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
|
|
5
4
|
import { HOSTED_ARGS, resolveHostedSite } from "../hosted-site.mjs";
|
|
6
|
-
import
|
|
5
|
+
import { discoverLiveSitemap, loadSitemapUrls } from "../sitemap.mjs";
|
|
7
6
|
import { defineCommand } from "citty";
|
|
8
7
|
import { fetchSitemap } from "gscdump/sites";
|
|
9
8
|
const HOSTED_SITEMAP_ALTERNATIVE = "read a live sitemap with `gscdump sitemaps urls <sitemap-url>`";
|
|
@@ -207,14 +206,8 @@ const discoverCommand = defineCommand({
|
|
|
207
206
|
}, null, 2));
|
|
208
207
|
return;
|
|
209
208
|
}
|
|
210
|
-
if (discovery._tag === "incomplete") {
|
|
211
|
-
|
|
212
|
-
process.exit(1);
|
|
213
|
-
}
|
|
214
|
-
if (!url) {
|
|
215
|
-
logger.warn(`No sitemap discovered for ${domain}`);
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
209
|
+
if (discovery._tag === "incomplete") throw new Error(`Sitemap discovery was incomplete: ${discovery.failures[0] ?? "unknown failure"}`);
|
|
210
|
+
if (!url) throw new Error(`No sitemap discovered for ${domain}`);
|
|
218
211
|
logger.success(`Discovered sitemap: ${url}`);
|
|
219
212
|
}
|
|
220
213
|
});
|
package/dist/commands/sites.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { commandLineError } from "../stop.mjs";
|
|
2
|
+
import { connectSiteStep, formatHostedSync, getHostedAccount, localModeRequired, resolveAuthentication, siteManagerOf } from "../auth-state.mjs";
|
|
1
3
|
import { sitesCommandMeta } from "../command-meta.mjs";
|
|
2
|
-
import { LOCAL_MODE_REQUIRED, formatHostedSync, getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
|
|
3
4
|
import { OUTPUT_ARGS, applyOutputMode, logger, plural } from "../utils.mjs";
|
|
4
5
|
import { createCommandContext, formatSiteResolution } from "../context.mjs";
|
|
5
6
|
import process from "node:process";
|
|
@@ -20,16 +21,10 @@ function pickDefaultMethod(siteUrl) {
|
|
|
20
21
|
}
|
|
21
22
|
function validateMethod(siteUrl, method) {
|
|
22
23
|
const upper = method.toUpperCase();
|
|
23
|
-
if (!ALL_METHODS.includes(upper)) {
|
|
24
|
-
logger.error(`Invalid --method: ${method}. Valid: ${ALL_METHODS.join(", ")}`);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
24
|
+
if (!ALL_METHODS.includes(upper)) throw commandLineError(`Invalid --method: ${method}. Valid: ${ALL_METHODS.join(", ")}`);
|
|
27
25
|
const site = siteUrlToVerificationSite(siteUrl);
|
|
28
26
|
const allowed = verificationMethodsFor(site);
|
|
29
|
-
if (!allowed.includes(upper)) {
|
|
30
|
-
logger.error(`Method ${upper} not valid for ${site.type === "INET_DOMAIN" ? "domain" : "URL-prefix"} property "${siteUrl}". Valid: ${allowed.join(", ")}`);
|
|
31
|
-
process.exit(1);
|
|
32
|
-
}
|
|
27
|
+
if (!allowed.includes(upper)) throw commandLineError(`Method ${upper} not valid for ${site.type === "INET_DOMAIN" ? "domain" : "URL-prefix"} property "${siteUrl}". Valid: ${allowed.join(", ")}`);
|
|
33
28
|
return upper;
|
|
34
29
|
}
|
|
35
30
|
function printPlacementInstructions(method, siteUrl, token) {
|
|
@@ -422,8 +417,8 @@ const LIST_ARGS = {
|
|
|
422
417
|
}
|
|
423
418
|
};
|
|
424
419
|
async function runListHostedSites(args, authentication) {
|
|
425
|
-
if (args["with-sitemaps"] || args["owner-only"]) throw
|
|
426
|
-
const { sites } = await getHostedAccount(authentication);
|
|
420
|
+
if (args["with-sitemaps"] || args["owner-only"]) throw localModeRequired("--with-sitemaps and --owner-only read Search Console permissions.");
|
|
421
|
+
const { sites, issuer } = await getHostedAccount(authentication);
|
|
427
422
|
if (args.json) {
|
|
428
423
|
console.log(JSON.stringify(sites.map((site) => ({
|
|
429
424
|
siteId: site.siteId,
|
|
@@ -438,7 +433,7 @@ async function runListHostedSites(args, authentication) {
|
|
|
438
433
|
return;
|
|
439
434
|
}
|
|
440
435
|
if (sites.length === 0) {
|
|
441
|
-
logger.warn(
|
|
436
|
+
logger.warn(`Your hosted record has no Sites. ${connectSiteStep(siteManagerOf(authentication, issuer))}`);
|
|
442
437
|
return;
|
|
443
438
|
}
|
|
444
439
|
logger.success(`Found ${plural(sites.length, "hosted Site", "hosted Sites")}:`);
|
package/dist/commands/skill.mjs
CHANGED
|
@@ -33,10 +33,7 @@ const installCommand = defineCommand({
|
|
|
33
33
|
homeDirectory: os.homedir(),
|
|
34
34
|
target: args.target
|
|
35
35
|
});
|
|
36
|
-
if (result._tag === "Err")
|
|
37
|
-
logger.error(result.message);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
36
|
+
if (result._tag === "Err") throw new Error(result.message);
|
|
40
37
|
if (json) {
|
|
41
38
|
console.log(JSON.stringify(result.installation));
|
|
42
39
|
return;
|
package/dist/commands/sync.mjs
CHANGED
|
@@ -395,10 +395,7 @@ const syncCommand = defineCommand({
|
|
|
395
395
|
siteUrls.push(siteUrl);
|
|
396
396
|
continue;
|
|
397
397
|
}
|
|
398
|
-
if (!args["all-sites"])
|
|
399
|
-
logger.error(formatSiteIdCollision(claim.error));
|
|
400
|
-
process.exit(1);
|
|
401
|
-
}
|
|
398
|
+
if (!args["all-sites"]) throw new Error(formatSiteIdCollision(claim.error));
|
|
402
399
|
logger.warn(`Skipped ${siteUrl}. ${formatSiteIdCollision(claim.error)}`);
|
|
403
400
|
}
|
|
404
401
|
const latest = getLatestGscDate();
|
|
@@ -416,10 +413,7 @@ const syncCommand = defineCommand({
|
|
|
416
413
|
now: Date.now(),
|
|
417
414
|
isAlive: isProcessAlive
|
|
418
415
|
});
|
|
419
|
-
if (existing.kind === "running") {
|
|
420
|
-
logger.error(`Another sync is running (pid ${existing.record.pid}, ${existing.record.done}/${existing.record.planned} days). Wait for it to finish, or run \`gscdump sync --status\`.`);
|
|
421
|
-
process.exit(1);
|
|
422
|
-
}
|
|
416
|
+
if (existing.kind === "running") throw new Error(`Another sync is running (pid ${existing.record.pid}, ${existing.record.done}/${existing.record.planned} days). Wait for it to finish, or run \`gscdump sync --status\`.`);
|
|
423
417
|
}
|
|
424
418
|
const ledger = await openQuotaLedger({ dataDir: store.dataDir });
|
|
425
419
|
const pacer = createRequestPacer({
|
package/dist/config.mjs
CHANGED
|
@@ -2,8 +2,8 @@ import { useCliRuntime } from "./runtime.mjs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { SearchTypes } from "gscdump/query";
|
|
6
5
|
import os from "node:os";
|
|
6
|
+
import { SearchTypes } from "gscdump/query";
|
|
7
7
|
function setConfigDir(dir) {
|
|
8
8
|
useCliRuntime().configDir = dir;
|
|
9
9
|
}
|
package/dist/context.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { LOCAL_MODE_REQUIRED, resolveAuthentication } from "./auth-state.mjs";
|
|
1
|
+
import { localModeRequired, resolveAuthentication } from "./auth-state.mjs";
|
|
3
2
|
import { loadResolvedConfig } from "./config.mjs";
|
|
3
|
+
import { openQuotaLedger, quotaFetchOptions } from "./quota-ledger.mjs";
|
|
4
4
|
import { resolveAuth } from "./auth.mjs";
|
|
5
5
|
import { createLocalStore } from "./local-store.mjs";
|
|
6
6
|
import { listStoreSites } from "./store-sites.mjs";
|
|
@@ -12,7 +12,7 @@ const FULL_SITE_URL_RE = /^(?:sc-domain:|https?:\/\/)/i;
|
|
|
12
12
|
async function createCommandContext(opts = {}) {
|
|
13
13
|
const { needsAuth = false, needsStore = false, interactive = false, byok, quota = true } = opts;
|
|
14
14
|
const { config, dataDir } = await loadResolvedConfig();
|
|
15
|
-
if (needsAuth && (await resolveAuthentication())._tag === "Hosted") throw
|
|
15
|
+
if (needsAuth && (await resolveAuthentication())._tag === "Hosted") throw localModeRequired();
|
|
16
16
|
const fetchOptions = needsAuth && quota ? quotaFetchOptions(await openQuotaLedger({ dataDir }), opts.fetchOptions) : opts.fetchOptions;
|
|
17
17
|
const auth = needsAuth ? await resolveAuth({
|
|
18
18
|
interactive,
|
package/dist/error-handler.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import { HOSTED_SESSION_EXPIRED, isSessionExpiredError } from "./hosted-session.mjs";
|
|
2
|
+
import { isUserStop, stopDetailsOf } from "./stop.mjs";
|
|
1
3
|
import { isUsageError } from "./command-registry.mjs";
|
|
2
4
|
import { describeRecordReadRefusal } from "./hosted-query.mjs";
|
|
3
5
|
import { quotaStopOf } from "./quota-ledger.mjs";
|
|
6
|
+
import { isQueryError } from "gscdump/query";
|
|
4
7
|
import { isAnalysisError } from "@gscdump/analysis/errors";
|
|
5
8
|
import { parseRecordReadRefusal } from "@gscdump/contracts";
|
|
6
9
|
import { isEngineError } from "@gscdump/engine/errors";
|
|
7
10
|
import { classifyError } from "gscdump/errors";
|
|
8
|
-
import { isQueryError } from "gscdump/query";
|
|
9
|
-
const HOSTED_KEY_REJECTED_REASON = "gscdump.com rejected the API key";
|
|
10
|
-
const HOSTED_KEY_REJECTED = `${HOSTED_KEY_REJECTED_REASON}. Run \`gscdump auth login --mode hosted --api-key KEY\` with a valid key.`;
|
|
11
11
|
const QUOTA_MESSAGE_RE = /quota|rate\s*limit/i;
|
|
12
12
|
const ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
13
13
|
function suggestionForGscError(error) {
|
|
@@ -109,18 +109,44 @@ function recordReadRefusalOf(error) {
|
|
|
109
109
|
if (!(error instanceof Error) || error.name !== "GscdumpV1Error") return null;
|
|
110
110
|
return parseRecordReadRefusal(error.details);
|
|
111
111
|
}
|
|
112
|
+
function recordReadStop(refusal, text) {
|
|
113
|
+
return {
|
|
114
|
+
code: refusal.reason === "record_not_ready" ? "RECORD_NOT_READY" : "RANGE_NOT_SYNCED",
|
|
115
|
+
message: text.hint ? `${text.message} ${text.hint}` : text.message,
|
|
116
|
+
nextCommand: null
|
|
117
|
+
};
|
|
118
|
+
}
|
|
112
119
|
function describeCliError(error) {
|
|
113
120
|
if (isUsageError(error)) return {
|
|
114
121
|
kind: "usage",
|
|
115
122
|
message: error.message
|
|
116
123
|
};
|
|
124
|
+
const stop = stopDetailsOf(error);
|
|
117
125
|
if (error instanceof Error && error.routeStop !== void 0) return {
|
|
118
126
|
kind: "stop",
|
|
119
|
-
message: error.message
|
|
127
|
+
message: error.message,
|
|
128
|
+
...stop ? { stop } : {}
|
|
120
129
|
};
|
|
121
130
|
if (error instanceof Error && quotaStopOf(error)) return {
|
|
122
131
|
kind: "stop",
|
|
123
|
-
message: error.message
|
|
132
|
+
message: error.message,
|
|
133
|
+
stop: {
|
|
134
|
+
code: "QUOTA_USED_UP",
|
|
135
|
+
message: error.message,
|
|
136
|
+
nextCommand: null
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
if (isUserStop(error) && stop) return {
|
|
140
|
+
kind: "stop",
|
|
141
|
+
message: stop.message,
|
|
142
|
+
stop
|
|
143
|
+
};
|
|
144
|
+
if (error instanceof Error && stop) return {
|
|
145
|
+
kind: "expected",
|
|
146
|
+
message: error.message,
|
|
147
|
+
hint: "",
|
|
148
|
+
showAuthSources: false,
|
|
149
|
+
stop
|
|
124
150
|
};
|
|
125
151
|
if (error instanceof TypeError && error.message === "fetch failed") {
|
|
126
152
|
const cause = error.cause;
|
|
@@ -142,19 +168,24 @@ function describeCliError(error) {
|
|
|
142
168
|
hint: error.mode === "local" ? "Pass --live to run against the GSC API." : "",
|
|
143
169
|
showAuthSources: false
|
|
144
170
|
};
|
|
145
|
-
|
|
146
|
-
if (refusal) return {
|
|
171
|
+
if (isSessionExpiredError(error)) return {
|
|
147
172
|
kind: "expected",
|
|
148
|
-
|
|
149
|
-
showAuthSources: false
|
|
150
|
-
};
|
|
151
|
-
const classified = classifyError(error);
|
|
152
|
-
if (classified.message === HOSTED_KEY_REJECTED) return {
|
|
153
|
-
kind: "expected",
|
|
154
|
-
message: classified.message,
|
|
173
|
+
message: HOSTED_SESSION_EXPIRED.message,
|
|
155
174
|
hint: "",
|
|
156
|
-
showAuthSources: false
|
|
175
|
+
showAuthSources: false,
|
|
176
|
+
stop: HOSTED_SESSION_EXPIRED
|
|
157
177
|
};
|
|
178
|
+
const refusal = recordReadRefusalOf(error);
|
|
179
|
+
if (refusal) {
|
|
180
|
+
const text = describeRecordReadRefusal(refusal);
|
|
181
|
+
return {
|
|
182
|
+
kind: "expected",
|
|
183
|
+
...text,
|
|
184
|
+
showAuthSources: false,
|
|
185
|
+
stop: recordReadStop(refusal, text)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const classified = classifyError(error);
|
|
158
189
|
return {
|
|
159
190
|
kind: "expected",
|
|
160
191
|
message: classified.message.replace(PARAM_NAME_RE, (name) => flagForParam(name)),
|
|
@@ -162,8 +193,33 @@ function describeCliError(error) {
|
|
|
162
193
|
showAuthSources: classified.kind === "auth-expired" || classified.kind === "permission-denied" || isAuthError(error)
|
|
163
194
|
};
|
|
164
195
|
}
|
|
196
|
+
function jsonErrorOf(report) {
|
|
197
|
+
switch (report.kind) {
|
|
198
|
+
case "usage": return {
|
|
199
|
+
code: "USAGE",
|
|
200
|
+
message: report.message,
|
|
201
|
+
nextCommand: null
|
|
202
|
+
};
|
|
203
|
+
case "stop": return report.stop ?? {
|
|
204
|
+
code: "FAILED",
|
|
205
|
+
message: report.message,
|
|
206
|
+
nextCommand: null
|
|
207
|
+
};
|
|
208
|
+
case "expected": return report.stop ?? {
|
|
209
|
+
code: "FAILED",
|
|
210
|
+
message: report.hint ? `${report.message} ${report.hint}` : report.message,
|
|
211
|
+
nextCommand: null
|
|
212
|
+
};
|
|
213
|
+
case "defect": return {
|
|
214
|
+
code: "UNEXPECTED",
|
|
215
|
+
message: report.message,
|
|
216
|
+
nextCommand: null
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
165
220
|
async function reportCliError(error, options) {
|
|
166
221
|
const report = describeCliError(error);
|
|
222
|
+
if (options.json) (options.writeStdout ?? ((value) => console.log(value)))(JSON.stringify({ error: jsonErrorOf(report) }, null, 2));
|
|
167
223
|
const red = (text) => options.color ? `\x1B[31m${text}\x1B[0m` : text;
|
|
168
224
|
const lines = [];
|
|
169
225
|
switch (report.kind) {
|
|
@@ -184,4 +240,4 @@ async function reportCliError(error, options) {
|
|
|
184
240
|
const text = lines.join("\n");
|
|
185
241
|
(options.write ?? ((value) => console.error(value)))(options.color ? text : text.replace(ANSI_RE, ""));
|
|
186
242
|
}
|
|
187
|
-
export {
|
|
243
|
+
export { LocalStoreUnsupportedError, describeCliError, flagForParam, isAuthError, jsonErrorOf, reportCliError };
|
package/dist/hosted-auth.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { userStopError } from "./stop.mjs";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
const ORIGIN = "https://gscdump.com";
|
|
3
4
|
const pollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal("pending") }), z.object({
|
|
@@ -5,6 +6,12 @@ const pollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal(
|
|
|
5
6
|
sessionId: z.string().regex(/^[a-f0-9]{64}$/)
|
|
6
7
|
})]);
|
|
7
8
|
const HOSTED_LOGIN_FAILURE = "Hosted login failed. Run `gscdump auth login --mode hosted` to try again.";
|
|
9
|
+
const LOGIN_CANCELLED = {
|
|
10
|
+
code: "LOGIN_CANCELLED",
|
|
11
|
+
message: "Login cancelled in the browser.",
|
|
12
|
+
nextCommand: "gscdump auth login --mode hosted"
|
|
13
|
+
};
|
|
14
|
+
const loginCancelledBodySchema = z.object({ data: z.object({ details: z.object({ reason: z.literal("login_cancelled") }) }) });
|
|
8
15
|
async function requestJson(request, route, init = {}) {
|
|
9
16
|
const response = await request(`${ORIGIN}/api/cli/auth/${route}`, {
|
|
10
17
|
...init,
|
|
@@ -13,6 +20,7 @@ async function requestJson(request, route, init = {}) {
|
|
|
13
20
|
});
|
|
14
21
|
if (!response.ok) {
|
|
15
22
|
if (response.status === 429 || response.status >= 500) throw new Error("gscdump.com login is temporarily unavailable. Try again later.");
|
|
23
|
+
if (response.status === 403 && loginCancelledBodySchema.safeParse(await response.json().catch(() => null)).success) throw userStopError(LOGIN_CANCELLED);
|
|
16
24
|
throw new Error(HOSTED_LOGIN_FAILURE);
|
|
17
25
|
}
|
|
18
26
|
return response.json();
|
|
@@ -31,4 +39,4 @@ async function loginWithHostedSession(deps) {
|
|
|
31
39
|
}
|
|
32
40
|
throw new Error("Authorization expired. Run `gscdump auth login --mode hosted` to try again.");
|
|
33
41
|
}
|
|
34
|
-
export { loginWithHostedSession };
|
|
42
|
+
export { LOGIN_CANCELLED, loginWithHostedSession };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const sessionExpiredDetailsSchema = z.object({ reason: z.literal("session_expired") });
|
|
3
|
+
const sessionExpiredBodySchema = z.union([z.object({ data: z.object({ details: sessionExpiredDetailsSchema }) }), z.object({ error: z.object({ details: sessionExpiredDetailsSchema }) })]);
|
|
4
|
+
const HOSTED_SESSION_EXPIRED = {
|
|
5
|
+
code: "HOSTED_CREDENTIALS_REJECTED",
|
|
6
|
+
message: "Your gscdump.com session expired. Run `gscdump auth login --mode hosted`.",
|
|
7
|
+
nextCommand: "gscdump auth login --mode hosted"
|
|
8
|
+
};
|
|
9
|
+
function isSessionExpiredBody(body) {
|
|
10
|
+
return sessionExpiredBodySchema.safeParse(body).success;
|
|
11
|
+
}
|
|
12
|
+
function isSessionExpiredError(error) {
|
|
13
|
+
if (!(error instanceof Error) || error.name !== "GscdumpV1Error") return false;
|
|
14
|
+
const { status, details } = error;
|
|
15
|
+
return status === 401 && sessionExpiredDetailsSchema.safeParse(details).success;
|
|
16
|
+
}
|
|
17
|
+
export { HOSTED_SESSION_EXPIRED, isSessionExpiredBody, isSessionExpiredError };
|